ccp-core 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +286 -2
  2. package/dist/adapters/express.d.ts +16 -0
  3. package/dist/adapters/express.js +49 -0
  4. package/dist/adapters/mcp.d.ts +57 -0
  5. package/dist/adapters/mcp.js +145 -0
  6. package/dist/adapters/next.d.ts +24 -0
  7. package/dist/adapters/next.js +65 -0
  8. package/dist/adapters/react.d.ts +25 -0
  9. package/dist/adapters/react.js +70 -0
  10. package/dist/adapters/status.d.ts +6 -0
  11. package/dist/adapters/status.js +18 -0
  12. package/dist/agent.d.ts +22 -0
  13. package/dist/agent.js +61 -0
  14. package/dist/consumer.d.ts +34 -0
  15. package/dist/consumer.js +78 -0
  16. package/dist/crypto.d.ts +10 -0
  17. package/dist/crypto.js +50 -0
  18. package/dist/decision.d.ts +17 -0
  19. package/dist/decision.js +67 -0
  20. package/dist/errors.d.ts +8 -0
  21. package/dist/errors.js +30 -0
  22. package/dist/grants.d.ts +51 -0
  23. package/dist/grants.js +233 -0
  24. package/dist/ids.d.ts +2 -0
  25. package/dist/ids.js +7 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.js +14 -0
  28. package/dist/json.d.ts +7 -0
  29. package/dist/json.js +96 -0
  30. package/dist/node/sqlite.d.ts +87 -0
  31. package/dist/node/sqlite.js +351 -0
  32. package/dist/parse.d.ts +8 -0
  33. package/dist/parse.js +194 -0
  34. package/dist/requester.d.ts +48 -0
  35. package/dist/requester.js +46 -0
  36. package/dist/runtime.d.ts +69 -0
  37. package/dist/runtime.js +309 -0
  38. package/dist/schema.d.ts +6 -0
  39. package/dist/schema.js +157 -0
  40. package/dist/signing.d.ts +7 -0
  41. package/dist/signing.js +21 -0
  42. package/dist/state-machine.d.ts +2 -0
  43. package/dist/state-machine.js +21 -0
  44. package/dist/stores.d.ts +153 -0
  45. package/dist/stores.js +135 -0
  46. package/dist/time.d.ts +2 -0
  47. package/dist/time.js +4 -0
  48. package/package.json +73 -4
package/README.md CHANGED
@@ -1,3 +1,287 @@
1
- # Temporary Holding Version
1
+ # ccp-core — CCP TypeScript SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript implementation of the Capability Contacts Protocol (draft
4
+ v0.4), shaped by role: **Requester**, **Consumer**, **Runtime**, **Agent**.
5
+ It reproduces all 51 frozen vectors in `../../test-vectors/`.
6
+
7
+ Runs in browsers and Node (no Node-only APIs in `src/`).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install ccp-core
13
+ ```
14
+
15
+ ## Requester — sign a manifest and a signal
16
+
17
+ ```ts
18
+ import { Requester, publicFromSeed } from "ccp-core";
19
+
20
+ const seed = crypto.getRandomValues(new Uint8Array(32));
21
+ const requester = new Requester(seed);
22
+
23
+ const manifest = requester.manifest({
24
+ provider: { id: requester.keyId, name: "Example Service" },
25
+ version: 1,
26
+ intents: {
27
+ "example.lookup": {
28
+ params_schema: "schema:example.lookup.params.v1",
29
+ result_schema: "schema:example.lookup.result.v1",
30
+ required_capabilities: ["store.read"],
31
+ placement: "local",
32
+ data_egress: { network: false, fields: [] },
33
+ confirmation: "grant-policy",
34
+ },
35
+ },
36
+ });
37
+
38
+ const signal = requester.signal({
39
+ requestId: "req-1",
40
+ grantDigest: "sha256:…", // the grant's manifest_digest
41
+ intent: "example.lookup",
42
+ params: { item_id: "item-123" },
43
+ issuedAt: "2026-10-22T12:00:00Z",
44
+ expiresAt: "2026-10-22T12:01:00Z",
45
+ });
46
+ ```
47
+
48
+ ## Consumer — verify and render a permission card
49
+
50
+ ```ts
51
+ import { verifyManifest, verifySignal, permissionCard } from "ccp-core";
52
+
53
+ const verified = verifyManifest(manifest); // throws CcpError on failure
54
+ const card = permissionCard(verified.manifest, "example.lookup", {
55
+ providerName: "Example Service",
56
+ });
57
+ // card.who / card.what / card.dataRead / card.where / card.egress /
58
+ // card.effects / card.recurrence / card.revoke
59
+
60
+ const inbox = verifySignal(signal, senderPublicKey);
61
+ ```
62
+
63
+ ## Runtime — enforce a grant
64
+
65
+ ```ts
66
+ import {
67
+ HostRegistry,
68
+ MemoryAuditLog,
69
+ MemoryGrantStore,
70
+ MemoryManifestStore,
71
+ Runtime,
72
+ SchemaRegistry,
73
+ createGrant,
74
+ verifyManifest,
75
+ } from "ccp-core";
76
+
77
+ const manifest = verifyManifest(manifestWire);
78
+
79
+ const grant = createGrant({
80
+ requesterKey: requester.keyId,
81
+ contactId: "c-1",
82
+ manifest,
83
+ intents: ["example.lookup"],
84
+ // Defaults to the manifest's suggested_limits. Values above the
85
+ // suggestion are rejected (§9.2).
86
+ limits: { calls_per_hour: 5 },
87
+ // Defaults to the manifest policy. Confirmation can be tightened but
88
+ // never relaxed to "none".
89
+ expiresAt: "2026-10-22T23:00:00Z",
90
+ });
91
+
92
+ const hosts = new HostRegistry();
93
+ hosts.registerHost("store.read", (id: string) => ({ item_id: id }));
94
+ hosts.registerIntent("example.lookup", (params, access) =>
95
+ access.call("store.read", params.item_id)
96
+ );
97
+
98
+ const runtime = new Runtime({
99
+ grants: new MemoryGrantStore([grant]),
100
+ manifests: new MemoryManifestStore([manifest]),
101
+ schemas: new SchemaRegistry({
102
+ "schema:example.lookup.params.v1": {
103
+ type: "object",
104
+ properties: { item_id: { type: "string" } },
105
+ required: ["item_id"],
106
+ additionalProperties: false,
107
+ },
108
+ }),
109
+ hosts,
110
+ audit: new MemoryAuditLog(),
111
+ });
112
+
113
+ const decision = await runtime.submit(signal, requesterPublicKey);
114
+ if (decision.outcome === "confirmation_required") {
115
+ await runtime.confirm(decision.pending_id!, true); // trusted UI decision
116
+ }
117
+ ```
118
+
119
+ `decision.outcome` is `authorized`, `confirmation_required`, or a §10.4
120
+ rejection code. `createGrant` only narrows what the manifest declares:
121
+ limits never exceed `suggested_limits`, confirmation is never relaxed,
122
+ capabilities come from the enabled intents, and placement/egress are
123
+ derived from them.
124
+
125
+ ## Durable stores
126
+
127
+ `Runtime` accepts pluggable stores. The interfaces are `GrantStore`,
128
+ `ManifestStore`, `ReplayStore`, `LimitStore`, `PendingStore`, and
129
+ `AuditStore`; the in-memory implementations are prefixed with `Memory`.
130
+ Authorization-state methods may be synchronous (the defaults, or a
131
+ synchronous SQLite driver) or asynchronous (Postgres, a remote store);
132
+ the runtime awaits them. `ManifestStore` stays synchronous because
133
+ verified manifests are immutable and can be loaded at startup.
134
+
135
+ Three methods carry atomicity requirements:
136
+
137
+ - `LimitStore.tryReserve` reserves one unit against every limit or none,
138
+ in one transaction.
139
+ - `ReplayStore.markIfNew` inserts atomically and returns `false` when the
140
+ request id already exists.
141
+ - `PendingStore.take` removes and returns a confirmation atomically, so it
142
+ can be resolved at most once across processes.
143
+
144
+ ```ts
145
+ const runtime = new Runtime({
146
+ grants: postgresGrants,
147
+ manifests: new MemoryManifestStore([manifest]), // immutable; load at startup
148
+ replay: postgresReplay,
149
+ limits: postgresLimits,
150
+ pending: postgresPending,
151
+ audit: postgresAudit,
152
+ schemas,
153
+ hosts,
154
+ });
155
+ ```
156
+
157
+ See `docs/guides/persistence.md` for the SQL shape and the full contract.
158
+
159
+ ### Node (SQLite)
160
+
161
+ The package ships a durable implementation on the built-in `node:sqlite`
162
+ module, with no extra dependency. It is Node-only and never imported by the
163
+ browser-safe core:
164
+
165
+ ```ts
166
+ import { Runtime } from "ccp-core";
167
+ import { SqliteStores } from "ccp-core/node";
168
+
169
+ const stores = new SqliteStores("ccp.db"); // or ":memory:"
170
+ const runtime = new Runtime({ ...stores, schemas, hosts });
171
+ // ...
172
+ stores.close();
173
+ ```
174
+
175
+ `node:sqlite` requires Node 22.5+ (available without a flag in Node 24, where
176
+ it is still marked experimental).
177
+
178
+ ## Agent — wrap a tool call
179
+
180
+ ```ts
181
+ import { Agent, attenuates } from "ccp-core";
182
+
183
+ const agent = new Agent(agentSeed);
184
+ const wire = agent.signalFor(
185
+ { intent: "example.lookup", params: { item_id: "item-123" } },
186
+ grant,
187
+ { requestId: "req-2", issuedAt: "…", expiresAt: "…" }
188
+ );
189
+
190
+ // Delegation must be no broader than the parent grant (§15.1).
191
+ attenuates(parentGrant, childGrant);
192
+ ```
193
+
194
+ ## Adapters — meet each framework in place
195
+
196
+ Adapters are subpath exports, so the core bundle stays free of Express and
197
+ React. They never decide authorization themselves; they parse the request,
198
+ resolve the sender key, and call the runtime.
199
+
200
+ ### Express provider
201
+
202
+ ```ts
203
+ import express from "express";
204
+ import { providerMiddleware } from "ccp-core/express";
205
+
206
+ const app = express();
207
+ app.use(express.json());
208
+ app.post(
209
+ "/ccp",
210
+ providerMiddleware({
211
+ runtime,
212
+ // Resolve from your authenticated channel, not from the signal.
213
+ keyResolver: (req) => publicKeys.get(req.header("x-contact")),
214
+ })
215
+ );
216
+ ```
217
+
218
+ The middleware reads `{ "signal": "<canonical json>" }` and forwards the
219
+ string untouched. It answers 200 for `authorized`/`confirmation_required`,
220
+ 400 for `invalid_request`, 401 when the sender is unknown, 403 for
221
+ `unauthenticated`/`no_matching_grant`, and 409 for other refusals.
222
+
223
+ ### Next.js / Web routes
224
+
225
+ ```ts
226
+ import { createManifestHandler, createSignalHandler } from "ccp-core/next";
227
+
228
+ export const GET = createManifestHandler({ manifest: manifestWire });
229
+ export const POST = createSignalHandler({ runtime, keyResolver });
230
+ ```
231
+
232
+ The signal handler takes the raw canonical JSON as the body and returns a
233
+ standard `Response`; it has no Next.js dependency and also works in any
234
+ `Request`/`Response` runtime. The manifest handler serves the signed wire
235
+ verbatim.
236
+
237
+ ### React permission card
238
+
239
+ ```tsx
240
+ import { usePermissionCard } from "ccp-core/react";
241
+
242
+ const { status, card, decision, approve, deny } = usePermissionCard({
243
+ runtime,
244
+ signal,
245
+ publicKey,
246
+ card: permissionCardModel,
247
+ });
248
+ ```
249
+
250
+ Headless: render `card` however you like. `status` is `loading`,
251
+ `authorized`, `confirmation_required`, `rejected`, or `cancelled`; calling
252
+ `approve()` / `deny()` resolves a pending confirmation. The hook submits once
253
+ per signal.
254
+
255
+ ### MCP agent bridge
256
+
257
+ ```ts
258
+ import { createMcpServer } from "ccp-core/mcp";
259
+
260
+ const bridge = createMcpServer({
261
+ runtime,
262
+ agent, // Agent key the grant is bound to
263
+ grant,
264
+ // Trusted host decision. Omit to surface confirmation_required instead.
265
+ confirm: async ({ params, card }) => askUser(card, params),
266
+ });
267
+
268
+ registerToolsOnYourMcpServer(bridge.tools);
269
+ const result = await bridge.callTool("example.lookup", {
270
+ item_id: "item-123",
271
+ });
272
+ ```
273
+
274
+ One bridge serves one grant; tool names are the granted intent names. The
275
+ adapter signs every call and lets the runtime decide. It never auto-approves:
276
+ without `confirm`, a `confirmation_required` outcome is returned with the
277
+ pending id and the §11.4 card. Tool results are data, not instructions.
278
+
279
+ Runnable examples: `examples/provider-node/` (HTTP provider) and
280
+ `examples/agent-mcp/` (MCP; `npm run mcp:demo`).
281
+
282
+ ## Tests
283
+
284
+ ```sh
285
+ npx vitest run # 51 frozen vectors + role unit tests
286
+ npx tsc --noEmit # typecheck
287
+ ```
@@ -0,0 +1,16 @@
1
+ import type { Request, RequestHandler } from "express";
2
+ import type { Runtime } from "../runtime.js";
3
+ /** Resolve the requester's Ed25519 public key, or `undefined` if unknown. */
4
+ export type ExpressKeyResolver = (request: Request) => Uint8Array | undefined | Promise<Uint8Array | undefined>;
5
+ export interface ProviderMiddlewareOptions {
6
+ /** Runtime that owns grants, replay protection, limits, and executors. */
7
+ runtime: Runtime;
8
+ /** Key for the sender of this request; `undefined` rejects with 401. */
9
+ keyResolver: ExpressKeyResolver;
10
+ }
11
+ /**
12
+ * Provider-side Express middleware. It reads `{ "signal": "<json>" }` (or a
13
+ * raw text body), resolves the sender key, and lets the runtime decide.
14
+ * Authorization never happens here.
15
+ */
16
+ export declare function providerMiddleware(options: ProviderMiddlewareOptions): RequestHandler;
@@ -0,0 +1,49 @@
1
+ import { httpStatusFor } from "./status.js";
2
+ /**
3
+ * Provider-side Express middleware. It reads `{ "signal": "<json>" }` (or a
4
+ * raw text body), resolves the sender key, and lets the runtime decide.
5
+ * Authorization never happens here.
6
+ */
7
+ export function providerMiddleware(options) {
8
+ const { runtime, keyResolver } = options;
9
+ return async (request, response) => {
10
+ const signal = extractSignal(request.body);
11
+ if (signal === undefined) {
12
+ response
13
+ .status(400)
14
+ .json({ outcome: "invalid_request", error: "invalid_request" });
15
+ return;
16
+ }
17
+ let publicKey;
18
+ try {
19
+ publicKey = await keyResolver(request);
20
+ }
21
+ catch {
22
+ publicKey = undefined;
23
+ }
24
+ if (publicKey === undefined) {
25
+ response
26
+ .status(401)
27
+ .json({ outcome: "unauthenticated", error: "unauthenticated" });
28
+ return;
29
+ }
30
+ try {
31
+ const decision = await runtime.submit(signal, publicKey);
32
+ response.status(httpStatusFor(decision)).json(decision);
33
+ }
34
+ catch {
35
+ response
36
+ .status(500)
37
+ .json({ outcome: "outcome_unknown", error: "outcome_unknown" });
38
+ }
39
+ };
40
+ }
41
+ function extractSignal(body) {
42
+ if (typeof body === "string")
43
+ return body;
44
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
45
+ return undefined;
46
+ }
47
+ const value = body.signal;
48
+ return typeof value === "string" ? value : undefined;
49
+ }
@@ -0,0 +1,57 @@
1
+ import type { Agent } from "../agent.js";
2
+ import type { PermissionCard, VerifiedManifest } from "../consumer.js";
3
+ import type { JsonValue } from "../json.js";
4
+ import type { Runtime } from "../runtime.js";
5
+ import type { Grant } from "../stores.js";
6
+ export interface McpTool {
7
+ /** Intent name, used verbatim as the MCP tool name. */
8
+ name: string;
9
+ description: string;
10
+ inputSchema: Record<string, unknown>;
11
+ outputSchema?: Record<string, unknown>;
12
+ }
13
+ export interface McpTextContent {
14
+ type: "text";
15
+ text: string;
16
+ }
17
+ export interface McpToolResult {
18
+ content: McpTextContent[];
19
+ structuredContent?: Record<string, unknown>;
20
+ isError?: boolean;
21
+ }
22
+ /** The exact action a trusted host is asked to approve (§11.4). */
23
+ export interface ConfirmationRequest {
24
+ pendingId: string;
25
+ requestId: string;
26
+ intent: string;
27
+ params: Record<string, JsonValue>;
28
+ card: PermissionCard;
29
+ }
30
+ export interface McpServerOptions {
31
+ /** Runtime that owns grants, limits, replay protection, and executors. */
32
+ runtime: Runtime;
33
+ /** Agent key that signs each tool call and that the grant is bound to. */
34
+ agent: Agent;
35
+ /** The agent's whole authority. Create one bridge per grant. */
36
+ grant: Grant;
37
+ /** Defaults to the runtime's verified manifest for this grant. */
38
+ manifest?: VerifiedManifest;
39
+ /**
40
+ * Trusted host decision. Omit to surface `confirmation_required` to the
41
+ * caller instead. The adapter never approves on its own.
42
+ */
43
+ confirm?: (request: ConfirmationRequest) => boolean | Promise<boolean>;
44
+ /** Lifetime of each signed signal, in seconds. Default 60. */
45
+ requestTtlSeconds?: number;
46
+ /** Defaults to `runtime.clock`. */
47
+ now?: () => string;
48
+ }
49
+ export interface McpServer {
50
+ readonly tools: readonly McpTool[];
51
+ callTool(name: string, args: unknown): Promise<McpToolResult>;
52
+ }
53
+ /**
54
+ * Transport-neutral MCP bridge. Expose `tools` through an MCP host and route
55
+ * `tools/call` to `callTool`; the runtime makes every authorization decision.
56
+ */
57
+ export declare function createMcpServer(options: McpServerOptions): McpServer;
@@ -0,0 +1,145 @@
1
+ import { permissionCard } from "../consumer.js";
2
+ import { confirmationPolicy } from "../decision.js";
3
+ import { AUTHORIZED, CONFIRMATION_REQUIRED, ERROR_CODES } from "../errors.js";
4
+ import { randomId } from "../ids.js";
5
+ /**
6
+ * Transport-neutral MCP bridge. Expose `tools` through an MCP host and route
7
+ * `tools/call` to `callTool`; the runtime makes every authorization decision.
8
+ */
9
+ export function createMcpServer(options) {
10
+ const { runtime, agent, grant } = options;
11
+ const manifest = options.manifest ?? runtime.manifests.get(grant.manifest_digest);
12
+ if (manifest === undefined) {
13
+ throw new Error(`manifest not found for grant ${grant.id}`);
14
+ }
15
+ const verified = manifest;
16
+ if (grant.requester_key !== agent.keyId) {
17
+ throw new Error(`grant ${grant.id} is not bound to this agent key`);
18
+ }
19
+ const now = options.now ?? runtime.clock;
20
+ const ttl = options.requestTtlSeconds ?? 60;
21
+ const tools = [];
22
+ for (const intent of grant.intents) {
23
+ const definition = verified.manifest.intents?.[intent];
24
+ if (definition === undefined) {
25
+ throw new Error(`intent ${intent} is not declared in the manifest`);
26
+ }
27
+ const inputSchema = runtime.schemas.get(definition.params_schema);
28
+ if (inputSchema === undefined) {
29
+ throw new Error(`params schema not registered: ${definition.params_schema}`);
30
+ }
31
+ const outputSchema = runtime.schemas.get(definition.result_schema);
32
+ tools.push({
33
+ name: intent,
34
+ description: describe(intent, definition, grant),
35
+ inputSchema,
36
+ ...(outputSchema === undefined ? {} : { outputSchema }),
37
+ });
38
+ }
39
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
40
+ async function settle(decision, tool, intent, params) {
41
+ if (decision.outcome === AUTHORIZED)
42
+ return success(tool, decision.result);
43
+ if (decision.outcome !== CONFIRMATION_REQUIRED) {
44
+ return failure(decision.outcome, {
45
+ request_id: decision.request_id,
46
+ detail: decision.detail,
47
+ });
48
+ }
49
+ const pendingId = decision.pending_id;
50
+ if (pendingId === undefined)
51
+ return failure("invalid_request");
52
+ if (options.confirm === undefined) {
53
+ return failure(CONFIRMATION_REQUIRED, {
54
+ request_id: decision.request_id,
55
+ pending_id: pendingId,
56
+ });
57
+ }
58
+ let approved = false;
59
+ try {
60
+ approved = await options.confirm({
61
+ pendingId,
62
+ requestId: decision.request_id ?? "",
63
+ intent,
64
+ params,
65
+ card: permissionCard(verified.manifest, intent),
66
+ });
67
+ }
68
+ catch {
69
+ approved = false;
70
+ }
71
+ const resolved = await runtime.confirm(pendingId, approved);
72
+ if (resolved.outcome === AUTHORIZED)
73
+ return success(tool, resolved.result);
74
+ if (resolved.outcome === "cancelled") {
75
+ return failure("cancelled", { request_id: resolved.request_id });
76
+ }
77
+ return failure(resolved.outcome, {
78
+ request_id: resolved.request_id,
79
+ detail: resolved.detail,
80
+ });
81
+ }
82
+ return {
83
+ tools,
84
+ async callTool(name, args) {
85
+ try {
86
+ const tool = byName.get(name);
87
+ if (tool === undefined)
88
+ return failure("intent_not_allowed");
89
+ if (args === null || typeof args !== "object" || Array.isArray(args)) {
90
+ return failure("invalid_request");
91
+ }
92
+ const params = args;
93
+ const issuedAt = now();
94
+ const expiresAt = shiftSeconds(issuedAt, ttl);
95
+ const signal = agent.signalFor({ intent: name, params }, grant, {
96
+ requestId: randomId(),
97
+ issuedAt,
98
+ expiresAt,
99
+ });
100
+ const decision = await runtime.submit(signal, agent.publicKey);
101
+ return await settle(decision, tool, name, params);
102
+ }
103
+ catch {
104
+ return failure("outcome_unknown");
105
+ }
106
+ },
107
+ };
108
+ }
109
+ function describe(intent, definition, grant) {
110
+ const providerText = typeof definition.description === "string" ? definition.description : "";
111
+ const policy = confirmationPolicy(grant, definition, intent);
112
+ return (`User-granted CCP capability "${intent}". ` +
113
+ `Provider description (untrusted data): ${JSON.stringify(providerText)}. ` +
114
+ (policy === "per_action"
115
+ ? "Each call may require explicit user confirmation. "
116
+ : "No per-call confirmation is required. ") +
117
+ "The user's runtime enforces this capability.");
118
+ }
119
+ function success(tool, result) {
120
+ const content = [
121
+ { type: "text", text: JSON.stringify(result) ?? "null" },
122
+ ];
123
+ const isObject = result !== null && typeof result === "object" && !Array.isArray(result);
124
+ return tool.outputSchema !== undefined && isObject
125
+ ? { content, structuredContent: result }
126
+ : { content };
127
+ }
128
+ function failure(outcome, extra = {}) {
129
+ const body = { outcome };
130
+ if (ERROR_CODES.has(outcome))
131
+ body.error = outcome;
132
+ for (const [key, value] of Object.entries(extra)) {
133
+ if (value !== undefined)
134
+ body[key] = value;
135
+ }
136
+ return {
137
+ content: [{ type: "text", text: JSON.stringify(body) }],
138
+ isError: true,
139
+ };
140
+ }
141
+ function shiftSeconds(timestamp, seconds) {
142
+ return new Date(Date.parse(timestamp) + seconds * 1000)
143
+ .toISOString()
144
+ .replace(/\.\d+Z$/, "Z");
145
+ }
@@ -0,0 +1,24 @@
1
+ import type { Runtime } from "../runtime.js";
2
+ /** Resolve the requester's Ed25519 public key, or `undefined` if unknown. */
3
+ export type WebKeyResolver = (request: Request) => Uint8Array | undefined | Promise<Uint8Array | undefined>;
4
+ export interface SignalHandlerOptions {
5
+ /** Runtime that owns grants, replay protection, limits, and executors. */
6
+ runtime: Runtime;
7
+ /** Key for the sender of this request; `undefined` rejects with 401. */
8
+ keyResolver: WebKeyResolver;
9
+ }
10
+ /**
11
+ * Framework-neutral Web handler: POST the canonical signal JSON as the body.
12
+ * Suitable for a Next.js route handler (`export const POST = ...`) or any
13
+ * runtime that speaks standard `Request`/`Response`.
14
+ */
15
+ export declare function createSignalHandler(options: SignalHandlerOptions): (request: Request) => Promise<Response>;
16
+ export interface ManifestHandlerOptions {
17
+ /** Signed manifest wire, exactly as produced by `signManifest`. */
18
+ manifest: string;
19
+ }
20
+ /**
21
+ * Serve the signed manifest verbatim. Consumers verify it themselves; the
22
+ * adapter does not re-serialize it.
23
+ */
24
+ export declare function createManifestHandler(options: ManifestHandlerOptions): (request: Request) => Promise<Response>;
@@ -0,0 +1,65 @@
1
+ import { httpStatusFor } from "./status.js";
2
+ /**
3
+ * Framework-neutral Web handler: POST the canonical signal JSON as the body.
4
+ * Suitable for a Next.js route handler (`export const POST = ...`) or any
5
+ * runtime that speaks standard `Request`/`Response`.
6
+ */
7
+ export function createSignalHandler(options) {
8
+ const { runtime, keyResolver } = options;
9
+ return async (request) => {
10
+ if (request.method !== "POST") {
11
+ return json(405, { outcome: "invalid_request", error: "invalid_request" });
12
+ }
13
+ let document;
14
+ try {
15
+ document = await request.text();
16
+ }
17
+ catch {
18
+ return json(400, {
19
+ outcome: "invalid_request",
20
+ error: "invalid_request",
21
+ });
22
+ }
23
+ if (document.trim() === "") {
24
+ return json(400, { outcome: "invalid_request", error: "invalid_request" });
25
+ }
26
+ let publicKey;
27
+ try {
28
+ publicKey = await keyResolver(request);
29
+ }
30
+ catch {
31
+ publicKey = undefined;
32
+ }
33
+ if (publicKey === undefined) {
34
+ return json(401, { outcome: "unauthenticated", error: "unauthenticated" });
35
+ }
36
+ try {
37
+ const decision = await runtime.submit(document, publicKey);
38
+ return json(httpStatusFor(decision), decision);
39
+ }
40
+ catch {
41
+ return json(500, { outcome: "outcome_unknown", error: "outcome_unknown" });
42
+ }
43
+ };
44
+ }
45
+ /**
46
+ * Serve the signed manifest verbatim. Consumers verify it themselves; the
47
+ * adapter does not re-serialize it.
48
+ */
49
+ export function createManifestHandler(options) {
50
+ return async (request) => {
51
+ if (request.method !== "GET" && request.method !== "HEAD") {
52
+ return json(405, { outcome: "invalid_request", error: "invalid_request" });
53
+ }
54
+ return new Response(request.method === "HEAD" ? null : options.manifest, {
55
+ status: 200,
56
+ headers: { "content-type": "application/json" },
57
+ });
58
+ };
59
+ }
60
+ function json(status, body) {
61
+ return new Response(JSON.stringify(body), {
62
+ status,
63
+ headers: { "content-type": "application/json" },
64
+ });
65
+ }
@@ -0,0 +1,25 @@
1
+ import type { Decision, Runtime } from "../runtime.js";
2
+ export type PermissionStatus = "loading" | "authorized" | "confirmation_required" | "rejected" | "cancelled";
3
+ export interface PermissionCardOptions<T = unknown> {
4
+ /** Runtime that owns grants, replay protection, limits, and executors. */
5
+ runtime: Runtime;
6
+ /** Canonical signed signal to submit. */
7
+ signal: string;
8
+ /** Requester public key that the runtime will verify the signal against. */
9
+ publicKey: Uint8Array;
10
+ /** Anything the caller wants to render; handed back untouched. */
11
+ card?: T;
12
+ }
13
+ export interface PermissionCardResult<T = unknown> {
14
+ status: PermissionStatus;
15
+ card: T | undefined;
16
+ decision: Decision | undefined;
17
+ approve: () => void;
18
+ deny: () => void;
19
+ }
20
+ /**
21
+ * Headless permission card for the consuming side: submit a signed signal,
22
+ * surface whether it needs confirmation, and let the user approve or deny.
23
+ * Presentation is entirely the caller's.
24
+ */
25
+ export declare function usePermissionCard<T = unknown>(options: PermissionCardOptions<T>): PermissionCardResult<T>;