Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1170,3 +1170,10 @@ Tokens are configured in the seed config and map to users. Pass them as `Authori
**Microsoft**: OIDC authorization code flow with PKCE support. Also supports client credentials grants and Microsoft Graph users, mail, calendar, and OneDrive routes.

**AWS**: Bearer tokens or IAM access key credentials. Default key pair always seeded: `AKIAIOSFODNN7EXAMPLE` / `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`.

## Context.dev company lookup

The `context` service supports brand retrieval by email domain for onboarding tests.
Create an instance on `context.emulators.dev`, mint an API key and seed explicit
`brands` with `domain` and `title`. Unknown domains return 404; no external Context.dev
credentials or requests are used. Other enrichment modes are not implemented.
17 changes: 17 additions & 0 deletions apps/web/app/docs/context/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Context.dev

A curated emulator for `POST /v1/brand/retrieve` with `type: "by_email"`.
Seed explicit domain/title pairs through `/_emulate/seed`. Only seeded domains
match; unknown domains return 404. No Context.dev key or network call is needed.

```json
{"brands":[{"domain":"company.example","title":"Example Company"}]}
```

Create an instance at `https://context.emulators.dev/_emulate/instances`, then
create an `api-key` credential through its `/_emulate/credentials` endpoint.
Use the returned provider base URL and bearer token for requests. The regular
ledger and fault controls support observing calls and injecting failures.

This subset returns company names with empty logo/color arrays. Other Context
lookup modes, enrichment fields, OAuth, GraphQL and MCP are not implemented.
3 changes: 2 additions & 1 deletion packages/@emulators/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"@emulators/posthog": "workspace:*",
"@emulators/x": "workspace:*",
"@emulators/workos": "workspace:*",
"@emulators/autumn": "workspace:*"
"@emulators/autumn": "workspace:*",
"@emulators/context": "workspace:*"
},
"devDependencies": {
"tsup": "^8",
Expand Down
7 changes: 7 additions & 0 deletions packages/@emulators/cloudflare/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
workosPlugin,
} from "@emulators/workos";
import { autumnPlugin, manifest as autumnManifest, seedFromConfig as autumnSeed } from "@emulators/autumn";
import { contextPlugin, manifest as contextManifest, seedFromConfig as contextSeed } from "@emulators/context";

// GitHub exposes three surfaces over ONE store: REST + GraphQL (githubPlugin) and
// an MCP server (mcpPlugin's transport + OAuth/DCR routes). They compose cleanly —
Expand Down Expand Up @@ -100,6 +101,12 @@ export interface ServiceEntry {
}

export const SERVICES: Record<string, ServiceEntry> = {
context: {
plugin: contextPlugin,
manifest: contextManifest,
seedFromConfig: contextSeed,
defaultFallback: () => ({ login: "context_test", id: 1, scopes: [] }),
},
github: {
plugin: githubWithMcpPlugin,
manifest: githubManifest,
Expand Down
17 changes: 17 additions & 0 deletions packages/@emulators/context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Context.dev

A curated emulator for `POST /v1/brand/retrieve` with `type: "by_email"`.
Seed explicit domain/title pairs through `/_emulate/seed`. Only seeded domains
match; unknown domains return 404. No Context.dev key or network call is needed.

```json
{"brands":[{"domain":"company.example","title":"Example Company"}]}
```

Create an instance at `https://context.emulators.dev/_emulate/instances`, then
create an `api-key` credential through its `/_emulate/credentials` endpoint.
Use the returned provider base URL and bearer token for requests. The regular
ledger and fault controls support observing calls and injecting failures.

This subset returns company names with empty logo/color arrays. Other Context
lookup modes, enrichment fields, OAuth, GraphQL and MCP are not implemented.
41 changes: 41 additions & 0 deletions packages/@emulators/context/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@emulators/context",
"version": "0.14.2",
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"homepage": "https://emulate.dev",
"repository": {
"type": "git",
"url": "https://github.com/UsefulSoftwareCo/emulate.git",
"directory": "packages/@emulators/context"
},
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/emulate/issues"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup --clean",
"dev": "tsup --watch",
"clean": "rm -rf dist .turbo",
"type-check": "tsc --noEmit",
"lint": "eslint src"
},
"dependencies": {
"@emulators/core": "workspace:*"
},
"devDependencies": {
"tsup": "^8",
"typescript": "^5.7"
}
}
94 changes: 94 additions & 0 deletions packages/@emulators/context/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { Entity, ServiceManifest, ServicePlugin, Store } from "@emulators/core";

interface Brand extends Entity {
domain: string;
title: string;
}
const brands = (store: Store) => store.collection<Brand>("context.brands", ["domain"]);
const record = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value);
const validDomain = (value: unknown): value is string =>
typeof value === "string" && /^[a-z0-9.-]+\.[a-z]{2,}$/.test(value);

/** Seed explicit company matches; absent domains deliberately return the provider's no-match response. */
export function seedFromConfig(store: Store, _baseUrl: string, config: unknown): void {
if (!record(config)) throw new Error("Expected a Context seed object");
if (config.brands === undefined) return;
if (!Array.isArray(config.brands)) throw new Error("Expected brands to be an array");
const parsed = config.brands.map((brand: unknown) => {
if (!record(brand) || !validDomain(brand.domain) || typeof brand.title !== "string" || !brand.title.trim())
throw new Error("Each brand requires a lowercase domain and a nonempty title");
return { domain: brand.domain, title: brand.title.trim() };
});
const collection = brands(store);
for (const brand of parsed) {
const existing = collection.findOneBy("domain", brand.domain);
if (existing) collection.update(existing.id, brand);
else collection.insert(brand);
}
}

/** Curated Context.dev by-email brand lookup over a real HTTP boundary. */
export const contextPlugin: ServicePlugin = {
name: "context",
register(app, store) {
app.post("/v1/brand/retrieve", async (c) => {
const input: unknown = await c.req.json().catch(() => null);
if (!record(input) || input.type !== "by_email" || typeof input.email !== "string")
return c.json({ error: "invalid_request", message: "Use type by_email with an email address." }, 422);
const parts = input.email.toLowerCase().split("@");
const domain = parts.length === 2 ? parts[1] : undefined;
if (!validDomain(domain)) return c.json({ error: "invalid_email" }, 422);
const brand = brands(store).findOneBy("domain", domain);
if (!brand) return c.json({ error: "brand_not_found" }, 404);
return c.json({
partial: false,
brand: {
domain: brand.domain,
title: brand.title,

logos: [],
colors: [],
},
});
});
},
};

/** Honest machine-readable coverage for the single supported Context endpoint. */
export const manifest: ServiceManifest = {
id: "context",
name: "Context.dev",
description: "Seeded company matching by email domain for onboarding tests.",
docsUrl: "https://docs.emulators.dev/context",
surfaces: [{ id: "rest", kind: "rest", title: "Brand retrieval by email", status: "partial", basePath: "/v1" }],
auth: [{ id: "api-key", title: "Bearer API key", type: "api-key", status: "supported" }],
specs: [
{
kind: "openapi",
title: "Curated brand lookup",
coverage: "hand-authored",
operations: [
{ operationId: "brand.retrieve", method: "POST", path: "/v1/brand/retrieve", status: "hand-authored" },
],
},
],
seedSchema: {
description: "Only seeded domains match. Unknown domains return 404; no external requests are made.",
fields: [
{ key: "brands", title: "Company brands", example: [{ domain: "company.example", title: "Example Company" }] },
],
example: { brands: [{ domain: "company.example", title: "Example Company" }] },
},
stateModel: { collections: [{ name: "context.brands", title: "Company matches" }] },
connections: [
{
id: "api",
title: "Brand lookup",
kind: "curl",
language: "shell",
template:
"curl -X POST '{{baseUrl}}/v1/brand/retrieve' -H 'Authorization: Bearer {{token}}' -H 'Content-Type: application/json' -d '{\"type\":\"by_email\",\"email\":\"person@company.example\"}'",
},
],
};
8 changes: 8 additions & 0 deletions packages/@emulators/context/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
19 changes: 19 additions & 0 deletions packages/@emulators/context/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from "tsup";
import { cpSync, mkdirSync } from "node:fs";
import { resolve } from "node:path";

const copyFonts = async () => {
const src = resolve(__dirname, "../core/src/fonts");
const dest = resolve(__dirname, "dist/fonts");
mkdirSync(dest, { recursive: true });
cpSync(src, dest, { recursive: true });
};

export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
noExternal: [/^@emulators\/core/],
onSuccess: copyFonts,
});
3 changes: 2 additions & 1 deletion packages/emulate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"tsup": "^8",
"typescript": "^5.7",
"@emulators/autumn": "workspace:*",
"@emulators/workos": "workspace:*"
"@emulators/workos": "workspace:*",
"@emulators/context": "workspace:*"
}
}
3 changes: 2 additions & 1 deletion packages/emulate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Global catalog:
Hosted services:
Available services include vercel, github, gitlab, google, slack, apple,
microsoft, okta, aws, resend, stripe, mongoatlas, clerk, spotify, x, workos,
autumn, posthog, and mcp.
autumn, posthog, mcp, and context.
Context supports seeded brand lookup by email; unmatched domains return 404.
MCP OAuth compliance scenarios are configured under mcp.oauth in seed data;
see the MCP manifest seed schema for issuer, resource, DCR, and token-auth knobs.
Microsoft Graph includes OneDrive file content upload/download routes under
Expand Down
11 changes: 11 additions & 0 deletions packages/emulate/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const SERVICE_NAME_LIST = [
// gitlab is appended last so adding it leaves every other service's default
// multi-service port (basePort + index) unchanged.
"gitlab",
"context",
] as const;
export type ServiceName = (typeof SERVICE_NAME_LIST)[number];
export const SERVICE_NAMES: readonly ServiceName[] = SERVICE_NAME_LIST;
Expand Down Expand Up @@ -273,6 +274,16 @@ function randomId(): string {
}

export const SERVICE_REGISTRY: Record<ServiceName, ServiceEntry> = {
context: {
label: "Context.dev company lookup emulator",
endpoints: "brand retrieval by email domain",
async load() {
const mod = await import("@emulators/context");
return { plugin: mod.contextPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
},
defaultFallback: () => ({ login: "context_test", id: 1, scopes: [] }),
initConfig: { context: { brands: [{ domain: "company.example", title: "Example Company" }] } },
},
vercel: {
label: "Vercel REST API emulator",
endpoints: "projects, deployments, domains, env vars, users, teams, file uploads, protection bypass",
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions skills/context/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: context
description: Use the Context.dev emulator to test company lookup by email domain through a running HTTP service.
---

# Context.dev company lookup

Create an instance with `POST https://context.emulators.dev/_emulate/instances`.
Save its returned provider base URL privately. Create an `api-key` credential
at `/_emulate/credentials` and seed `{"brands":[{"domain":"company.example","title":"Example Company"}]}`
at `/_emulate/seed`. Call `POST /v1/brand/retrieve` with bearer auth and
`{"type":"by_email","email":"person@company.example"}`.

Unknown domains return 404. Only company names are populated; logos/colors are
empty. Use the shared ledger and fault endpoints for inspection and failures.
For local use, run `npx emulate --service context`.
Loading