@a2a-exposed/control-core 0.8.7 → 0.8.9

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.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Requires Node 22.18+. The skills are installed into the project the agent works
33
33
  npx -y a2a-exposed@latest pair set-password --web
34
34
  ```
35
35
 
36
- `status` is read-only and prints the next step. Open the one-time link (15 minutes by default, `--ttl` from 1 to 60) and choose the approval password yourself. Another agent then runs `npx -y a2a-exposed@latest connect https://agent.example.com` (the code lasts 10 minutes). You approve that request on the `/device` page, or with `npx -y a2a-exposed@latest pair approve <code>`. Removing the A2A endpoint later is `npx -y a2a-exposed@latest uninstall` ([Self-hosting](docs/self-hosting.md#remove-an-inbox)).
36
+ `status` is read-only and prints the next step. Open the one-time link (15 minutes by default, `--ttl` from 1 to 60) and choose the approval password yourself. Another agent then runs `npx -y a2a-exposed@latest connect https://agent.example.com` (the code lasts 60 minutes). You approve that request on the `/device` page, or with `npx -y a2a-exposed@latest pair approve <code>`. Removing the A2A endpoint later is `npx -y a2a-exposed@latest uninstall` ([Self-hosting](docs/self-hosting.md#remove-an-inbox)).
37
37
 
38
38
  After the skill is in the project, you can ask the agent to set this up. It follows the setup skill. Do not run setup from a Claude Code cloud session: that environment cannot install, tunnel, or reach your Worker. Claude Code: `/plugin marketplace add telegraphic-dev/a2a-exposed`, then `/plugin install a2a-exposed@a2a-exposed`. [Getting started](docs/getting-started.md) has Node, the client table, and what to do if setup stops halfway.
39
39
 
package/dist/index.js CHANGED
@@ -59,6 +59,65 @@ async function newApprovalClient(input) {
59
59
  return { clientId, clientSecret, secretHash: await sha256(clientSecret), redirectUris, approval };
60
60
  }
61
61
 
62
+ // src/tenants/events.ts
63
+ var EVENT_REPLAY_WINDOW_S = 300;
64
+ var URL_MAX = 2e3;
65
+ var SECRET_MAX = 512;
66
+ function httpsEventUrl(value) {
67
+ const raw = (value || "").trim();
68
+ if (!raw || raw.length > URL_MAX) return "";
69
+ try {
70
+ const url = new URL(raw);
71
+ if (url.protocol !== "https:" || url.username || url.password) return "";
72
+ return raw;
73
+ } catch {
74
+ return "";
75
+ }
76
+ }
77
+ function eventSecretText(value) {
78
+ const text = (value || "").trim();
79
+ if (!text || text.length > SECRET_MAX || /[\u0000-\u001f]/.test(text)) return "";
80
+ return text;
81
+ }
82
+ function pairingEventConfig(eventUrl, eventSecret) {
83
+ const url = httpsEventUrl(eventUrl);
84
+ const secret = eventSecretText(eventSecret);
85
+ if (!url || !secret) return null;
86
+ return { EVENT_URL: url, EVENT_SECRET: secret };
87
+ }
88
+ function headerValue(headers, name) {
89
+ if (typeof headers.get === "function") return headers.get(name) || "";
90
+ const record = headers;
91
+ const direct = record[name];
92
+ if (typeof direct === "string") return direct;
93
+ const found = Object.keys(record).find((key) => key.toLowerCase() === name.toLowerCase());
94
+ const value = found ? record[found] : "";
95
+ return typeof value === "string" ? value : "";
96
+ }
97
+ async function hmacHex(secret, message) {
98
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
99
+ const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
100
+ return [...new Uint8Array(mac)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
101
+ }
102
+ function timingSafeEqual(a, b) {
103
+ const left = new TextEncoder().encode(a);
104
+ const right = new TextEncoder().encode(b);
105
+ if (left.length !== right.length) return false;
106
+ let diff = 0;
107
+ for (let i = 0; i < left.length; i++) diff |= left[i] ^ right[i];
108
+ return diff === 0;
109
+ }
110
+ async function verifyEventSignature(body, headers, secret, now = Date.now()) {
111
+ const key = eventSecretText(secret);
112
+ const timestamp = headerValue(headers, "x-a2a-timestamp").trim();
113
+ const presented = headerValue(headers, "x-a2a-signature").trim();
114
+ if (!key || !/^\d{1,12}$/.test(timestamp) || !presented.startsWith("sha256=")) return false;
115
+ const skew = Math.abs(Math.floor(now / 1e3) - Number(timestamp));
116
+ if (skew > EVENT_REPLAY_WINDOW_S) return false;
117
+ const expected = `sha256=${await hmacHex(key, `${timestamp}.${body}`)}`;
118
+ return timingSafeEqual(expected, presented);
119
+ }
120
+
62
121
  // src/tenants/push.ts
63
122
  function namespaceFor(ns, region) {
64
123
  if ((region === "eu" || region === "fedramp") && typeof ns.jurisdiction === "function") return ns.jurisdiction(region);
@@ -226,6 +285,97 @@ function tenantNameProblem(name) {
226
285
  return "";
227
286
  }
228
287
 
288
+ // src/tenants/wake-secrets.ts
289
+ var INFO = "tenant-wake-secrets v1";
290
+ var SALT = "a2a-exposed";
291
+ var WAKE_SECRET_KEYS = [
292
+ "WAKE_WEBHOOK_KEY",
293
+ "WAKE_HMAC_SECRET",
294
+ "WAKE_ACCESS_CLIENT_ID",
295
+ "WAKE_ACCESS_CLIENT_SECRET",
296
+ "EVENT_SECRET"
297
+ ];
298
+ var MAX_WAKE_VALUE = 4096;
299
+ var MAX_WAKE_PLAIN = (() => {
300
+ let n = 2;
301
+ for (let i = 0; i < WAKE_SECRET_KEYS.length; i++) {
302
+ if (i) n += 1;
303
+ n += WAKE_SECRET_KEYS[i].length + 5 + MAX_WAKE_VALUE * 6;
304
+ }
305
+ return n;
306
+ })();
307
+ var MAX_WAKE_SEALED = 3 + 16 + 1 + Math.ceil((MAX_WAKE_PLAIN * 3 + 16) * 4 / 3);
308
+ function b64(bytes2) {
309
+ let raw = "";
310
+ for (let i = 0; i < bytes2.length; i += 32768) raw += String.fromCharCode(...bytes2.subarray(i, i + 32768));
311
+ return btoa(raw);
312
+ }
313
+ var unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
314
+ async function derive(ikm, tenantId) {
315
+ if (!ikm || !tenantId) return null;
316
+ const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(ikm), "HKDF", false, ["deriveKey"]);
317
+ return crypto.subtle.deriveKey(
318
+ { name: "HKDF", hash: "SHA-256", salt: new TextEncoder().encode(SALT), info: new TextEncoder().encode(`${INFO}\0${tenantId}`) },
319
+ base,
320
+ { name: "AES-GCM", length: 256 },
321
+ false,
322
+ ["encrypt", "decrypt"]
323
+ );
324
+ }
325
+ function payloadOf(secrets) {
326
+ const payload = {};
327
+ for (const key of WAKE_SECRET_KEYS) {
328
+ const value = secrets[key];
329
+ if (value == null || value === "") continue;
330
+ if (typeof value !== "string" || value.length > MAX_WAKE_VALUE) return null;
331
+ payload[key] = value;
332
+ }
333
+ return payload;
334
+ }
335
+ async function sealWakeSecrets(ikm, tenantId, secrets) {
336
+ const key = await derive(ikm, tenantId);
337
+ const payload = payloadOf(secrets);
338
+ if (!key || !payload) return null;
339
+ const json = JSON.stringify(payload);
340
+ const encoded = new TextEncoder().encode(json);
341
+ if (json.length > MAX_WAKE_PLAIN || encoded.length > MAX_WAKE_PLAIN * 3) return null;
342
+ const iv = crypto.getRandomValues(new Uint8Array(12));
343
+ const ct = new Uint8Array(await crypto.subtle.encrypt(
344
+ { name: "AES-GCM", iv, additionalData: new TextEncoder().encode(tenantId) },
345
+ key,
346
+ encoded
347
+ ));
348
+ const sealed = `v1.${b64(iv)}.${b64(ct)}`;
349
+ return sealed.length <= MAX_WAKE_SEALED ? sealed : null;
350
+ }
351
+ async function openWakeSecrets(ikm, tenantId, sealed) {
352
+ if (sealed == null || sealed === "") return {};
353
+ if (sealed.length > MAX_WAKE_SEALED) return null;
354
+ const key = await derive(ikm, tenantId);
355
+ if (!key) return null;
356
+ const [version, iv, ct, extra] = sealed.split(".");
357
+ if (version !== "v1" || !iv || !ct || extra !== void 0) return null;
358
+ try {
359
+ const plain = new TextDecoder().decode(await crypto.subtle.decrypt(
360
+ { name: "AES-GCM", iv: unb64(iv), additionalData: new TextEncoder().encode(tenantId) },
361
+ key,
362
+ unb64(ct)
363
+ ));
364
+ if (plain.length > MAX_WAKE_PLAIN) return null;
365
+ const parsed = JSON.parse(plain);
366
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
367
+ const out = {};
368
+ for (const name of WAKE_SECRET_KEYS) {
369
+ const value = parsed[name];
370
+ if (typeof value !== "string" || !value || value.length > MAX_WAKE_VALUE) continue;
371
+ out[name] = value;
372
+ }
373
+ return out;
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+
229
379
  // src/tenants/create.ts
230
380
  function configuredFreeAgentLimit(value) {
231
381
  const trimmed = (value ?? "").trim();
@@ -265,7 +415,11 @@ async function createTenant(db, plane, input) {
265
415
  const hash = await sha2562(token);
266
416
  const now = (/* @__PURE__ */ new Date()).toISOString();
267
417
  const region = regionOf(input.dataRegion);
418
+ const events = input.eventUrl && input.eventSecret ? pairingEventConfig(input.eventUrl, input.eventSecret) : null;
419
+ const sealedEvents = events ? await sealWakeSecrets((input.secretsKey || "").trim(), id, { EVENT_SECRET: events.EVENT_SECRET }) : null;
268
420
  const config = { AGENT_NAME: name };
421
+ if (events && sealedEvents) config.EVENT_URL = events.EVENT_URL;
422
+ const secretsEnc = sealedEvents;
269
423
  const version = 1;
270
424
  const domain = (input.domain || "").trim().toLowerCase().replace(/^\.+|\.+$/g, "");
271
425
  const publicUrl = domain ? `https://${name}.${domain}` : null;
@@ -281,8 +435,8 @@ async function createTenant(db, plane, input) {
281
435
  `INSERT INTO tenants (
282
436
  id, name, owner_account_id, status, region, config_json, limits_json, approval_json,
283
437
  owner_token_hash, secrets_enc, config_version, created_at, updated_at
284
- ) VALUES (?, ?, ?, 'active', ?, ?, '{}', ?, ?, NULL, ?, ?, ?)`
285
- ).bind(id, name, input.accountId, region, JSON.stringify(config), JSON.stringify(approval), hash, version, now, now),
438
+ ) VALUES (?, ?, ?, 'active', ?, ?, '{}', ?, ?, ?, ?, ?, ?)`
439
+ ).bind(id, name, input.accountId, region, JSON.stringify(config), JSON.stringify(approval), hash, secretsEnc, version, now, now),
286
440
  ...client ? [db.prepare(
287
441
  "INSERT INTO oauth_client (id, secret_hash, owner_account_id, redirect_uris, created_at) VALUES (?, ?, ?, ?, ?)"
288
442
  ).bind(client.clientId, client.secretHash, input.accountId, JSON.stringify(client.redirectUris), now)] : [],
@@ -303,7 +457,7 @@ async function createTenant(db, plane, input) {
303
457
  limits_json: "{}",
304
458
  approval_json: JSON.stringify(approval),
305
459
  owner_token_hash: hash,
306
- secrets_enc: null,
460
+ secrets_enc: secretsEnc,
307
461
  version,
308
462
  created_at: now
309
463
  };
@@ -343,96 +497,6 @@ async function listTenants(db, accountId, domain) {
343
497
  }));
344
498
  }
345
499
 
346
- // src/tenants/wake-secrets.ts
347
- var INFO = "tenant-wake-secrets v1";
348
- var SALT = "a2a-exposed";
349
- var WAKE_SECRET_KEYS = [
350
- "WAKE_WEBHOOK_KEY",
351
- "WAKE_HMAC_SECRET",
352
- "WAKE_ACCESS_CLIENT_ID",
353
- "WAKE_ACCESS_CLIENT_SECRET"
354
- ];
355
- var MAX_WAKE_VALUE = 4096;
356
- var MAX_WAKE_PLAIN = (() => {
357
- let n = 2;
358
- for (let i = 0; i < WAKE_SECRET_KEYS.length; i++) {
359
- if (i) n += 1;
360
- n += WAKE_SECRET_KEYS[i].length + 5 + MAX_WAKE_VALUE * 6;
361
- }
362
- return n;
363
- })();
364
- var MAX_WAKE_SEALED = 3 + 16 + 1 + Math.ceil((MAX_WAKE_PLAIN * 3 + 16) * 4 / 3);
365
- function b64(bytes2) {
366
- let raw = "";
367
- for (let i = 0; i < bytes2.length; i += 32768) raw += String.fromCharCode(...bytes2.subarray(i, i + 32768));
368
- return btoa(raw);
369
- }
370
- var unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
371
- async function derive(ikm, tenantId) {
372
- if (!ikm || !tenantId) return null;
373
- const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(ikm), "HKDF", false, ["deriveKey"]);
374
- return crypto.subtle.deriveKey(
375
- { name: "HKDF", hash: "SHA-256", salt: new TextEncoder().encode(SALT), info: new TextEncoder().encode(`${INFO}\0${tenantId}`) },
376
- base,
377
- { name: "AES-GCM", length: 256 },
378
- false,
379
- ["encrypt", "decrypt"]
380
- );
381
- }
382
- function payloadOf(secrets) {
383
- const payload = {};
384
- for (const key of WAKE_SECRET_KEYS) {
385
- const value = secrets[key];
386
- if (value == null || value === "") continue;
387
- if (typeof value !== "string" || value.length > MAX_WAKE_VALUE) return null;
388
- payload[key] = value;
389
- }
390
- return payload;
391
- }
392
- async function sealWakeSecrets(ikm, tenantId, secrets) {
393
- const key = await derive(ikm, tenantId);
394
- const payload = payloadOf(secrets);
395
- if (!key || !payload) return null;
396
- const json = JSON.stringify(payload);
397
- const encoded = new TextEncoder().encode(json);
398
- if (json.length > MAX_WAKE_PLAIN || encoded.length > MAX_WAKE_PLAIN * 3) return null;
399
- const iv = crypto.getRandomValues(new Uint8Array(12));
400
- const ct = new Uint8Array(await crypto.subtle.encrypt(
401
- { name: "AES-GCM", iv, additionalData: new TextEncoder().encode(tenantId) },
402
- key,
403
- encoded
404
- ));
405
- const sealed = `v1.${b64(iv)}.${b64(ct)}`;
406
- return sealed.length <= MAX_WAKE_SEALED ? sealed : null;
407
- }
408
- async function openWakeSecrets(ikm, tenantId, sealed) {
409
- if (sealed == null || sealed === "") return {};
410
- if (sealed.length > MAX_WAKE_SEALED) return null;
411
- const key = await derive(ikm, tenantId);
412
- if (!key) return null;
413
- const [version, iv, ct, extra] = sealed.split(".");
414
- if (version !== "v1" || !iv || !ct || extra !== void 0) return null;
415
- try {
416
- const plain = new TextDecoder().decode(await crypto.subtle.decrypt(
417
- { name: "AES-GCM", iv: unb64(iv), additionalData: new TextEncoder().encode(tenantId) },
418
- key,
419
- unb64(ct)
420
- ));
421
- if (plain.length > MAX_WAKE_PLAIN) return null;
422
- const parsed = JSON.parse(plain);
423
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
424
- const out = {};
425
- for (const name of WAKE_SECRET_KEYS) {
426
- const value = parsed[name];
427
- if (typeof value !== "string" || !value || value.length > MAX_WAKE_VALUE) continue;
428
- out[name] = value;
429
- }
430
- return out;
431
- } catch {
432
- return null;
433
- }
434
- }
435
-
436
500
  // src/tenants/wake.ts
437
501
  var WAKE_PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "generic"];
438
502
  var PLAIN_HEADERS = /* @__PURE__ */ new Set([
@@ -2097,7 +2161,55 @@ async function releaseWorking(db, inboxId, now) {
2097
2161
  const row = await readInboxById(db, inboxId);
2098
2162
  if (row?.status === "working") await patchInbox(db, inboxId, now, { status: "pending" });
2099
2163
  }
2164
+ function httpsIssuer(value) {
2165
+ const raw = value.trim();
2166
+ if (!raw || raw.length > 300) return "";
2167
+ try {
2168
+ const url = new URL(raw);
2169
+ if (url.protocol !== "https:" || url.username || url.password) return "";
2170
+ return raw;
2171
+ } catch {
2172
+ return "";
2173
+ }
2174
+ }
2175
+ function plainCredential(value, max) {
2176
+ const text = value.trim();
2177
+ if (!text || text.length > max || /[\u0000-\u001f]/.test(text)) return "";
2178
+ return text;
2179
+ }
2180
+ function subjectsText(value) {
2181
+ const list = typeof value === "string" ? value.split(",") : value;
2182
+ const subjects = list.map((item) => item.trim()).filter((item) => item && !/[\u0000-\u001f]/.test(item));
2183
+ const text = subjects.join(",");
2184
+ return text && text.length <= 2e3 ? text : "";
2185
+ }
2186
+ function inboxApprovalBindings(input) {
2187
+ if (!input) return null;
2188
+ const issuer = httpsIssuer(input.issuer);
2189
+ const clientId = plainCredential(input.clientId, 200);
2190
+ const clientSecret = plainCredential(input.clientSecret, 512);
2191
+ const allowed = subjectsText(input.allowedSubjects);
2192
+ if (!issuer || !clientId || !clientSecret || !allowed) return null;
2193
+ return [
2194
+ { type: "plain_text", name: "APPROVAL_OIDC_ISSUER", text: issuer },
2195
+ { type: "plain_text", name: "APPROVAL_OIDC_CLIENT_ID", text: clientId },
2196
+ { type: "secret_text", name: "APPROVAL_OIDC_CLIENT_SECRET", text: clientSecret },
2197
+ { type: "plain_text", name: "APPROVAL_OIDC_ALLOWED_SUBJECTS", text: allowed },
2198
+ { type: "plain_text", name: "APPROVAL_METHODS", text: "oidc" }
2199
+ ];
2200
+ }
2201
+ function inboxEventBindings(eventUrl, eventSecret) {
2202
+ const url = httpsEventUrl(eventUrl);
2203
+ const secret = eventSecretText(eventSecret);
2204
+ if (!url || !secret) return null;
2205
+ return [
2206
+ { type: "plain_text", name: "EVENT_URL", text: url },
2207
+ { type: "secret_text", name: "EVENT_SECRET", text: secret }
2208
+ ];
2209
+ }
2100
2210
  function scriptMetadata(input) {
2211
+ const approval = inboxApprovalBindings(input.approvalOidc);
2212
+ const events = inboxEventBindings(input.eventUrl, input.eventSecret);
2101
2213
  return {
2102
2214
  main_module: "index.js",
2103
2215
  compatibility_date: INBOX_COMPATIBILITY_DATE,
@@ -2108,7 +2220,9 @@ function scriptMetadata(input) {
2108
2220
  { type: "plain_text", name: "PAIRING_APPROVAL", text: "human" },
2109
2221
  { type: "plain_text", name: "WAKE_PRESET", text: "generic" },
2110
2222
  { type: "plain_text", name: PROVISION_TAG_NAME, text: input.scriptTag },
2111
- { type: "secret_text", name: "OWNER_TOKEN", text: input.ownerToken }
2223
+ { type: "secret_text", name: "OWNER_TOKEN", text: input.ownerToken },
2224
+ ...approval ?? [],
2225
+ ...events ?? []
2112
2226
  ]
2113
2227
  };
2114
2228
  }
@@ -2527,7 +2641,10 @@ async function runInboxStep(input) {
2527
2641
  script: input.script,
2528
2642
  migrations: input.migrations,
2529
2643
  grant,
2530
- permissionDelaysMs: input.permissionDelaysMs ?? PERMISSION_DELAYS_MS
2644
+ permissionDelaysMs: input.permissionDelaysMs ?? PERMISSION_DELAYS_MS,
2645
+ approvalOidc: input.approvalOidc,
2646
+ eventUrl: input.eventUrl,
2647
+ eventSecret: input.eventSecret
2531
2648
  };
2532
2649
  try {
2533
2650
  if (next !== "verify") {
@@ -2813,7 +2930,7 @@ async function runUpload(ctx) {
2813
2930
  accountId,
2814
2931
  name: workerName,
2815
2932
  script: ctx.script,
2816
- metadata: scriptMetadata({ d1Id: databaseId, publicUrl, workerName, ownerToken, scriptTag })
2933
+ metadata: scriptMetadata({ d1Id: databaseId, publicUrl, workerName, ownerToken, scriptTag, approvalOidc: ctx.approvalOidc, eventUrl: ctx.eventUrl, eventSecret: ctx.eventSecret })
2817
2934
  });
2818
2935
  if (uploaded2 !== true) {
2819
2936
  return { wizard: await failOrWait(ctx, "upload", uploaded2, "Cloudflare stops while uploading the inbox.", "upload") };
@@ -3016,6 +3133,7 @@ export {
3016
3133
  CONTROL_MIGRATIONS_DIR,
3017
3134
  D1_WRITE,
3018
3135
  DNS_WRITE,
3136
+ EVENT_REPLAY_WINDOW_S,
3019
3137
  EVERY_PLAN,
3020
3138
  HOST_RE,
3021
3139
  INBOX_COMPATIBILITY_DATE,
@@ -3070,6 +3188,7 @@ export {
3070
3188
  deliverOutbox,
3071
3189
  describeCfFailure,
3072
3190
  describeSetupGrant,
3191
+ eventSecretText,
3073
3192
  fieldsFromForm,
3074
3193
  findD1,
3075
3194
  flushOutbox,
@@ -3078,8 +3197,11 @@ export {
3078
3197
  grantedScopeSet,
3079
3198
  hashOwnerToken,
3080
3199
  headerSafeToken,
3200
+ httpsEventUrl,
3081
3201
  httpsOrigin,
3202
+ inboxApprovalBindings,
3082
3203
  inboxCanProvision,
3204
+ inboxEventBindings,
3083
3205
  listAccounts,
3084
3206
  listCloudflareInboxes,
3085
3207
  listTenants,
@@ -3091,6 +3213,7 @@ export {
3091
3213
  normalizeHostname,
3092
3214
  openCloudflareRefresh,
3093
3215
  openWakeSecrets,
3216
+ pairingEventConfig,
3094
3217
  parseSetupChoice,
3095
3218
  parseSetupHost,
3096
3219
  permissionNotReady,
@@ -3126,6 +3249,7 @@ export {
3126
3249
  tenantForAccount,
3127
3250
  tenantNameProblem,
3128
3251
  uploadWorker,
3252
+ verifyEventSignature,
3129
3253
  viewInboxWizard,
3130
3254
  wakeHeaderProblem,
3131
3255
  wakeSettingsOf,
@@ -26,8 +26,9 @@ export * from "../provision/statements.js";
26
26
  export * from "../provision/permission-wait.js";
27
27
  export * from "../provision/inbox-row.js";
28
28
  export * from "../provision/remove.js";
29
- export { INBOX_COMPATIBILITY_DATE, PERMISSION_DELAYS_MS, PERMISSION_TRIES, WIZARD_STEPS, createCloudflareAgent, dashboardLinks, fieldsFromForm, permissionRetryDue, resumeFields, runInboxStep, viewInboxWizard, } from "../provision/inbox.js";
30
- export type { InboxMigration, InboxWizard, ProvisionFields, WizardControl, WizardState, WizardStepId, WizardStepView, } from "../provision/inbox.js";
29
+ export { INBOX_COMPATIBILITY_DATE, PERMISSION_DELAYS_MS, PERMISSION_TRIES, WIZARD_STEPS, createCloudflareAgent, dashboardLinks, fieldsFromForm, permissionRetryDue, inboxApprovalBindings, inboxEventBindings, resumeFields, runInboxStep, viewInboxWizard, } from "../provision/inbox.js";
30
+ export { EVENT_REPLAY_WINDOW_S, eventSecretText, httpsEventUrl, pairingEventConfig, verifyEventSignature, } from "../tenants/events.js";
31
+ export type { ApprovalOidcInput, InboxMigration, InboxWizard, ProvisionFields, WizardControl, WizardState, WizardStepId, WizardStepView, } from "../provision/inbox.js";
31
32
  export { INBOX_MIGRATIONS } from "../provision/inbox-migrations.js";
32
33
  export { CONTROL_MIGRATIONS, CONTROL_MIGRATIONS_DIR } from "./migrations.js";
33
34
  export type { ControlMigration } from "./migrations.js";
@@ -89,6 +89,36 @@ interface StoredStepNote {
89
89
  }
90
90
  /** A new Cloudflare agent. The finished ones stay. */
91
91
  export declare function createCloudflareAgent(db: Sql, userId: string, host: SetupHost, now?: number): Promise<string>;
92
+ /** Account sign-in for a Cloudflare agent. The dashboard creates the client and passes these four fields. */
93
+ export type ApprovalOidcInput = {
94
+ issuer: string;
95
+ clientId: string;
96
+ clientSecret: string;
97
+ allowedSubjects: string | readonly string[];
98
+ };
99
+ type ScriptBinding = {
100
+ type: "d1";
101
+ name: string;
102
+ id: string;
103
+ } | {
104
+ type: "plain_text";
105
+ name: string;
106
+ text: string;
107
+ } | {
108
+ type: "secret_text";
109
+ name: string;
110
+ text: string;
111
+ };
112
+ /**
113
+ * Worker bindings for account sign-in. Returns the list when the issuer is https and every field
114
+ * has a value. Otherwise returns null and the upload keeps the approval password.
115
+ */
116
+ export declare function inboxApprovalBindings(input: ApprovalOidcInput | undefined): ScriptBinding[] | null;
117
+ /**
118
+ * Bindings for a signed pairing event. Null unless the URL is https and the secret has a value,
119
+ * so a partial pair leaves the upload unchanged.
120
+ */
121
+ export declare function inboxEventBindings(eventUrl?: string, eventSecret?: string): ScriptBinding[] | null;
92
122
  export declare function fieldsFromForm(form: FormData): ProvisionFields;
93
123
  export declare function resumeFields(value: {
94
124
  name?: unknown;
@@ -136,6 +166,15 @@ export declare function runInboxStep(input: {
136
166
  fetchImpl?: typeof fetch;
137
167
  /** Pauses after a permission error. Tests pass zeros. Production waits about 15 seconds across 4 tries. */
138
168
  permissionDelaysMs?: number[];
169
+ /**
170
+ * Account sign-in for this Cloudflare agent. When issuer, client id, client secret, and allowed
171
+ * subjects are all set, the upload writes the APPROVAL_OIDC_* bindings and APPROVAL_METHODS=oidc.
172
+ * Omit it and the agent stays on the approval password.
173
+ */
174
+ approvalOidc?: ApprovalOidcInput;
175
+ /** https URL and secret for pairing events. Both are required; a partial pair writes no EVENT_* binding. */
176
+ eventUrl?: string;
177
+ eventSecret?: string;
139
178
  }): Promise<{
140
179
  wizard: InboxWizard;
141
180
  ownerToken?: string;
@@ -31,6 +31,13 @@ export declare function createTenant(db: Sql, plane: DataPlane, input: {
31
31
  issuer?: string;
32
32
  label?: string;
33
33
  freeAgentLimit?: number | null;
34
+ /**
35
+ * https URL and secret for pairing events, plus the key that seals the secret.
36
+ * A partial pair, or a secret that cannot be sealed, is left out of the tenant.
37
+ */
38
+ eventUrl?: string;
39
+ eventSecret?: string;
40
+ secretsKey?: string;
34
41
  }): Promise<CreateResult>;
35
42
  export declare function listTenants(db: Sql, accountId: string, domain?: string): Promise<{
36
43
  id: string;
@@ -0,0 +1,19 @@
1
+ /** How far `x-a2a-timestamp` may sit from the check, in seconds. */
2
+ export declare const EVENT_REPLAY_WINDOW_S = 300;
3
+ /** https URL with no userinfo, at most 2000 characters. Any other value returns "". */
4
+ export declare function httpsEventUrl(value: string | undefined): string;
5
+ /** Shared secret for the HMAC. Empty when missing, too long, or it contains a control character. */
6
+ export declare function eventSecretText(value: string | undefined): string;
7
+ /**
8
+ * Tenant config fields for pairing events. Null unless the URL is https and the secret has a value,
9
+ * so a partial pair is left out of the push.
10
+ */
11
+ export declare function pairingEventConfig(eventUrl: string, eventSecret: string): {
12
+ EVENT_URL: string;
13
+ EVENT_SECRET: string;
14
+ } | null;
15
+ /**
16
+ * True when `x-a2a-signature` is the HMAC-SHA256 of the timestamp, a period, and the raw body,
17
+ * and `x-a2a-timestamp` is within 5 minutes of `now`. The header value is `sha256=<hex>`.
18
+ */
19
+ export declare function verifyEventSignature(body: string, headers: Headers | Record<string, string | null | undefined>, secret: string, now?: number): Promise<boolean>;
@@ -1,5 +1,5 @@
1
- /** Secret wake fields. Anything else in the blob is ignored. */
2
- export declare const WAKE_SECRET_KEYS: readonly ["WAKE_WEBHOOK_KEY", "WAKE_HMAC_SECRET", "WAKE_ACCESS_CLIENT_ID", "WAKE_ACCESS_CLIENT_SECRET"];
1
+ /** Sealed secret fields. Anything else in the blob is ignored. */
2
+ export declare const WAKE_SECRET_KEYS: readonly ["WAKE_WEBHOOK_KEY", "WAKE_HMAC_SECRET", "WAKE_ACCESS_CLIENT_ID", "WAKE_ACCESS_CLIENT_SECRET", "EVENT_SECRET"];
3
3
  export type WakeSecretKey = (typeof WAKE_SECRET_KEYS)[number];
4
4
  export type WakeSecrets = Partial<Record<WakeSecretKey, string>>;
5
5
  /** One secret. The form uses the same cap. */
@@ -11,6 +11,11 @@ export declare const MAX_WAKE_VALUE = 4096;
11
11
  export declare const MAX_WAKE_PLAIN: number;
12
12
  /** `v1.` + base64(12-byte iv) + `.` + base64(utf-8 plaintext + 16-byte tag). */
13
13
  export declare const MAX_WAKE_SEALED: number;
14
+ /**
15
+ * Config written next to a sealed blob. The sealed fields stay in `secrets_enc`.
16
+ * With no ciphertext, the config is returned unchanged.
17
+ */
18
+ export declare function omitSealedSecrets<T extends Record<string, unknown>>(config: T, secretsEnc: string | null | undefined): T;
14
19
  /** `v1.<iv>.<ciphertext>`, or null when the key or a value is unusable. */
15
20
  export declare function sealWakeSecrets(ikm: string, tenantId: string, secrets: WakeSecrets): Promise<string | null>;
16
21
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2a-exposed/control-core",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "A2A endpoint provisioning, routing, and D1 migrations for a2a-exposed.",
5
5
  "type": "module",
6
6
  "scripts": {