@a2a-exposed/control-core 0.8.8 → 0.8.10

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/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([
@@ -2134,8 +2198,18 @@ function inboxApprovalBindings(input) {
2134
2198
  { type: "plain_text", name: "APPROVAL_METHODS", text: "oidc" }
2135
2199
  ];
2136
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
+ }
2137
2210
  function scriptMetadata(input) {
2138
2211
  const approval = inboxApprovalBindings(input.approvalOidc);
2212
+ const events = inboxEventBindings(input.eventUrl, input.eventSecret);
2139
2213
  return {
2140
2214
  main_module: "index.js",
2141
2215
  compatibility_date: INBOX_COMPATIBILITY_DATE,
@@ -2147,7 +2221,8 @@ function scriptMetadata(input) {
2147
2221
  { type: "plain_text", name: "WAKE_PRESET", text: "generic" },
2148
2222
  { type: "plain_text", name: PROVISION_TAG_NAME, text: input.scriptTag },
2149
2223
  { type: "secret_text", name: "OWNER_TOKEN", text: input.ownerToken },
2150
- ...approval ?? []
2224
+ ...approval ?? [],
2225
+ ...events ?? []
2151
2226
  ]
2152
2227
  };
2153
2228
  }
@@ -2567,7 +2642,9 @@ async function runInboxStep(input) {
2567
2642
  migrations: input.migrations,
2568
2643
  grant,
2569
2644
  permissionDelaysMs: input.permissionDelaysMs ?? PERMISSION_DELAYS_MS,
2570
- approvalOidc: input.approvalOidc
2645
+ approvalOidc: input.approvalOidc,
2646
+ eventUrl: input.eventUrl,
2647
+ eventSecret: input.eventSecret
2571
2648
  };
2572
2649
  try {
2573
2650
  if (next !== "verify") {
@@ -2853,7 +2930,7 @@ async function runUpload(ctx) {
2853
2930
  accountId,
2854
2931
  name: workerName,
2855
2932
  script: ctx.script,
2856
- metadata: scriptMetadata({ d1Id: databaseId, publicUrl, workerName, ownerToken, scriptTag, approvalOidc: ctx.approvalOidc })
2933
+ metadata: scriptMetadata({ d1Id: databaseId, publicUrl, workerName, ownerToken, scriptTag, approvalOidc: ctx.approvalOidc, eventUrl: ctx.eventUrl, eventSecret: ctx.eventSecret })
2857
2934
  });
2858
2935
  if (uploaded2 !== true) {
2859
2936
  return { wizard: await failOrWait(ctx, "upload", uploaded2, "Cloudflare stops while uploading the inbox.", "upload") };
@@ -3056,6 +3133,7 @@ export {
3056
3133
  CONTROL_MIGRATIONS_DIR,
3057
3134
  D1_WRITE,
3058
3135
  DNS_WRITE,
3136
+ EVENT_REPLAY_WINDOW_S,
3059
3137
  EVERY_PLAN,
3060
3138
  HOST_RE,
3061
3139
  INBOX_COMPATIBILITY_DATE,
@@ -3110,6 +3188,7 @@ export {
3110
3188
  deliverOutbox,
3111
3189
  describeCfFailure,
3112
3190
  describeSetupGrant,
3191
+ eventSecretText,
3113
3192
  fieldsFromForm,
3114
3193
  findD1,
3115
3194
  flushOutbox,
@@ -3118,9 +3197,11 @@ export {
3118
3197
  grantedScopeSet,
3119
3198
  hashOwnerToken,
3120
3199
  headerSafeToken,
3200
+ httpsEventUrl,
3121
3201
  httpsOrigin,
3122
3202
  inboxApprovalBindings,
3123
3203
  inboxCanProvision,
3204
+ inboxEventBindings,
3124
3205
  listAccounts,
3125
3206
  listCloudflareInboxes,
3126
3207
  listTenants,
@@ -3132,6 +3213,7 @@ export {
3132
3213
  normalizeHostname,
3133
3214
  openCloudflareRefresh,
3134
3215
  openWakeSecrets,
3216
+ pairingEventConfig,
3135
3217
  parseSetupChoice,
3136
3218
  parseSetupHost,
3137
3219
  permissionNotReady,
@@ -3167,6 +3249,7 @@ export {
3167
3249
  tenantForAccount,
3168
3250
  tenantNameProblem,
3169
3251
  uploadWorker,
3252
+ verifyEventSignature,
3170
3253
  viewInboxWizard,
3171
3254
  wakeHeaderProblem,
3172
3255
  wakeSettingsOf,
@@ -26,7 +26,8 @@ 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, inboxApprovalBindings, resumeFields, runInboxStep, viewInboxWizard, } 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";
30
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";
@@ -114,6 +114,11 @@ type ScriptBinding = {
114
114
  * has a value. Otherwise returns null and the upload keeps the approval password.
115
115
  */
116
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;
117
122
  export declare function fieldsFromForm(form: FormData): ProvisionFields;
118
123
  export declare function resumeFields(value: {
119
124
  name?: unknown;
@@ -167,6 +172,9 @@ export declare function runInboxStep(input: {
167
172
  * Omit it and the agent stays on the approval password.
168
173
  */
169
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;
170
178
  }): Promise<{
171
179
  wizard: InboxWizard;
172
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.8",
3
+ "version": "0.8.10",
4
4
  "description": "A2A endpoint provisioning, routing, and D1 migrations for a2a-exposed.",
5
5
  "type": "module",
6
6
  "scripts": {