@a2a-exposed/control-core 0.8.34 → 0.8.35
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
|
@@ -307,7 +307,33 @@ var MAX_WAKE_PLAIN = (() => {
|
|
|
307
307
|
}
|
|
308
308
|
return n;
|
|
309
309
|
})();
|
|
310
|
-
var MAX_WAKE_SEALED = 3 + 16 + 1 + Math.ceil((MAX_WAKE_PLAIN * 3 + 16) * 4 / 3);
|
|
310
|
+
var MAX_WAKE_SEALED = 3 + 64 + 1 + 16 + 1 + Math.ceil((MAX_WAKE_PLAIN * 3 + 16) * 4 / 3);
|
|
311
|
+
function tenantSecretsKeyring(input) {
|
|
312
|
+
if (!input) return null;
|
|
313
|
+
if (!input.trimStart().startsWith("{")) return { active: "", keys: { "": input }, legacy: true };
|
|
314
|
+
if (input.length > 65536) return null;
|
|
315
|
+
try {
|
|
316
|
+
const value = JSON.parse(input);
|
|
317
|
+
if (!value || value.v !== 2 || typeof value.active !== "string" || !value.keys || typeof value.keys !== "object" || Array.isArray(value.keys) || Object.keys(value).some((name) => !["v", "active", "keys"].includes(name))) return null;
|
|
318
|
+
const entries = Object.entries(value.keys);
|
|
319
|
+
if (entries.length < 1 || entries.length > 2 || !Object.hasOwn(value.keys, value.active)) return null;
|
|
320
|
+
for (const [kid, key] of entries) {
|
|
321
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(kid) || typeof key !== "string" || !key.trim() || key.length > 4096) return null;
|
|
322
|
+
}
|
|
323
|
+
return { active: value.active, keys: value.keys, legacy: false };
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function tenantSecretsMaterials(input) {
|
|
329
|
+
const ring = tenantSecretsKeyring(input);
|
|
330
|
+
if (!ring) return [];
|
|
331
|
+
return [ring.keys[ring.active], ...Object.entries(ring.keys).filter(([kid]) => kid !== ring.active).map(([, key]) => key)];
|
|
332
|
+
}
|
|
333
|
+
function unreadable() {
|
|
334
|
+
console.error(JSON.stringify({ event: "wake_secrets_unreadable", level: "error" }));
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
311
337
|
function b64(bytes2) {
|
|
312
338
|
let raw = "";
|
|
313
339
|
for (let i = 0; i < bytes2.length; i += 32768) raw += String.fromCharCode(...bytes2.subarray(i, i + 32768));
|
|
@@ -336,47 +362,68 @@ function payloadOf(secrets) {
|
|
|
336
362
|
return payload;
|
|
337
363
|
}
|
|
338
364
|
async function sealWakeSecrets(ikm, tenantId, secrets) {
|
|
339
|
-
const
|
|
365
|
+
const ring = tenantSecretsKeyring(ikm);
|
|
366
|
+
const key = ring ? await derive(ring.keys[ring.active], tenantId) : null;
|
|
340
367
|
const payload = payloadOf(secrets);
|
|
341
368
|
if (!key || !payload) return null;
|
|
342
369
|
const json = JSON.stringify(payload);
|
|
343
370
|
const encoded = new TextEncoder().encode(json);
|
|
344
371
|
if (json.length > MAX_WAKE_PLAIN || encoded.length > MAX_WAKE_PLAIN * 3) return null;
|
|
372
|
+
const prefix = ring.legacy ? "v1" : `v2.${ring.active}`;
|
|
373
|
+
const aad = ring.legacy ? tenantId : `${prefix}\0${tenantId}`;
|
|
345
374
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
346
375
|
const ct = new Uint8Array(await crypto.subtle.encrypt(
|
|
347
|
-
{ name: "AES-GCM", iv, additionalData: new TextEncoder().encode(
|
|
376
|
+
{ name: "AES-GCM", iv, additionalData: new TextEncoder().encode(aad) },
|
|
348
377
|
key,
|
|
349
378
|
encoded
|
|
350
379
|
));
|
|
351
|
-
const sealed =
|
|
380
|
+
const sealed = `${prefix}.${b64(iv)}.${b64(ct)}`;
|
|
352
381
|
return sealed.length <= MAX_WAKE_SEALED ? sealed : null;
|
|
353
382
|
}
|
|
354
383
|
async function openWakeSecrets(ikm, tenantId, sealed) {
|
|
355
384
|
if (sealed == null || sealed === "") return {};
|
|
356
|
-
if (sealed.length > MAX_WAKE_SEALED) return
|
|
357
|
-
const
|
|
358
|
-
if (!
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
|
|
385
|
+
if (typeof sealed !== "string" || sealed.length > MAX_WAKE_SEALED) return unreadable();
|
|
386
|
+
const ring = tenantSecretsKeyring(ikm);
|
|
387
|
+
if (!ring || !tenantId) return unreadable();
|
|
388
|
+
const parts = sealed.split(".");
|
|
389
|
+
const version = parts[0];
|
|
390
|
+
const kid = version === "v2" ? parts[1] : "";
|
|
391
|
+
if (version !== "v1" && version !== "v2") return unreadable();
|
|
392
|
+
if (parts.length !== (version === "v2" ? 4 : 3)) return unreadable();
|
|
393
|
+
const [iv, ct] = parts.slice(version === "v2" ? 2 : 1);
|
|
394
|
+
if (!iv || !ct || version === "v2" && (ring.legacy || !Object.hasOwn(ring.keys, kid))) return unreadable();
|
|
395
|
+
const materials = version === "v2" ? [ring.keys[kid]] : tenantSecretsMaterials(ikm);
|
|
396
|
+
const aad = version === "v2" ? `v2.${kid}\0${tenantId}` : tenantId;
|
|
397
|
+
for (const material of materials) {
|
|
398
|
+
try {
|
|
399
|
+
const key = await derive(material, tenantId);
|
|
400
|
+
if (!key) continue;
|
|
401
|
+
const ivBytes = unb64(iv);
|
|
402
|
+
if (ivBytes.length !== 12) return unreadable();
|
|
403
|
+
const plain = new TextDecoder().decode(await crypto.subtle.decrypt(
|
|
404
|
+
{ name: "AES-GCM", iv: ivBytes, additionalData: new TextEncoder().encode(aad) },
|
|
405
|
+
key,
|
|
406
|
+
unb64(ct)
|
|
407
|
+
));
|
|
408
|
+
if (plain.length > MAX_WAKE_PLAIN) return unreadable();
|
|
409
|
+
const parsed = JSON.parse(plain);
|
|
410
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return unreadable();
|
|
411
|
+
const out = {};
|
|
412
|
+
for (const name of WAKE_SECRET_KEYS) {
|
|
413
|
+
const value = parsed[name];
|
|
414
|
+
if (typeof value !== "string" || !value || value.length > MAX_WAKE_VALUE) continue;
|
|
415
|
+
out[name] = value;
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
} catch {
|
|
375
419
|
}
|
|
376
|
-
return out;
|
|
377
|
-
} catch {
|
|
378
|
-
return null;
|
|
379
420
|
}
|
|
421
|
+
return unreadable();
|
|
422
|
+
}
|
|
423
|
+
async function rewrapWakeSecrets(ikm, tenantId, sealed) {
|
|
424
|
+
if (!sealed) return null;
|
|
425
|
+
const secrets = await openWakeSecrets(ikm, tenantId, sealed);
|
|
426
|
+
return secrets ? sealWakeSecrets(ikm, tenantId, secrets) : null;
|
|
380
427
|
}
|
|
381
428
|
|
|
382
429
|
// src/tenants/create.ts
|
|
@@ -3672,6 +3719,7 @@ export {
|
|
|
3672
3719
|
resultList,
|
|
3673
3720
|
resultObject,
|
|
3674
3721
|
resumeFields,
|
|
3722
|
+
rewrapWakeSecrets,
|
|
3675
3723
|
rotateOwnerToken,
|
|
3676
3724
|
routes,
|
|
3677
3725
|
runInboxStep,
|
|
@@ -6,7 +6,7 @@ export { rotateOwnerToken } from "../tenants/rotate.js";
|
|
|
6
6
|
export type { RotateResult } from "../tenants/rotate.js";
|
|
7
7
|
export { WAKE_PRESETS, configFields, saveWakeSettings, tenantForAccount, wakeHeaderProblem, wakeSettingsOf, wakeUrlProblem, } from "../tenants/wake.js";
|
|
8
8
|
export type { WakeSave, WakeSaveInput, WakeSettings } from "../tenants/wake.js";
|
|
9
|
-
export { MAX_WAKE_PLAIN, MAX_WAKE_SEALED, MAX_WAKE_VALUE, WAKE_SECRET_KEYS, openWakeSecrets, sealWakeSecrets, } from "../tenants/wake-secrets.js";
|
|
9
|
+
export { MAX_WAKE_PLAIN, MAX_WAKE_SEALED, MAX_WAKE_VALUE, WAKE_SECRET_KEYS, openWakeSecrets, rewrapWakeSecrets, sealWakeSecrets, } from "../tenants/wake-secrets.js";
|
|
10
10
|
export type { WakeSecretKey, WakeSecrets } from "../tenants/wake-secrets.js";
|
|
11
11
|
export { RESERVED_NAMES, TENANT_NAME, tenantNameProblem } from "../tenants/reserved.js";
|
|
12
12
|
export { httpsOrigin, newApprovalClient } from "../tenants/approval.js";
|
|
@@ -32,7 +32,7 @@ export interface ControlEnv extends PlaneEnv {
|
|
|
32
32
|
SIGNUP_ALLOWLIST?: string;
|
|
33
33
|
/** Default agents per account. Unset or empty: no cap, unless that account sets agent_limit. An explicit non-negative integer sets the cap. */
|
|
34
34
|
FREE_AGENT_LIMIT?: string;
|
|
35
|
-
/** Same
|
|
35
|
+
/** Same raw key or JSON rotation keyring as the data-plane Worker. Seals tenant secrets. Unset: a new key cannot be saved. */
|
|
36
36
|
TENANT_SECRETS_KEY?: string;
|
|
37
37
|
/**
|
|
38
38
|
* Fine-grained token with Issues write on FEEDBACK_REPO. A secret.
|
|
@@ -9,17 +9,29 @@ export declare const MAX_WAKE_VALUE = 4096;
|
|
|
9
9
|
* and every known field may be present. `openWakeSecrets` accepts this and nothing larger.
|
|
10
10
|
*/
|
|
11
11
|
export declare const MAX_WAKE_PLAIN: number;
|
|
12
|
-
/**
|
|
12
|
+
/** Longest envelope: `v2.<64-char kid>.<iv>.<ciphertext>`. */
|
|
13
13
|
export declare const MAX_WAKE_SEALED: number;
|
|
14
|
+
/** A bounded keyring lives in the existing secret binding, never in public config. */
|
|
15
|
+
export interface TenantSecretsKeyring {
|
|
16
|
+
active: string;
|
|
17
|
+
keys: Record<string, string>;
|
|
18
|
+
legacy: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Raw keys preserve v1 writes for a reader-first rollout. JSON keyrings enable v2 writes. */
|
|
21
|
+
export declare function tenantSecretsKeyring(input: string): TenantSecretsKeyring | null;
|
|
22
|
+
/** Current material first, then the single read-only rotation key. No caller may log these values. */
|
|
23
|
+
export declare function tenantSecretsMaterials(input: string): string[];
|
|
14
24
|
/**
|
|
15
25
|
* Config written next to a sealed blob. The sealed fields stay in `secrets_enc`.
|
|
16
26
|
* With no ciphertext, the config is returned unchanged.
|
|
17
27
|
*/
|
|
18
28
|
export declare function omitSealedSecrets<T extends Record<string, unknown>>(config: T, secretsEnc: string | null | undefined): T;
|
|
19
|
-
/** `
|
|
29
|
+
/** `v2.<kid>.<iv>.<ciphertext>` for keyrings; raw keys keep v1 writes until rollout completes. */
|
|
20
30
|
export declare function sealWakeSecrets(ikm: string, tenantId: string, secrets: WakeSecrets): Promise<string | null>;
|
|
21
31
|
/**
|
|
22
32
|
* The sealed fields, `{}` when there is nothing to open, or null when the blob cannot be read.
|
|
23
33
|
* A null result must not be treated as "no secrets": the caller keeps the previous ciphertext.
|
|
24
34
|
*/
|
|
25
35
|
export declare function openWakeSecrets(ikm: string, tenantId: string, sealed: string | null | undefined): Promise<WakeSecrets | null>;
|
|
36
|
+
/** Pure maintenance helper. The caller must CAS the original blob and queue the new tenant config. */
|
|
37
|
+
export declare function rewrapWakeSecrets(ikm: string, tenantId: string, sealed: string): Promise<string | null>;
|