@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 +1 -1
- package/dist/index.js +220 -96
- package/dist/types/control/src/core/index.d.ts +3 -2
- package/dist/types/control/src/provision/inbox.d.ts +39 -0
- package/dist/types/control/src/tenants/create.d.ts +7 -0
- package/dist/types/control/src/tenants/events.d.ts +19 -0
- package/dist/types/control/src/tenants/wake-secrets.d.ts +7 -2
- package/package.json +1 -1
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
|
|
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', ?, ?, '{}', ?, ?,
|
|
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:
|
|
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
|
|
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
|
-
/**
|
|
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
|
/**
|