@tangle-network/agent-app 0.47.6 → 0.47.7

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.
@@ -29,6 +29,8 @@ export interface DurableWorkspaceKeyRecord {
29
29
  sourceKeyFingerprint: string;
30
30
  /** The persisted name used to recover a remote create after a crash. Null only for pre-name rows. */
31
31
  name: string | null;
32
+ /** The persisted retry identity for the remote create. Absent on legacy rows without a persisted create identity. */
33
+ idempotencyKey?: string | null;
32
34
  keyId: string;
33
35
  keyEncrypted: string;
34
36
  budgetUsd: number;
@@ -48,9 +50,21 @@ type DurableWorkspaceKeyCreateResult = Awaited<ReturnType<KeyProvisioner['create
48
50
  budgetRemaining?: number;
49
51
  expiresAt?: string | null;
50
52
  };
53
+ /** The exact request identity that must be reused when recovering a create. */
54
+ export interface DurableWorkspaceKeyCreateInput {
55
+ name: string;
56
+ product: string;
57
+ budgetUsd: number;
58
+ expiresAt: string;
59
+ /** Stable across process restarts for one persisted provisioning row. */
60
+ idempotencyKey: string;
61
+ }
51
62
  /** The remote operations required by the durable manager. */
52
63
  export interface DurableWorkspaceKeyProvisioner {
53
- createKey(input: Parameters<KeyProvisioner['createKey']>[0]): Promise<DurableWorkspaceKeyCreateResult>;
64
+ /** True only when replay returns the original child for the persisted request identity. */
65
+ supportsIdempotentCreate?: true;
66
+ /** Create one child key. Non-idempotent providers recover through stable-name discovery only. */
67
+ createKey(input: DurableWorkspaceKeyCreateInput): Promise<DurableWorkspaceKeyCreateResult>;
54
68
  getKey(id: string): Promise<{
55
69
  budgetUsd?: number | null;
56
70
  budgetSpent?: number;
@@ -66,6 +80,22 @@ export interface DurableWorkspaceKeyProvisioner {
66
80
  id: string;
67
81
  }>>;
68
82
  }
83
+ /** Optional compare-and-set lifecycle writes for stores that support fencing. */
84
+ export interface DurableWorkspaceKeyConditionalWrites {
85
+ /** Return false when the row was no longer provisioning. */
86
+ markProvisioningRemote(input: {
87
+ id: string;
88
+ keyId: string;
89
+ }): Promise<boolean>;
90
+ /** Return false when the row was no longer provisioning. */
91
+ markActive(input: {
92
+ id: string;
93
+ keyId: string;
94
+ keyEncrypted: string;
95
+ expiresAt: Date;
96
+ budgetUsd: number;
97
+ }): Promise<boolean>;
98
+ }
69
99
  /** Persistence operations for identity-bound key lifecycle state. */
70
100
  export interface DurableWorkspaceKeyStore {
71
101
  /** Return the active row for this exact product scope. */
@@ -87,7 +117,12 @@ export interface DurableWorkspaceKeyStore {
87
117
  expiresAt: Date;
88
118
  budgetUsd: number;
89
119
  }): Promise<void>;
90
- /** Keep a failed cleanup visible and schedule a later retry. */
120
+ /**
121
+ * Optional fenced writes. The manager uses false to compensate a stale
122
+ * creator. Stores without this field retain the pre-fence behavior.
123
+ */
124
+ conditionalWrites?: DurableWorkspaceKeyConditionalWrites;
125
+ /** Keep a failed cleanup visible and schedule a later retry. Terminal rows are immutable. */
91
126
  markRevocationPending(input: {
92
127
  id: string;
93
128
  error?: string | null;
@@ -139,13 +174,22 @@ export interface DurableWorkspaceKeyManager {
139
174
  }): Promise<WorkspaceRuntimeKey>;
140
175
  /** Read usage without returning the child secret. */
141
176
  getUsage(identity: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null>;
142
- /** Retry due cleanup rows. A supplied owner/workspace limits the retry to its scope. */
143
- retryPendingRevocations(scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>): Promise<number>;
177
+ /**
178
+ * Retry due cleanup rows. A supplied owner/workspace limits the retry to its
179
+ * scope. Empty provisioning rows also require the full identity so a retry
180
+ * can use the original source binding safely.
181
+ */
182
+ retryPendingRevocations(scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>, identity?: WorkspaceKeyIdentity): Promise<number>;
144
183
  }
145
184
  /** Configuration for {@link createIdentityBoundWorkspaceKeyManager}. */
146
185
  export interface DurableWorkspaceKeyManagerOptions {
147
186
  store: DurableWorkspaceKeyStore;
148
187
  provisioner: DurableWorkspaceKeyProvisioner;
188
+ /**
189
+ * Control-plane client for historical get/revoke/list operations. Use this
190
+ * when the source credential can rotate or disappear. It never mints keys.
191
+ */
192
+ recoveryProvisioner?: Pick<DurableWorkspaceKeyProvisioner, 'getKey' | 'revokeKey' | 'findCreatedKeys'>;
149
193
  crypto: KeyCrypto;
150
194
  /** Product partition. Products must use separate values. */
151
195
  product: WorkspaceKeyProduct;
@@ -239,4 +239,4 @@ export interface PlatformBalanceManager<Plan extends string> {
239
239
  export declare function createPlatformBalanceManager<Plan extends string>(opts: PlatformBalanceManagerOptions<Plan>): PlatformBalanceManager<Plan>;
240
240
  /** Create a workspace key manager that handles key provisioning and budget tracking */
241
241
  export declare function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager;
242
- export { createIdentityBoundWorkspaceKeyManager, type DurableWorkspaceKeyManager, type DurableWorkspaceKeyManagerOptions, type DurableWorkspaceKeyProvisioner, type DurableWorkspaceKeyProvisioningRecord, type DurableWorkspaceKeyRecord, type DurableWorkspaceKeyScope, type DurableWorkspaceKeyStore, type WorkspaceKeyIdentity, type WorkspaceKeyProduct, type WorkspaceKeyStatus, type WorkspaceKeyUsage, type WorkspaceRuntimeKey, } from './identity-bound';
242
+ export { createIdentityBoundWorkspaceKeyManager, type DurableWorkspaceKeyManager, type DurableWorkspaceKeyManagerOptions, type DurableWorkspaceKeyCreateInput, type DurableWorkspaceKeyConditionalWrites, type DurableWorkspaceKeyProvisioner, type DurableWorkspaceKeyProvisioningRecord, type DurableWorkspaceKeyRecord, type DurableWorkspaceKeyScope, type DurableWorkspaceKeyStore, type WorkspaceKeyIdentity, type WorkspaceKeyProduct, type WorkspaceKeyStatus, type WorkspaceKeyUsage, type WorkspaceRuntimeKey, } from './identity-bound';
@@ -3,7 +3,7 @@ import {
3
3
  createPlatformBalanceManager,
4
4
  createTcloudKeyProvisioner,
5
5
  createWorkspaceKeyManager
6
- } from "../chunk-TS2ZIB53.js";
6
+ } from "../chunk-4ZJYNI53.js";
7
7
  export {
8
8
  createIdentityBoundWorkspaceKeyManager,
9
9
  createPlatformBalanceManager,
@@ -79,6 +79,16 @@ function errorMessage(error, fallback) {
79
79
  function isProvisioningId(value) {
80
80
  return value.startsWith("provisioning:");
81
81
  }
82
+ async function markProvisioningRemote(store, input) {
83
+ if (store.conditionalWrites) return store.conditionalWrites.markProvisioningRemote(input);
84
+ await store.markProvisioningRemote(input);
85
+ return true;
86
+ }
87
+ async function markActive(store, input) {
88
+ if (store.conditionalWrites) return store.conditionalWrites.markActive(input);
89
+ await store.markActive(input);
90
+ return true;
91
+ }
82
92
  function usageFromRemote(row, remote, now) {
83
93
  const budgetUsd = remote.budgetUsd ?? row.budgetUsd;
84
94
  const budgetSpent = remote.budgetSpent ?? 0;
@@ -125,6 +135,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
125
135
  if (revocationRetryMaxMs < revocationRetryBaseMs) throw new Error("revocationRetryMaxMs must cover the base delay");
126
136
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
127
137
  const isRemoteMissing = options.isRemoteMissing ?? defaultRemoteMissing;
138
+ const recoveryProvisioner = options.recoveryProvisioner ?? options.provisioner;
128
139
  const localLocks = /* @__PURE__ */ new Map();
129
140
  async function withLocalLock(scope, work) {
130
141
  const previous = localLocks.get(scope) ?? Promise.resolve();
@@ -231,7 +242,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
231
242
  return true;
232
243
  }
233
244
  try {
234
- await options.provisioner.revokeKey(row.keyId);
245
+ await recoveryProvisioner.revokeKey(row.keyId);
235
246
  await options.store.markRevoked(row.id, now());
236
247
  return true;
237
248
  } catch (error) {
@@ -248,26 +259,61 @@ function createIdentityBoundWorkspaceKeyManager(options) {
248
259
  return false;
249
260
  }
250
261
  }
251
- async function cleanupProvisioning(row, identity) {
252
- let candidates;
262
+ async function provisioningCandidates(row) {
253
263
  if (row.keyId && !isProvisioningId(row.keyId)) {
254
- candidates = [{ id: row.keyId }];
255
- } else {
256
- const name = row.name?.trim() || options.legacyNameForRecord?.(row)?.trim();
257
- if (!name) {
258
- await options.store.markRevocationPending({
259
- id: row.id,
260
- error: "provisioning row has no recoverable remote name; migrate the row or configure legacyNameForRecord",
261
- nextAttemptAt: now(),
262
- incrementAttempts: false
263
- });
264
- return false;
265
- }
266
- candidates = await options.provisioner.findCreatedKeys({
264
+ return { name: row.name?.trim() ?? "", candidates: [{ id: row.keyId }] };
265
+ }
266
+ const name = row.name?.trim() || options.legacyNameForRecord?.(row)?.trim();
267
+ if (!name) return null;
268
+ return {
269
+ name,
270
+ candidates: await recoveryProvisioner.findCreatedKeys({
267
271
  name,
268
272
  product: row.product,
269
273
  sourceKeyId: row.sourceKeyId
274
+ })
275
+ };
276
+ }
277
+ async function probeProvisioning(row, identity, name) {
278
+ const idempotencyKey = row.idempotencyKey?.trim();
279
+ if (!identity || !sameIdentity(row, identity, product) || !options.provisioner.supportsIdempotentCreate || !idempotencyKey) return [];
280
+ try {
281
+ const created = await options.provisioner.createKey({
282
+ name,
283
+ product: row.product,
284
+ budgetUsd: row.budgetUsd,
285
+ expiresAt: row.expiresAt.toISOString(),
286
+ idempotencyKey
270
287
  });
288
+ const remoteId = created.id?.trim();
289
+ if (remoteId) return [{ id: remoteId }];
290
+ } catch (error) {
291
+ await options.store.markRevocationPending({
292
+ id: row.id,
293
+ error: errorMessage(error, "remote provisioning recovery failed"),
294
+ nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),
295
+ incrementAttempts: true
296
+ });
297
+ return null;
298
+ }
299
+ return (await provisioningCandidates({ ...row, name, keyId: `provisioning:${idempotencyKey}` }))?.candidates ?? [];
300
+ }
301
+ async function cleanupProvisioning(row, identity) {
302
+ const resolved = await provisioningCandidates(row);
303
+ if (!resolved) {
304
+ await options.store.markRevocationPending({
305
+ id: row.id,
306
+ error: "provisioning row has no recoverable remote name; migrate the row or configure legacyNameForRecord",
307
+ nextAttemptAt: now(),
308
+ incrementAttempts: false
309
+ });
310
+ return false;
311
+ }
312
+ let { name, candidates } = resolved;
313
+ if (candidates.length === 0) {
314
+ const probed = await probeProvisioning(row, identity, name);
315
+ if (probed === null) return false;
316
+ candidates = probed;
271
317
  }
272
318
  const scope = { workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product: row.product };
273
319
  const active = await options.store.getActive(scope);
@@ -276,7 +322,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
276
322
  const toRevoke = candidates.map((candidate) => candidate.id.trim()).filter((candidateId) => candidateId && candidateId !== activeKeyId);
277
323
  for (const keyId of toRevoke) {
278
324
  try {
279
- await options.provisioner.revokeKey(keyId);
325
+ await recoveryProvisioner.revokeKey(keyId);
280
326
  } catch (error) {
281
327
  if (isRemoteMissing(error)) continue;
282
328
  await options.store.markRevocationPending({
@@ -288,10 +334,16 @@ function createIdentityBoundWorkspaceKeyManager(options) {
288
334
  return false;
289
335
  }
290
336
  }
291
- if (toRevoke.length > 0 || candidates.length === 0 || activeKeyId !== null) {
337
+ if (candidates.length > 0) {
292
338
  await options.store.markRevoked(row.id, now());
293
339
  } else {
294
- await options.store.markOrphaned(row.id, "no unowned remote child matched the durable provisioning record");
340
+ await options.store.markRevocationPending({
341
+ id: row.id,
342
+ error: "no remote child matched the durable provisioning record; retrying crash recovery",
343
+ nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),
344
+ incrementAttempts: true
345
+ });
346
+ return false;
295
347
  }
296
348
  return true;
297
349
  }
@@ -335,13 +387,21 @@ function createIdentityBoundWorkspaceKeyManager(options) {
335
387
  throw new Error("a previous workspace child-key cleanup is pending; issuance is blocked until it completes");
336
388
  }
337
389
  }
338
- async function retryPendingRevocations(scopeInput) {
390
+ async function retryPendingRevocations(scopeInput, identityInput) {
391
+ const identity = identityInput ? normalizeIdentity(identityInput) : void 0;
392
+ if (identity && scopeInput && (identity.workspaceId !== scopeInput.workspaceId || identity.ownerUserId !== scopeInput.ownerUserId)) {
393
+ throw new Error("workspace child key retry identity does not match its scope");
394
+ }
339
395
  if (scopeInput) {
340
396
  const workspaceId = scopeInput.workspaceId.trim();
341
397
  const ownerUserId = scopeInput.ownerUserId.trim();
342
398
  if (!workspaceId || !ownerUserId) throw new Error("workspace child key retry scope is incomplete");
343
399
  const scope = scopeKey({ workspaceId, ownerUserId, product });
344
- return withLocalLock(scope, () => withDurableLease(scope, async () => (await retryPendingRevocationsUnlocked({ workspaceId, ownerUserId })).completed));
400
+ return withLocalLock(scope, () => withDurableLease(scope, async () => (await retryPendingRevocationsUnlocked({ workspaceId, ownerUserId }, identity)).completed));
401
+ }
402
+ if (identity) {
403
+ const scope = scopeKey(scopeFor(identity, product));
404
+ return withLocalLock(scope, () => withDurableLease(scope, async () => (await retryPendingRevocationsUnlocked({ workspaceId: identity.workspaceId, ownerUserId: identity.ownerUserId }, identity)).completed));
345
405
  }
346
406
  const rows = await options.store.listPendingRevocations({ product, now: now(), limit: PENDING_REVOCATION_BATCH_SIZE });
347
407
  let completed = 0;
@@ -372,7 +432,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
372
432
  }
373
433
  let remote;
374
434
  try {
375
- remote = await options.provisioner.getKey(row.keyId);
435
+ remote = await recoveryProvisioner.getKey(row.keyId);
376
436
  } catch (error) {
377
437
  if (isRemoteMissing(error)) {
378
438
  await options.store.markRevoked(row.id, now());
@@ -400,6 +460,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
400
460
  const name = (options.nameForIdentity?.(identity, mintOperationId) ?? defaultKeyName(identity, product, mintOperationId)).trim();
401
461
  if (!name) throw new Error("workspace child key remote name is required");
402
462
  const rowId = operationId();
463
+ const idempotencyKey = `workspace-key:${rowId}`;
403
464
  const provisioningRow = {
404
465
  id: rowId,
405
466
  workspaceId: identity.workspaceId,
@@ -409,6 +470,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
409
470
  sourceKeyId: sourceKeyId(identity),
410
471
  sourceKeyFingerprint: identity.sourceKeyFingerprint,
411
472
  name,
473
+ idempotencyKey,
412
474
  keyId: `provisioning:${mintOperationId}`,
413
475
  keyEncrypted: "",
414
476
  budgetUsd,
@@ -426,7 +488,8 @@ function createIdentityBoundWorkspaceKeyManager(options) {
426
488
  name,
427
489
  product,
428
490
  budgetUsd,
429
- expiresAt: expiresAt.toISOString()
491
+ expiresAt: expiresAt.toISOString(),
492
+ idempotencyKey
430
493
  });
431
494
  } catch (error) {
432
495
  try {
@@ -445,7 +508,7 @@ function createIdentityBoundWorkspaceKeyManager(options) {
445
508
  if (!remoteId || !secret) {
446
509
  if (remoteId) {
447
510
  try {
448
- await options.store.markProvisioningRemote({ id: rowId, keyId: remoteId });
511
+ await markProvisioningRemote(options.store, { id: rowId, keyId: remoteId });
449
512
  } catch {
450
513
  }
451
514
  }
@@ -457,28 +520,47 @@ function createIdentityBoundWorkspaceKeyManager(options) {
457
520
  await revokePending(pendingRow, identity);
458
521
  throw new Error("remote create returned no usable child key");
459
522
  }
460
- onRemoteKey?.(provisioningRow, remoteId);
523
+ let cleanupRow = provisioningRow;
524
+ onRemoteKey?.(cleanupRow, remoteId);
525
+ async function retainRetiredCreatorCleanup() {
526
+ const retainedRow = { ...provisioningRow, id: operationId(), keyId: remoteId };
527
+ try {
528
+ await options.store.insertProvisioning(retainedRow);
529
+ } catch (cause) {
530
+ throw new Error("returned child cleanup record could not be persisted", { cause });
531
+ }
532
+ cleanupRow = retainedRow;
533
+ onRemoteKey?.(cleanupRow, remoteId);
534
+ }
461
535
  try {
462
- await options.store.markProvisioningRemote({ id: rowId, keyId: remoteId });
536
+ const recorded = await markProvisioningRemote(options.store, { id: rowId, keyId: remoteId });
537
+ if (!recorded) {
538
+ await retainRetiredCreatorCleanup();
539
+ throw new Error("workspace child-key provisioning row was retired before the remote id was recorded");
540
+ }
463
541
  const keyEncrypted = await options.crypto.encrypt(secret);
464
542
  const remoteExpiresAt = created.expiresAt?.trim();
465
543
  const activeExpiresAt = remoteExpiresAt ? new Date(remoteExpiresAt) : expiresAt;
466
544
  if (!Number.isFinite(activeExpiresAt.getTime())) throw new Error("remote child key expiry is malformed");
467
545
  const activeBudgetUsd = created.budgetUsd ?? budgetUsd;
468
546
  if (!Number.isFinite(activeBudgetUsd) || activeBudgetUsd < 0) throw new Error("remote child key budget is malformed");
469
- await options.store.markActive({
547
+ const activated = await markActive(options.store, {
470
548
  id: rowId,
471
549
  keyId: remoteId,
472
550
  keyEncrypted,
473
551
  expiresAt: activeExpiresAt,
474
552
  budgetUsd: activeBudgetUsd
475
553
  });
554
+ if (!activated) {
555
+ await retainRetiredCreatorCleanup();
556
+ throw new Error("workspace child-key provisioning row was retired before activation");
557
+ }
476
558
  } catch (error) {
477
559
  try {
478
- await markPending(rowId, error, false);
560
+ await markPending(cleanupRow.id, error, false);
479
561
  } catch {
480
562
  }
481
- await revokePending({ ...provisioningRow, keyId: remoteId, status: "revocation_pending" }, identity);
563
+ await revokePending({ ...cleanupRow, keyId: remoteId, status: "revocation_pending" }, identity);
482
564
  throw error;
483
565
  }
484
566
  const row = await options.store.getActive(scopeFor(identity, product));
@@ -698,4 +780,4 @@ export {
698
780
  createPlatformBalanceManager,
699
781
  createWorkspaceKeyManager
700
782
  };
701
- //# sourceMappingURL=chunk-TS2ZIB53.js.map
783
+ //# sourceMappingURL=chunk-4ZJYNI53.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/billing/crypto-probe.ts","../src/billing/identity-bound.ts","../src/billing/index.ts"],"sourcesContent":["import type { KeyCrypto } from './index'\n\n/** Prove the at-rest crypto seam before a remote key is minted. */\nexport async function assertWorkspaceKeyCryptoUsable(crypto: KeyCrypto): Promise<void> {\n const probe = 'agent-app:key-manager:crypto-probe'\n let roundTripped: string\n try {\n roundTripped = await crypto.decrypt(await crypto.encrypt(probe))\n } catch (error) {\n throw new Error(\n 'Key encryption is misconfigured: the crypto seam threw before minting. ' +\n 'Validate FIELD_ENCRYPTION_KEY (64-char hex) at startup. No platform key was minted.',\n { cause: error },\n )\n }\n if (roundTripped !== probe) {\n throw new Error(\n 'Key encryption is misconfigured: encrypt/decrypt round-trip did not preserve the plaintext. ' +\n 'No platform key was minted.',\n )\n }\n}\n","import { assertWorkspaceKeyCryptoUsable } from './crypto-probe'\nimport type { KeyCrypto, KeyProvisioner } from './index'\n\n/** Lifecycle state for a durable delegated key row. */\nexport type WorkspaceKeyStatus =\n | 'legacy'\n | 'provisioning'\n | 'active'\n | 'revocation_pending'\n | 'revoked'\n | 'orphaned'\n\n/** Product partition used by the key store and remote provisioner. */\nexport type WorkspaceKeyProduct = string\n\n/** The identity fields that bind a child key to its paying owner and source. */\nexport interface WorkspaceKeyIdentity {\n workspaceId: string\n ownerUserId: string\n platformUserId: string\n sourceKeyId?: string | null\n /** A non-secret digest of the source credential. */\n sourceKeyFingerprint: string\n}\n\n/** A product/workspace/owner lookup scope. */\nexport interface DurableWorkspaceKeyScope {\n workspaceId: string\n ownerUserId: string\n product: WorkspaceKeyProduct\n}\n\n/** A persisted child-key row owned by this manager. */\nexport interface DurableWorkspaceKeyRecord {\n id: string\n workspaceId: string\n ownerUserId: string\n product: WorkspaceKeyProduct\n platformUserId: string\n sourceKeyId: string | null\n sourceKeyFingerprint: string\n /** The persisted name used to recover a remote create after a crash. Null only for pre-name rows. */\n name: string | null\n /** The persisted retry identity for the remote create. Absent on legacy rows without a persisted create identity. */\n idempotencyKey?: string | null\n keyId: string\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date\n status: WorkspaceKeyStatus\n revocationAttempts: number\n nextRevocationAt: Date | null\n lastRevocationError: string | null\n createdAt: Date\n}\n\n/** A row that has been written before its remote child key exists. */\nexport interface DurableWorkspaceKeyProvisioningRecord extends DurableWorkspaceKeyRecord {\n status: 'provisioning'\n}\n\ntype DurableWorkspaceKeyCreateResult = Awaited<ReturnType<KeyProvisioner['createKey']>> & {\n budgetUsd?: number | null\n budgetRemaining?: number\n expiresAt?: string | null\n}\n\n/** The exact request identity that must be reused when recovering a create. */\nexport interface DurableWorkspaceKeyCreateInput {\n name: string\n product: string\n budgetUsd: number\n expiresAt: string\n /** Stable across process restarts for one persisted provisioning row. */\n idempotencyKey: string\n}\n\n/** The remote operations required by the durable manager. */\nexport interface DurableWorkspaceKeyProvisioner {\n /** True only when replay returns the original child for the persisted request identity. */\n supportsIdempotentCreate?: true\n /** Create one child key. Non-idempotent providers recover through stable-name discovery only. */\n createKey(input: DurableWorkspaceKeyCreateInput): Promise<DurableWorkspaceKeyCreateResult>\n getKey(id: string): Promise<{\n budgetUsd?: number | null\n budgetSpent?: number\n expiresAt?: string | null\n }>\n revokeKey: KeyProvisioner['revokeKey']\n /** Find remote keys for a specific persisted provisioning attempt. */\n findCreatedKeys(input: {\n name: string\n product: string\n sourceKeyId?: string | null\n }): Promise<Array<{ id: string }>>\n}\n\n/** Optional compare-and-set lifecycle writes for stores that support fencing. */\nexport interface DurableWorkspaceKeyConditionalWrites {\n /** Return false when the row was no longer provisioning. */\n markProvisioningRemote(input: { id: string; keyId: string }): Promise<boolean>\n /** Return false when the row was no longer provisioning. */\n markActive(input: {\n id: string\n keyId: string\n keyEncrypted: string\n expiresAt: Date\n budgetUsd: number\n }): Promise<boolean>\n}\n\n/** Persistence operations for identity-bound key lifecycle state. */\nexport interface DurableWorkspaceKeyStore {\n /** Return the active row for this exact product scope. */\n getActive(scope: DurableWorkspaceKeyScope): Promise<DurableWorkspaceKeyRecord | null>\n /** Return every unfinished provisioning row for this exact product scope. */\n listProvisioning(scope: DurableWorkspaceKeyScope): Promise<DurableWorkspaceKeyProvisioningRecord[]>\n /** Insert before the remote create so a crashed create can be recovered. */\n insertProvisioning(record: DurableWorkspaceKeyProvisioningRecord): Promise<void>\n /** Save the remote id while the row remains in provisioning state. */\n markProvisioningRemote(input: { id: string; keyId: string }): Promise<void>\n /** Promote a fully encrypted row to active state. */\n markActive(input: {\n id: string\n keyId: string\n keyEncrypted: string\n expiresAt: Date\n budgetUsd: number\n }): Promise<void>\n /**\n * Optional fenced writes. The manager uses false to compensate a stale\n * creator. Stores without this field retain the pre-fence behavior.\n */\n conditionalWrites?: DurableWorkspaceKeyConditionalWrites\n /** Keep a failed cleanup visible and schedule a later retry. Terminal rows are immutable. */\n markRevocationPending(input: {\n id: string\n error?: string | null\n nextAttemptAt: Date\n incrementAttempts: boolean\n }): Promise<void>\n /** Mark a remote key as revoked. Missing remote keys count as revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n /** Mark a provisioning row terminal when no remote key remains to clean. */\n markOrphaned(id: string, error: string): Promise<void>\n /** Query due cleanup rows. The implementation should filter by the supplied scope. */\n listPendingRevocations(input: {\n product: WorkspaceKeyProduct\n workspaceId?: string\n ownerUserId?: string\n now: Date\n limit: number\n /** Include rows scheduled for a later retry when checking before issuance. */\n includeFuture?: boolean\n }): Promise<DurableWorkspaceKeyRecord[]>\n /** Acquire a cross-process lease for one product/workspace/owner scope. */\n acquireLease(scope: string, operationId: string, now: Date, leaseMs: number): Promise<boolean>\n /** Renew an acquired lease before its expiry. */\n renewLease(scope: string, operationId: string, now: Date, leaseMs: number): Promise<boolean>\n /** Release an acquired lease. */\n releaseLease(scope: string, operationId: string): Promise<void>\n}\n\n/** Live budget information for a child key. */\nexport interface WorkspaceKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string\n exhausted: boolean\n}\n\n/** The decrypted key plus the usage snapshot used to issue it. */\nexport interface WorkspaceRuntimeKey {\n key: string\n usage: WorkspaceKeyUsage\n /** True when this call minted a new remote key. */\n refreshed: boolean\n}\n\n/** Identity-bound durable key manager API. */\nexport interface DurableWorkspaceKeyManager {\n /** Reuse a matching active key or reconcile and mint one under a lease. */\n ensureKey(identity: WorkspaceKeyIdentity, options?: { budgetUsd?: number }): Promise<WorkspaceRuntimeKey>\n /** Read usage without returning the child secret. */\n getUsage(identity: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null>\n /**\n * Retry due cleanup rows. A supplied owner/workspace limits the retry to its\n * scope. Empty provisioning rows also require the full identity so a retry\n * can use the original source binding safely.\n */\n retryPendingRevocations(\n scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>,\n identity?: WorkspaceKeyIdentity,\n ): Promise<number>\n}\n\n/** Configuration for {@link createIdentityBoundWorkspaceKeyManager}. */\nexport interface DurableWorkspaceKeyManagerOptions {\n store: DurableWorkspaceKeyStore\n provisioner: DurableWorkspaceKeyProvisioner\n /**\n * Control-plane client for historical get/revoke/list operations. Use this\n * when the source credential can rotate or disappear. It never mints keys.\n */\n recoveryProvisioner?: Pick<DurableWorkspaceKeyProvisioner, 'getKey' | 'revokeKey' | 'findCreatedKeys'>\n crypto: KeyCrypto\n /** Product partition. Products must use separate values. */\n product: WorkspaceKeyProduct\n defaultBudgetUsd: number\n now?: () => Date\n /** Local lifetime used when the remote response omits an expiry. */\n keyLifetimeMs?: number\n /** Lease lifetime for remote operations. */\n leaseMs?: number\n /** Maximum time to wait for another issuer to release the scope lease. */\n leaseWaitMs?: number\n /** Poll delay while waiting for a scope lease. */\n leasePollMs?: number\n /** Renewal interval. It must be shorter than `leaseMs`. */\n leaseRenewIntervalMs?: number\n /** Initial delay for a failed remote revoke. */\n revocationRetryBaseMs?: number\n /** Maximum delay for a failed remote revoke. */\n revocationRetryMaxMs?: number\n /** Stable remote name. The operation id makes the default crash-safe. */\n nameForIdentity?: (identity: WorkspaceKeyIdentity, operationId: string) => string\n /** Resolve names for rows written before `name` became durable. Null blocks issuance safely. */\n legacyNameForRecord?: (record: DurableWorkspaceKeyRecord) => string | null | undefined\n /** Recognize a missing remote key without depending on a provider SDK. */\n isRemoteMissing?: (error: unknown) => boolean\n}\n\nconst DEFAULT_KEY_LIFETIME_MS = 55 * 60_000\nconst DEFAULT_LEASE_MS = 60_000\nconst DEFAULT_LEASE_WAIT_MS = 15_000\nconst DEFAULT_LEASE_POLL_MS = 50\nconst DEFAULT_LEASE_RENEW_INTERVAL_MS = Math.floor(DEFAULT_LEASE_MS / 3)\nconst DEFAULT_REVOCATION_RETRY_BASE_MS = 2_000\nconst DEFAULT_REVOCATION_RETRY_MAX_MS = 60_000\nconst PENDING_REVOCATION_BATCH_SIZE = 20\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction operationId(): string {\n return globalThis.crypto.randomUUID()\n}\n\nfunction requirePositive(value: number, label: string): number {\n if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number`)\n return value\n}\n\nfunction requireNonNegative(value: number, label: string): number {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a non-negative finite number`)\n return value\n}\n\nfunction normalizeIdentity(identity: WorkspaceKeyIdentity): WorkspaceKeyIdentity {\n const workspaceId = identity.workspaceId.trim()\n const ownerUserId = identity.ownerUserId.trim()\n const platformUserId = identity.platformUserId.trim()\n const sourceKeyFingerprint = identity.sourceKeyFingerprint.trim()\n const sourceKeyId = identity.sourceKeyId?.trim() || null\n if (!workspaceId || !ownerUserId || !platformUserId || !sourceKeyFingerprint) {\n throw new Error('workspace child key identity is incomplete')\n }\n return { workspaceId, ownerUserId, platformUserId, sourceKeyId, sourceKeyFingerprint }\n}\n\nfunction scopeFor(identity: Pick<WorkspaceKeyIdentity, 'workspaceId' | 'ownerUserId'>, product: string): DurableWorkspaceKeyScope {\n return { workspaceId: identity.workspaceId, ownerUserId: identity.ownerUserId, product }\n}\n\nfunction scopeKey(scope: DurableWorkspaceKeyScope): string {\n return JSON.stringify([scope.product, scope.workspaceId, scope.ownerUserId])\n}\n\nfunction safeNamePart(value: string): string {\n const normalized = value.replace(/[^A-Za-z0-9._-]/g, '-').replace(/-+/g, '-')\n return normalized.slice(0, 80) || 'unknown'\n}\n\nfunction defaultKeyName(identity: WorkspaceKeyIdentity, product: string, operationId: string): string {\n return `agent-app:${safeNamePart(product)}:${safeNamePart(identity.workspaceId)}:${safeNamePart(identity.platformUserId)}:${operationId}`\n}\n\nfunction sourceKeyId(identity: WorkspaceKeyIdentity): string | null {\n return identity.sourceKeyId?.trim() || null\n}\n\nfunction sameIdentity(row: DurableWorkspaceKeyRecord, identity: WorkspaceKeyIdentity, product: string): boolean {\n return row.workspaceId === identity.workspaceId\n && row.ownerUserId === identity.ownerUserId\n && row.product === product\n && row.platformUserId === identity.platformUserId\n && row.sourceKeyId === sourceKeyId(identity)\n && row.sourceKeyFingerprint === identity.sourceKeyFingerprint\n}\n\nfunction sameRowIdentity(a: DurableWorkspaceKeyRecord, b: DurableWorkspaceKeyRecord): boolean {\n return a.workspaceId === b.workspaceId\n && a.ownerUserId === b.ownerUserId\n && a.product === b.product\n && a.platformUserId === b.platformUserId\n && a.sourceKeyId === b.sourceKeyId\n && a.sourceKeyFingerprint === b.sourceKeyFingerprint\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return (error instanceof Error ? error.message : fallback)\n .replace(/Bearer\\s+[^\\s,;]+/gi, 'Bearer [redacted]')\n .replace(/((?:authorization|api[-_ ]?key|token|secret|password)\\s*[:=]\\s*)(?!Bearer\\b)[^\\s,;]+/gi, '$1[redacted]')\n .slice(0, 240)\n}\n\nfunction isProvisioningId(value: string): boolean {\n return value.startsWith('provisioning:')\n}\n\nasync function markProvisioningRemote(\n store: DurableWorkspaceKeyStore,\n input: { id: string; keyId: string },\n): Promise<boolean> {\n if (store.conditionalWrites) return store.conditionalWrites.markProvisioningRemote(input)\n await store.markProvisioningRemote(input)\n return true\n}\n\nasync function markActive(\n store: DurableWorkspaceKeyStore,\n input: {\n id: string\n keyId: string\n keyEncrypted: string\n expiresAt: Date\n budgetUsd: number\n },\n): Promise<boolean> {\n if (store.conditionalWrites) return store.conditionalWrites.markActive(input)\n await store.markActive(input)\n return true\n}\n\nfunction usageFromRemote(\n row: DurableWorkspaceKeyRecord,\n remote: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['getKey']>>,\n now: Date,\n): WorkspaceKeyUsage {\n const budgetUsd = remote.budgetUsd ?? row.budgetUsd\n const budgetSpent = remote.budgetSpent ?? 0\n if (!Number.isFinite(budgetUsd) || budgetUsd < 0) throw new Error('workspace child key budget is malformed')\n if (!Number.isFinite(budgetSpent) || budgetSpent < 0) throw new Error('workspace child key usage is malformed')\n const expiresAt = remote.expiresAt ?? row.expiresAt.toISOString()\n const expiryMs = Date.parse(expiresAt)\n if (!Number.isFinite(expiryMs)) throw new Error('workspace child key expiry is malformed')\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: row.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt,\n exhausted: budgetRemaining <= 0 || expiryMs <= now.getTime(),\n }\n}\n\nfunction defaultRemoteMissing(error: unknown): boolean {\n const value = error as { status?: unknown; code?: unknown } | null\n return value?.status === 404 || value?.code === 'not_found'\n}\n\n/**\n * Create a product-neutral manager for child-key issuance and cleanup.\n *\n * The provisioner must already be authenticated for the current source\n * credential. The manager persists only the source id and its non-secret\n * fingerprint, then refuses to reuse a row from another identity.\n */\nexport function createIdentityBoundWorkspaceKeyManager(\n options: DurableWorkspaceKeyManagerOptions,\n): DurableWorkspaceKeyManager {\n const product = options.product.trim()\n if (!product) throw new Error('workspace child key product is required')\n const defaultBudgetUsd = requirePositive(options.defaultBudgetUsd, 'defaultBudgetUsd')\n const keyLifetimeMs = requirePositive(options.keyLifetimeMs ?? DEFAULT_KEY_LIFETIME_MS, 'keyLifetimeMs')\n const leaseMs = requirePositive(options.leaseMs ?? DEFAULT_LEASE_MS, 'leaseMs')\n const leaseWaitMs = requireNonNegative(options.leaseWaitMs ?? DEFAULT_LEASE_WAIT_MS, 'leaseWaitMs')\n const leasePollMs = requirePositive(options.leasePollMs ?? DEFAULT_LEASE_POLL_MS, 'leasePollMs')\n const leaseRenewIntervalMs = requirePositive(\n options.leaseRenewIntervalMs ?? Math.min(DEFAULT_LEASE_RENEW_INTERVAL_MS, Math.floor(leaseMs / 3)),\n 'leaseRenewIntervalMs',\n )\n if (leaseRenewIntervalMs >= leaseMs) throw new Error('leaseRenewIntervalMs must be shorter than leaseMs')\n const revocationRetryBaseMs = requirePositive(\n options.revocationRetryBaseMs ?? DEFAULT_REVOCATION_RETRY_BASE_MS,\n 'revocationRetryBaseMs',\n )\n const revocationRetryMaxMs = requirePositive(\n options.revocationRetryMaxMs ?? DEFAULT_REVOCATION_RETRY_MAX_MS,\n 'revocationRetryMaxMs',\n )\n if (revocationRetryMaxMs < revocationRetryBaseMs) throw new Error('revocationRetryMaxMs must cover the base delay')\n\n const now = options.now ?? (() => new Date())\n const isRemoteMissing = options.isRemoteMissing ?? defaultRemoteMissing\n const recoveryProvisioner = options.recoveryProvisioner ?? options.provisioner\n const localLocks = new Map<string, Promise<void>>()\n\n async function withLocalLock<T>(scope: string, work: () => Promise<T>): Promise<T> {\n const previous = localLocks.get(scope) ?? Promise.resolve()\n let release!: () => void\n const current = new Promise<void>((resolve) => { release = resolve })\n localLocks.set(scope, current)\n await previous\n try {\n return await work()\n } finally {\n release()\n if (localLocks.get(scope) === current) localLocks.delete(scope)\n }\n }\n\n async function withDurableLease<T>(\n scope: string,\n work: () => Promise<T>,\n onLeaseLost?: () => Promise<void>,\n ): Promise<T> {\n const leaseOperationId = operationId()\n const startedAt = Date.now()\n let acquired = false\n while (!acquired && Date.now() - startedAt <= leaseWaitMs) {\n acquired = await options.store.acquireLease(scope, leaseOperationId, now(), leaseMs)\n if (!acquired) await sleep(leasePollMs)\n }\n if (!acquired) throw new Error('workspace child-key issuance is busy; retry the request')\n\n let stopped = false\n let leaseLost: Error | null = null\n let leaseTimer: ReturnType<typeof setTimeout> | undefined\n let renewalPromise: Promise<void> | undefined\n const scheduleRenewal = () => {\n if (stopped || leaseLost) return\n leaseTimer = setTimeout(() => {\n leaseTimer = undefined\n const renewal = (async () => {\n try {\n if (!await options.store.renewLease(scope, leaseOperationId, now(), leaseMs)) {\n leaseLost = new Error('workspace child-key lease was lost during reconciliation')\n return\n }\n scheduleRenewal()\n } catch (error) {\n leaseLost = error instanceof Error ? error : new Error('workspace child-key lease renewal failed')\n }\n })()\n renewalPromise = renewal\n void renewal.finally(() => {\n if (renewalPromise === renewal) renewalPromise = undefined\n }).catch(() => undefined)\n }, leaseRenewIntervalMs)\n }\n scheduleRenewal()\n\n let workError: unknown\n let leaseLossCleanupError: unknown\n try {\n const result = await work()\n if (leaseLost) throw leaseLost\n return result\n } catch (error) {\n workError = error\n throw error\n } finally {\n stopped = true\n if (leaseTimer) clearTimeout(leaseTimer)\n if (renewalPromise) await renewalPromise\n if (leaseLost && onLeaseLost) {\n try {\n await onLeaseLost()\n } catch (error) {\n leaseLossCleanupError = error\n }\n }\n try {\n await options.store.releaseLease(scope, leaseOperationId)\n } catch (error) {\n if (workError === undefined) throw error\n }\n if (workError === undefined && leaseLossCleanupError) {\n throw new Error('workspace child-key lease was lost and cleanup failed', { cause: leaseLossCleanupError })\n }\n if (workError === undefined && leaseLost) throw leaseLost\n }\n }\n\n function retryDelay(attempts: number): number {\n return Math.min(revocationRetryMaxMs, revocationRetryBaseMs * 2 ** Math.min(Math.max(attempts - 1, 0), 5))\n }\n\n async function markPending(id: string, error: unknown, incrementAttempts: boolean): Promise<void> {\n const delay = incrementAttempts ? retryDelay(1) : 0\n await options.store.markRevocationPending({\n id,\n error: errorMessage(error, 'remote key cleanup failed'),\n nextAttemptAt: new Date(now().getTime() + delay),\n incrementAttempts,\n })\n }\n\n async function revokePending(row: DurableWorkspaceKeyRecord, identity?: WorkspaceKeyIdentity): Promise<boolean> {\n if (!row.keyId || isProvisioningId(row.keyId)) return cleanupProvisioning(row, identity)\n const active = await options.store.getActive({\n workspaceId: row.workspaceId,\n ownerUserId: row.ownerUserId,\n product: row.product,\n })\n if (active && active.id !== row.id && active.keyId === row.keyId) {\n await options.store.markRevoked(row.id, now())\n return true\n }\n try {\n await recoveryProvisioner.revokeKey(row.keyId)\n await options.store.markRevoked(row.id, now())\n return true\n } catch (error) {\n if (isRemoteMissing(error)) {\n await options.store.markRevoked(row.id, now())\n return true\n }\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote key revocation failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n }\n\n async function provisioningCandidates(row: DurableWorkspaceKeyRecord): Promise<{\n name: string\n candidates: Array<{ id: string }>\n } | null> {\n if (row.keyId && !isProvisioningId(row.keyId)) {\n return { name: row.name?.trim() ?? '', candidates: [{ id: row.keyId }] }\n }\n const name = row.name?.trim() || options.legacyNameForRecord?.(row)?.trim()\n if (!name) return null\n return {\n name,\n candidates: await recoveryProvisioner.findCreatedKeys({\n name,\n product: row.product,\n sourceKeyId: row.sourceKeyId,\n }),\n }\n }\n\n async function probeProvisioning(\n row: DurableWorkspaceKeyRecord,\n identity: WorkspaceKeyIdentity | undefined,\n name: string,\n ): Promise<Array<{ id: string }> | null> {\n const idempotencyKey = row.idempotencyKey?.trim()\n if (!identity || !sameIdentity(row, identity, product)\n || !options.provisioner.supportsIdempotentCreate || !idempotencyKey) return []\n try {\n const created = await options.provisioner.createKey({\n name,\n product: row.product,\n budgetUsd: row.budgetUsd,\n expiresAt: row.expiresAt.toISOString(),\n idempotencyKey,\n })\n const remoteId = created.id?.trim()\n if (remoteId) return [{ id: remoteId }]\n } catch (error) {\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote provisioning recovery failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return null\n }\n return (await provisioningCandidates({ ...row, name, keyId: `provisioning:${idempotencyKey}` }))?.candidates ?? []\n }\n\n async function cleanupProvisioning(row: DurableWorkspaceKeyRecord, identity?: WorkspaceKeyIdentity): Promise<boolean> {\n const resolved = await provisioningCandidates(row)\n if (!resolved) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'provisioning row has no recoverable remote name; migrate the row or configure legacyNameForRecord',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return false\n }\n let { name, candidates } = resolved\n if (candidates.length === 0) {\n const probed = await probeProvisioning(row, identity, name)\n if (probed === null) return false\n candidates = probed\n }\n\n const scope = { workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product: row.product }\n const active = await options.store.getActive(scope)\n const activeMatches = active && (identity ? sameIdentity(active, identity, product) : sameRowIdentity(active, row))\n const activeKeyId = activeMatches ? active.keyId : null\n const toRevoke = candidates\n .map((candidate) => candidate.id.trim())\n .filter((candidateId) => candidateId && candidateId !== activeKeyId)\n\n for (const keyId of toRevoke) {\n try {\n await recoveryProvisioner.revokeKey(keyId)\n } catch (error) {\n if (isRemoteMissing(error)) continue\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote provisioning cleanup failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n }\n\n if (candidates.length > 0) {\n await options.store.markRevoked(row.id, now())\n } else {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'no remote child matched the durable provisioning record; retrying crash recovery',\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n return true\n }\n\n async function reconcileProvisioning(identity: WorkspaceKeyIdentity): Promise<void> {\n const scope = scopeFor(identity, product)\n const rows = await options.store.listProvisioning(scope)\n for (const row of rows) {\n if (!(await cleanupProvisioning(row, identity))) {\n throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n }\n }\n }\n\n async function retryPendingRevocationsUnlocked(\n scopeFilter?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>,\n activeIdentity?: WorkspaceKeyIdentity,\n ): Promise<{ completed: number; pending: number }> {\n const rows = await options.store.listPendingRevocations({\n product,\n workspaceId: scopeFilter?.workspaceId,\n ownerUserId: scopeFilter?.ownerUserId,\n now: now(),\n limit: PENDING_REVOCATION_BATCH_SIZE,\n })\n let completed = 0\n let pending = 0\n for (const row of rows) {\n if (row.product !== product) continue\n if (scopeFilter && (row.workspaceId !== scopeFilter.workspaceId || row.ownerUserId !== scopeFilter.ownerUserId)) continue\n if (await revokePending(row, activeIdentity)) completed += 1\n else pending += 1\n }\n return { completed, pending }\n }\n\n async function assertNoPendingRevocations(identity: WorkspaceKeyIdentity): Promise<void> {\n const rows = await options.store.listPendingRevocations({\n product,\n workspaceId: identity.workspaceId,\n ownerUserId: identity.ownerUserId,\n now: now(),\n limit: PENDING_REVOCATION_BATCH_SIZE,\n includeFuture: true,\n })\n if (rows.some((row) => row.product === product\n && row.workspaceId === identity.workspaceId\n && row.ownerUserId === identity.ownerUserId)) {\n throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n }\n }\n\n async function retryPendingRevocations(\n scopeInput?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>,\n identityInput?: WorkspaceKeyIdentity,\n ): Promise<number> {\n const identity = identityInput ? normalizeIdentity(identityInput) : undefined\n if (identity && scopeInput\n && (identity.workspaceId !== scopeInput.workspaceId || identity.ownerUserId !== scopeInput.ownerUserId)) {\n throw new Error('workspace child key retry identity does not match its scope')\n }\n if (scopeInput) {\n const workspaceId = scopeInput.workspaceId.trim()\n const ownerUserId = scopeInput.ownerUserId.trim()\n if (!workspaceId || !ownerUserId) throw new Error('workspace child key retry scope is incomplete')\n const scope = scopeKey({ workspaceId, ownerUserId, product })\n return withLocalLock(scope, () => withDurableLease(scope, async () => (\n await retryPendingRevocationsUnlocked({ workspaceId, ownerUserId }, identity)).completed))\n }\n\n if (identity) {\n const scope = scopeKey(scopeFor(identity, product))\n return withLocalLock(scope, () => withDurableLease(scope, async () => (\n await retryPendingRevocationsUnlocked({ workspaceId: identity.workspaceId, ownerUserId: identity.ownerUserId }, identity)).completed))\n }\n\n const rows = await options.store.listPendingRevocations({ product, now: now(), limit: PENDING_REVOCATION_BATCH_SIZE })\n let completed = 0\n const scopes = new Set<string>()\n for (const row of rows) {\n if (row.product !== product) continue\n const scope = scopeKey({ workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product })\n if (scopes.has(scope)) continue\n scopes.add(scope)\n completed += await withLocalLock(scope, () => withDurableLease(scope, async () => (await retryPendingRevocationsUnlocked({\n workspaceId: row.workspaceId,\n ownerUserId: row.ownerUserId,\n })).completed))\n }\n return completed\n }\n\n async function readActive(identity: WorkspaceKeyIdentity): Promise<{ row: DurableWorkspaceKeyRecord; usage: WorkspaceKeyUsage } | null> {\n const row = await options.store.getActive(scopeFor(identity, product))\n if (!row) return null\n if (!sameIdentity(row, identity, product)) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'active child key identity does not match the authenticated owner and source',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return null\n }\n\n let remote: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['getKey']>>\n try {\n remote = await recoveryProvisioner.getKey(row.keyId)\n } catch (error) {\n if (isRemoteMissing(error)) {\n await options.store.markRevoked(row.id, now())\n return null\n }\n throw error\n }\n const usage = usageFromRemote(row, remote, now())\n if (usage.exhausted) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'active child key is expired or exhausted',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return null\n }\n return { row, usage }\n }\n\n async function mint(\n identity: WorkspaceKeyIdentity,\n budgetUsd: number,\n onRemoteKey?: (row: DurableWorkspaceKeyProvisioningRecord, remoteId: string) => void,\n ): Promise<WorkspaceRuntimeKey> {\n await assertWorkspaceKeyCryptoUsable(options.crypto)\n const mintOperationId = operationId()\n const createdAt = now()\n const expiresAt = new Date(createdAt.getTime() + keyLifetimeMs)\n const name = (options.nameForIdentity?.(identity, mintOperationId) ?? defaultKeyName(identity, product, mintOperationId)).trim()\n if (!name) throw new Error('workspace child key remote name is required')\n const rowId = operationId()\n const idempotencyKey = `workspace-key:${rowId}`\n const provisioningRow: DurableWorkspaceKeyProvisioningRecord = {\n id: rowId,\n workspaceId: identity.workspaceId,\n ownerUserId: identity.ownerUserId,\n product,\n platformUserId: identity.platformUserId,\n sourceKeyId: sourceKeyId(identity),\n sourceKeyFingerprint: identity.sourceKeyFingerprint,\n name,\n idempotencyKey,\n keyId: `provisioning:${mintOperationId}`,\n keyEncrypted: '',\n budgetUsd,\n expiresAt,\n status: 'provisioning',\n revocationAttempts: 0,\n nextRevocationAt: null,\n lastRevocationError: null,\n createdAt,\n }\n await options.store.insertProvisioning(provisioningRow)\n\n let created: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['createKey']>>\n try {\n created = await options.provisioner.createKey({\n name,\n product,\n budgetUsd,\n expiresAt: expiresAt.toISOString(),\n idempotencyKey,\n })\n } catch (error) {\n // Keep cleanup running even when the state write fails. The remote\n // request may have committed before its response failed.\n try {\n await markPending(rowId, error, false)\n } catch {\n // cleanupProvisioning below can still find the remote name.\n }\n try {\n await cleanupProvisioning({ ...provisioningRow, status: 'revocation_pending' }, identity)\n } catch (cleanupError) {\n throw new Error('remote create failed and its cleanup could not be completed', { cause: cleanupError })\n }\n throw error\n }\n\n const remoteId = created.id?.trim() ?? ''\n const secret = created.key?.trim() ?? ''\n if (!remoteId || !secret) {\n if (remoteId) {\n try {\n await markProvisioningRemote(options.store, { id: rowId, keyId: remoteId })\n } catch {\n // revokePending uses the returned id even when this state write fails.\n }\n }\n try {\n await markPending(rowId, new Error('remote create returned no usable child key'), false)\n } catch {\n // Always attempt remote cleanup below.\n }\n const pendingRow = { ...provisioningRow, keyId: remoteId || provisioningRow.keyId, status: 'revocation_pending' as const }\n await revokePending(pendingRow, identity)\n throw new Error('remote create returned no usable child key')\n }\n\n let cleanupRow = provisioningRow\n onRemoteKey?.(cleanupRow, remoteId)\n\n async function retainRetiredCreatorCleanup(): Promise<void> {\n // A retired row cannot hold another pending transition. Persist the\n // returned child separately before revocation so a failed revoke survives restart.\n const retainedRow = { ...provisioningRow, id: operationId(), keyId: remoteId }\n try {\n await options.store.insertProvisioning(retainedRow)\n } catch (cause) {\n throw new Error('returned child cleanup record could not be persisted', { cause })\n }\n cleanupRow = retainedRow\n onRemoteKey?.(cleanupRow, remoteId)\n }\n\n try {\n const recorded = await markProvisioningRemote(options.store, { id: rowId, keyId: remoteId })\n if (!recorded) {\n await retainRetiredCreatorCleanup()\n throw new Error('workspace child-key provisioning row was retired before the remote id was recorded')\n }\n const keyEncrypted = await options.crypto.encrypt(secret)\n const remoteExpiresAt = created.expiresAt?.trim()\n const activeExpiresAt = remoteExpiresAt ? new Date(remoteExpiresAt) : expiresAt\n if (!Number.isFinite(activeExpiresAt.getTime())) throw new Error('remote child key expiry is malformed')\n const activeBudgetUsd = created.budgetUsd ?? budgetUsd\n if (!Number.isFinite(activeBudgetUsd) || activeBudgetUsd < 0) throw new Error('remote child key budget is malformed')\n const activated = await markActive(options.store, {\n id: rowId,\n keyId: remoteId,\n keyEncrypted,\n expiresAt: activeExpiresAt,\n budgetUsd: activeBudgetUsd,\n })\n if (!activated) {\n await retainRetiredCreatorCleanup()\n throw new Error('workspace child-key provisioning row was retired before activation')\n }\n } catch (error) {\n // A persistence failure must not strand the already-created remote key.\n try {\n await markPending(cleanupRow.id, error, false)\n } catch {\n // revokePending below still has the remote id and attempts cleanup.\n }\n await revokePending({ ...cleanupRow, keyId: remoteId, status: 'revocation_pending' }, identity)\n throw error\n }\n\n const row = await options.store.getActive(scopeFor(identity, product))\n if (!row || row.keyId !== remoteId || !sameIdentity(row, identity, product)) {\n const error = new Error('workspace child key was not persisted as active')\n try {\n await options.store.markRevocationPending({\n id: rowId,\n error: error.message,\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n } catch {\n // The remote id is still available for the cleanup attempt below.\n }\n await revokePending({ ...provisioningRow, keyId: remoteId, status: 'revocation_pending' }, identity)\n throw error\n }\n const usage = usageFromRemote(row, {\n budgetUsd: created.budgetUsd,\n budgetSpent: 0,\n expiresAt: created.expiresAt ?? expiresAt.toISOString(),\n }, now())\n return { key: secret, usage, refreshed: true }\n }\n\n async function ensureKey(identityInput: WorkspaceKeyIdentity, keyOptions?: { budgetUsd?: number }): Promise<WorkspaceRuntimeKey> {\n const identity = normalizeIdentity(identityInput)\n const budgetUsd = keyOptions?.budgetUsd ?? defaultBudgetUsd\n requirePositive(budgetUsd, 'budgetUsd')\n const scope = scopeKey(scopeFor(identity, product))\n let mintedRemote: { row: DurableWorkspaceKeyProvisioningRecord; remoteId: string } | null = null\n return withLocalLock(scope, () => withDurableLease(scope, async () => {\n const initialCleanup = await retryPendingRevocationsUnlocked(identity, identity)\n if (initialCleanup.pending > 0) throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n await reconcileProvisioning(identity)\n const active = await readActive(identity)\n if (active) {\n const key = await options.crypto.decrypt(active.row.keyEncrypted)\n if (!key.trim()) throw new Error('workspace child key secret is empty')\n return { key, usage: active.usage, refreshed: false }\n }\n const finalCleanup = await retryPendingRevocationsUnlocked(identity, identity)\n if (finalCleanup.pending > 0) throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n await assertNoPendingRevocations(identity)\n return mint(identity, budgetUsd, (row, remoteId) => {\n mintedRemote = { row, remoteId }\n })\n }, async () => {\n const remote = mintedRemote\n if (!remote) return\n const cleaned = await revokePending({\n ...remote.row,\n keyId: remote.remoteId,\n status: 'revocation_pending',\n }, identity)\n if (!cleaned) throw new Error('lease-loss child-key cleanup is pending')\n }))\n }\n\n async function getUsage(identityInput: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null> {\n const identity = normalizeIdentity(identityInput)\n const scope = scopeKey(scopeFor(identity, product))\n return withLocalLock(scope, () => withDurableLease(scope, async () => {\n const active = await readActive(identity)\n return active?.usage ?? null\n }))\n }\n\n return { ensureKey, getUsage, retryPendingRevocations }\n}\n","import { assertWorkspaceKeyCryptoUsable } from './crypto-probe'\n\n/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations the key manager needs. Wire it from the\n * platform via {@link createTcloudKeyProvisioner} rather than casting. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/**\n * The subset of the `@tangle-network/tcloud` `TCloudClient` the provisioner uses\n * — declared with METHOD syntax so the real client (whose `product` is a narrow\n * union and whose budgets are `number | null`) is assignable bivariantly. The\n * real SDK client satisfies this; pass it straight in.\n */\nexport interface TcloudKeyClient {\n createKey(opts: {\n name: string\n product?: string\n budgetUsd?: number\n expiresAt?: string\n parentKeyId?: string\n allowedModels?: string[]\n rpmLimit?: number\n }): Promise<{ id: string; key: string }>\n getKey(id: string): Promise<{ budgetUsd?: number | null; budgetSpent?: number; expiresAt?: string | null }>\n revokeKey(id: string): Promise<unknown>\n}\n\n/**\n * Adapt the tcloud SDK client to {@link KeyProvisioner} — the typed seam that\n * replaces the `as unknown as KeyProvisioner` cast every consumer otherwise\n * repeats. The platform already exposes child-key minting (parent→child key,\n * per-key USD budget, expiry); this maps its shapes (`product` union,\n * `number | null` budgets) onto the manager's contract (`null → undefined`).\n */\nexport function createTcloudKeyProvisioner(client: TcloudKeyClient): KeyProvisioner {\n return {\n createKey: async (input) => {\n const created = await client.createKey(input)\n return { id: created.id, key: created.key }\n },\n revokeKey: (keyId) => client.revokeKey(keyId),\n getKey: async (keyId) => {\n const info = await client.getKey(keyId)\n return {\n budgetUsd: info.budgetUsd ?? undefined,\n budgetSpent: info.budgetSpent ?? undefined,\n expiresAt: info.expiresAt ?? null,\n }\n },\n }\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\n/** Define configuration options for managing workspace keys including provisioning, storage, and cryptography */\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\n/** Describe usage and budget details for a workspace model key including expiration and exhaustion status */\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\n/** Manage workspace keys by ensuring, rotating, and tracking usage of active child-key secrets */\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\n// ---------------------------------------------------------------------------\n// Shared-platform-balance billing\n//\n// A DIFFERENT model from the per-workspace child-key manager above: here every\n// user runs against a SHARED platform balance (id.tangle.tools), keyed by the\n// user's platform identity. The app owns no key minting — it reads the balance,\n// gates a billable turn, and deducts spend through the platform billing API.\n// Plan limits, the platform transport, and identity resolution are SEAMS the\n// product supplies; this module imports no DB and no HTTP client.\n// ---------------------------------------------------------------------------\n\n/** A user's resolved platform identity (from the app's SSO account store). */\nexport interface PlatformIdentity {\n platformUserId: string\n /** The user's per-user platform API key (reads), or null when unlinked. */\n apiKey: string | null\n}\n\n/** Spendable balance for a platform user. */\nexport interface PlatformBalanceInfo {\n balance: number\n lifetimeSpent: number\n}\n\n/** Per-product spend aggregate. */\nexport interface PlatformProductUsage {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Plan limits — a PARAMETER per product (dollar allowance, concurrency,\n * overage policy). Never baked into the framework. */\nexport interface PlanLimit {\n monthlyBalanceUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * The platform billing transport — the product wires these to id.tangle.tools\n * (or any balance backend). Reads authenticate as the user (their `apiKey`);\n * the deduct write is a service-token call naming the target user. This module\n * never touches HTTP — it only sequences these calls.\n */\nexport interface PlatformBillingClient<Plan extends string> {\n /** Resolve the user's platform identity, or null when there is no SSO account. */\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n /** Subscription plan for the user (via their platform key). */\n getPlan(apiKey: string): Promise<Plan>\n /** Spendable balance for the user (via their platform key). */\n getBalance(apiKey: string): Promise<PlatformBalanceInfo>\n /** Per-product usage rows for the user (via their platform key). */\n getUsageByProduct(apiKey: string): Promise<PlatformProductUsage[]>\n /** Deduct spend against the user's balance (service-token write). */\n deduct(input: { platformUserId: string; amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n}\n\n/** Define shared billing state including user ID, plan, balances, concurrency, and overage permission */\nexport interface SharedBillingState<Plan extends string> {\n /** Platform user id, or null when the user has no Tangle SSO account. */\n platformUserId: string | null\n plan: Plan\n monthlyBalanceUsd: number\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define configuration options for managing platform balance based on billing plans */\nexport interface PlatformBalanceManagerOptions<Plan extends string> {\n client: PlatformBillingClient<Plan>\n /** Plan → limits map (the product's pricing). */\n planLimits: Record<Plan, PlanLimit>\n /** The plan an unlinked / outage user falls to (fails CLOSED). */\n freePlan: Plan\n /** The product slug to attribute usage to (for `getProductUsage`). */\n productSlug: string\n}\n\n/** Manage user plans and balances including state retrieval, billing authorization, deduction, and usage tracking */\nexport interface PlatformBalanceManager<Plan extends string> {\n /** Resolve the user's plan + balance. Unlinked or platform-outage users fail\n * CLOSED: free plan, zero remaining balance — a billable run is never started\n * against an unknown balance. */\n getState(userId: string): Promise<SharedBillingState<Plan>>\n /** Gate a billable turn: allowed when the plan permits overage or remaining\n * balance is positive. Returns the state so the caller deducts against it. */\n canStartBillableTurn(userId: string): Promise<{ allowed: boolean; state: SharedBillingState<Plan> }>\n /** Deduct `amountUsd` against the user's platform balance. Throws when the\n * user is not platform-linked. */\n deduct(userId: string, params: { amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n /** This product's spend for the user (drives a usage panel). */\n getProductUsage(userId: string): Promise<{ spentUsd: number; transactionCount: number }>\n}\n\n/** Create a platform balance manager to handle user plan limits and state based on provided options */\nexport function createPlatformBalanceManager<Plan extends string>(\n opts: PlatformBalanceManagerOptions<Plan>,\n): PlatformBalanceManager<Plan> {\n const { client, planLimits, freePlan, productSlug } = opts\n\n const getState: PlatformBalanceManager<Plan>['getState'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n // No SSO account, or linked without a platform key: unlinked free tier with\n // zero balance. Reads require the user's key — never call them empty.\n if (!identity || !identity.apiKey) {\n const limits = planLimits[freePlan]\n return {\n platformUserId: identity?.platformUserId ?? null,\n plan: freePlan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n const [plan, balance] = await Promise.all([client.getPlan(identity.apiKey), client.getBalance(identity.apiKey)])\n const limits = planLimits[plan]\n return {\n platformUserId: identity.platformUserId,\n plan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n\n const canStartBillableTurn: PlatformBalanceManager<Plan>['canStartBillableTurn'] = async (userId) => {\n const state = await getState(userId)\n if (!state.platformUserId) return { allowed: false, state }\n const allowed = state.overageAllowed || state.remainingBalanceUsd > 0\n return { allowed, state }\n }\n\n const deduct: PlatformBalanceManager<Plan>['deduct'] = async (userId, params) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity) throw new Error('Shared billing requires a platform-linked user')\n await client.deduct({\n platformUserId: identity.platformUserId,\n amountUsd: params.amountUsd,\n type: params.type,\n description: params.description,\n referenceId: params.referenceId,\n })\n }\n\n const getProductUsage: PlatformBalanceManager<Plan>['getProductUsage'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity?.apiKey) return { spentUsd: 0, transactionCount: 0 }\n const rows = await client.getUsageByProduct(identity.apiKey)\n const product = rows.find((row) => row.product === productSlug)\n return { spentUsd: product?.totalSpent ?? 0, transactionCount: product?.count ?? 0 }\n }\n\n return { getState, canStartBillableTurn, deduct, getProductUsage }\n}\n\n/** Create a workspace key manager that handles key provisioning and budget tracking */\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n // Prove the at-rest crypto seam is usable BEFORE spending a platform mint.\n // The `KeyCrypto` impl may close over an env-resolved key (createFieldCrypto)\n // that is empty/invalid and only throws at encrypt time — after the mint —\n // orphaning the child key. A round-trip probe makes a misconfigured consumer\n // fail loud with ZERO platform spend.\n await assertWorkspaceKeyCryptoUsable(opts.crypto)\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const mintedKeyId = created.id\n\n // Past this point the platform has minted a budget-bearing child key. ANY\n // failure before the row is persisted (encrypt, insert) MUST revoke the\n // just-minted key, else it leaks as a zombie holding parent-key headroom —\n // a missing/invalid encryption key would otherwise burn the child budget\n // per mint attempt until the parent can no longer mint (fleet-wide outage).\n let keyEncrypted: string\n let priors: Array<{ id: string; keyId: string }>\n try {\n keyEncrypted = await opts.crypto.encrypt(created.key)\n priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: mintedKeyId, keyEncrypted, budgetUsd, expiresAt })\n } catch (err) {\n // Best-effort revoke of the orphan. A revoke failure must NOT mask the\n // original cause — log it loudly and rethrow the original error.\n try {\n await opts.provisioner.revokeKey(mintedKeyId)\n } catch (revokeErr) {\n console.error(\n `[workspace-key-manager] FAILED to revoke orphaned child key ${mintedKeyId} ` +\n `for workspace ${workspaceId} after a post-mint failure — it now leaks parent-key budget. ` +\n `Revoke error:`,\n revokeErr,\n )\n }\n throw err\n }\n\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n\nexport {\n createIdentityBoundWorkspaceKeyManager,\n type DurableWorkspaceKeyManager,\n type DurableWorkspaceKeyManagerOptions,\n type DurableWorkspaceKeyCreateInput,\n type DurableWorkspaceKeyConditionalWrites,\n type DurableWorkspaceKeyProvisioner,\n type DurableWorkspaceKeyProvisioningRecord,\n type DurableWorkspaceKeyRecord,\n type DurableWorkspaceKeyScope,\n type DurableWorkspaceKeyStore,\n type WorkspaceKeyIdentity,\n type WorkspaceKeyProduct,\n type WorkspaceKeyStatus,\n type WorkspaceKeyUsage,\n type WorkspaceRuntimeKey,\n} from './identity-bound'\n"],"mappings":"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","names":["sourceKeyId","operationId","limits"]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-TA5Q4I2K.js";
4
4
  import {
5
5
  createWorkspaceKeyManager
6
- } from "../chunk-TS2ZIB53.js";
6
+ } from "../chunk-4ZJYNI53.js";
7
7
 
8
8
  // src/preset-cloudflare/workflow-instance.ts
9
9
  async function ensureCloudflareWorkflowInstance(binding, input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.47.6",
3
+ "version": "0.47.7",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/billing/crypto-probe.ts","../src/billing/identity-bound.ts","../src/billing/index.ts"],"sourcesContent":["import type { KeyCrypto } from './index'\n\n/** Prove the at-rest crypto seam before a remote key is minted. */\nexport async function assertWorkspaceKeyCryptoUsable(crypto: KeyCrypto): Promise<void> {\n const probe = 'agent-app:key-manager:crypto-probe'\n let roundTripped: string\n try {\n roundTripped = await crypto.decrypt(await crypto.encrypt(probe))\n } catch (error) {\n throw new Error(\n 'Key encryption is misconfigured: the crypto seam threw before minting. ' +\n 'Validate FIELD_ENCRYPTION_KEY (64-char hex) at startup. No platform key was minted.',\n { cause: error },\n )\n }\n if (roundTripped !== probe) {\n throw new Error(\n 'Key encryption is misconfigured: encrypt/decrypt round-trip did not preserve the plaintext. ' +\n 'No platform key was minted.',\n )\n }\n}\n","import { assertWorkspaceKeyCryptoUsable } from './crypto-probe'\nimport type { KeyCrypto, KeyProvisioner } from './index'\n\n/** Lifecycle state for a durable delegated key row. */\nexport type WorkspaceKeyStatus =\n | 'legacy'\n | 'provisioning'\n | 'active'\n | 'revocation_pending'\n | 'revoked'\n | 'orphaned'\n\n/** Product partition used by the key store and remote provisioner. */\nexport type WorkspaceKeyProduct = string\n\n/** The identity fields that bind a child key to its paying owner and source. */\nexport interface WorkspaceKeyIdentity {\n workspaceId: string\n ownerUserId: string\n platformUserId: string\n sourceKeyId?: string | null\n /** A non-secret digest of the source credential. */\n sourceKeyFingerprint: string\n}\n\n/** A product/workspace/owner lookup scope. */\nexport interface DurableWorkspaceKeyScope {\n workspaceId: string\n ownerUserId: string\n product: WorkspaceKeyProduct\n}\n\n/** A persisted child-key row owned by this manager. */\nexport interface DurableWorkspaceKeyRecord {\n id: string\n workspaceId: string\n ownerUserId: string\n product: WorkspaceKeyProduct\n platformUserId: string\n sourceKeyId: string | null\n sourceKeyFingerprint: string\n /** The persisted name used to recover a remote create after a crash. Null only for pre-name rows. */\n name: string | null\n keyId: string\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date\n status: WorkspaceKeyStatus\n revocationAttempts: number\n nextRevocationAt: Date | null\n lastRevocationError: string | null\n createdAt: Date\n}\n\n/** A row that has been written before its remote child key exists. */\nexport interface DurableWorkspaceKeyProvisioningRecord extends DurableWorkspaceKeyRecord {\n status: 'provisioning'\n}\n\ntype DurableWorkspaceKeyCreateResult = Awaited<ReturnType<KeyProvisioner['createKey']>> & {\n budgetUsd?: number | null\n budgetRemaining?: number\n expiresAt?: string | null\n}\n\n/** The remote operations required by the durable manager. */\nexport interface DurableWorkspaceKeyProvisioner {\n createKey(input: Parameters<KeyProvisioner['createKey']>[0]): Promise<DurableWorkspaceKeyCreateResult>\n getKey(id: string): Promise<{\n budgetUsd?: number | null\n budgetSpent?: number\n expiresAt?: string | null\n }>\n revokeKey: KeyProvisioner['revokeKey']\n /** Find remote keys for a specific persisted provisioning attempt. */\n findCreatedKeys(input: {\n name: string\n product: string\n sourceKeyId?: string | null\n }): Promise<Array<{ id: string }>>\n}\n\n/** Persistence operations for identity-bound key lifecycle state. */\nexport interface DurableWorkspaceKeyStore {\n /** Return the active row for this exact product scope. */\n getActive(scope: DurableWorkspaceKeyScope): Promise<DurableWorkspaceKeyRecord | null>\n /** Return every unfinished provisioning row for this exact product scope. */\n listProvisioning(scope: DurableWorkspaceKeyScope): Promise<DurableWorkspaceKeyProvisioningRecord[]>\n /** Insert before the remote create so a crashed create can be recovered. */\n insertProvisioning(record: DurableWorkspaceKeyProvisioningRecord): Promise<void>\n /** Save the remote id while the row remains in provisioning state. */\n markProvisioningRemote(input: { id: string; keyId: string }): Promise<void>\n /** Promote a fully encrypted row to active state. */\n markActive(input: {\n id: string\n keyId: string\n keyEncrypted: string\n expiresAt: Date\n budgetUsd: number\n }): Promise<void>\n /** Keep a failed cleanup visible and schedule a later retry. */\n markRevocationPending(input: {\n id: string\n error?: string | null\n nextAttemptAt: Date\n incrementAttempts: boolean\n }): Promise<void>\n /** Mark a remote key as revoked. Missing remote keys count as revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n /** Mark a provisioning row terminal when no remote key remains to clean. */\n markOrphaned(id: string, error: string): Promise<void>\n /** Query due cleanup rows. The implementation should filter by the supplied scope. */\n listPendingRevocations(input: {\n product: WorkspaceKeyProduct\n workspaceId?: string\n ownerUserId?: string\n now: Date\n limit: number\n /** Include rows scheduled for a later retry when checking before issuance. */\n includeFuture?: boolean\n }): Promise<DurableWorkspaceKeyRecord[]>\n /** Acquire a cross-process lease for one product/workspace/owner scope. */\n acquireLease(scope: string, operationId: string, now: Date, leaseMs: number): Promise<boolean>\n /** Renew an acquired lease before its expiry. */\n renewLease(scope: string, operationId: string, now: Date, leaseMs: number): Promise<boolean>\n /** Release an acquired lease. */\n releaseLease(scope: string, operationId: string): Promise<void>\n}\n\n/** Live budget information for a child key. */\nexport interface WorkspaceKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string\n exhausted: boolean\n}\n\n/** The decrypted key plus the usage snapshot used to issue it. */\nexport interface WorkspaceRuntimeKey {\n key: string\n usage: WorkspaceKeyUsage\n /** True when this call minted a new remote key. */\n refreshed: boolean\n}\n\n/** Identity-bound durable key manager API. */\nexport interface DurableWorkspaceKeyManager {\n /** Reuse a matching active key or reconcile and mint one under a lease. */\n ensureKey(identity: WorkspaceKeyIdentity, options?: { budgetUsd?: number }): Promise<WorkspaceRuntimeKey>\n /** Read usage without returning the child secret. */\n getUsage(identity: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null>\n /** Retry due cleanup rows. A supplied owner/workspace limits the retry to its scope. */\n retryPendingRevocations(scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>): Promise<number>\n}\n\n/** Configuration for {@link createIdentityBoundWorkspaceKeyManager}. */\nexport interface DurableWorkspaceKeyManagerOptions {\n store: DurableWorkspaceKeyStore\n provisioner: DurableWorkspaceKeyProvisioner\n crypto: KeyCrypto\n /** Product partition. Products must use separate values. */\n product: WorkspaceKeyProduct\n defaultBudgetUsd: number\n now?: () => Date\n /** Local lifetime used when the remote response omits an expiry. */\n keyLifetimeMs?: number\n /** Lease lifetime for remote operations. */\n leaseMs?: number\n /** Maximum time to wait for another issuer to release the scope lease. */\n leaseWaitMs?: number\n /** Poll delay while waiting for a scope lease. */\n leasePollMs?: number\n /** Renewal interval. It must be shorter than `leaseMs`. */\n leaseRenewIntervalMs?: number\n /** Initial delay for a failed remote revoke. */\n revocationRetryBaseMs?: number\n /** Maximum delay for a failed remote revoke. */\n revocationRetryMaxMs?: number\n /** Stable remote name. The operation id makes the default crash-safe. */\n nameForIdentity?: (identity: WorkspaceKeyIdentity, operationId: string) => string\n /** Resolve names for rows written before `name` became durable. Null blocks issuance safely. */\n legacyNameForRecord?: (record: DurableWorkspaceKeyRecord) => string | null | undefined\n /** Recognize a missing remote key without depending on a provider SDK. */\n isRemoteMissing?: (error: unknown) => boolean\n}\n\nconst DEFAULT_KEY_LIFETIME_MS = 55 * 60_000\nconst DEFAULT_LEASE_MS = 60_000\nconst DEFAULT_LEASE_WAIT_MS = 15_000\nconst DEFAULT_LEASE_POLL_MS = 50\nconst DEFAULT_LEASE_RENEW_INTERVAL_MS = Math.floor(DEFAULT_LEASE_MS / 3)\nconst DEFAULT_REVOCATION_RETRY_BASE_MS = 2_000\nconst DEFAULT_REVOCATION_RETRY_MAX_MS = 60_000\nconst PENDING_REVOCATION_BATCH_SIZE = 20\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction operationId(): string {\n return globalThis.crypto.randomUUID()\n}\n\nfunction requirePositive(value: number, label: string): number {\n if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number`)\n return value\n}\n\nfunction requireNonNegative(value: number, label: string): number {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a non-negative finite number`)\n return value\n}\n\nfunction normalizeIdentity(identity: WorkspaceKeyIdentity): WorkspaceKeyIdentity {\n const workspaceId = identity.workspaceId.trim()\n const ownerUserId = identity.ownerUserId.trim()\n const platformUserId = identity.platformUserId.trim()\n const sourceKeyFingerprint = identity.sourceKeyFingerprint.trim()\n const sourceKeyId = identity.sourceKeyId?.trim() || null\n if (!workspaceId || !ownerUserId || !platformUserId || !sourceKeyFingerprint) {\n throw new Error('workspace child key identity is incomplete')\n }\n return { workspaceId, ownerUserId, platformUserId, sourceKeyId, sourceKeyFingerprint }\n}\n\nfunction scopeFor(identity: Pick<WorkspaceKeyIdentity, 'workspaceId' | 'ownerUserId'>, product: string): DurableWorkspaceKeyScope {\n return { workspaceId: identity.workspaceId, ownerUserId: identity.ownerUserId, product }\n}\n\nfunction scopeKey(scope: DurableWorkspaceKeyScope): string {\n return JSON.stringify([scope.product, scope.workspaceId, scope.ownerUserId])\n}\n\nfunction safeNamePart(value: string): string {\n const normalized = value.replace(/[^A-Za-z0-9._-]/g, '-').replace(/-+/g, '-')\n return normalized.slice(0, 80) || 'unknown'\n}\n\nfunction defaultKeyName(identity: WorkspaceKeyIdentity, product: string, operationId: string): string {\n return `agent-app:${safeNamePart(product)}:${safeNamePart(identity.workspaceId)}:${safeNamePart(identity.platformUserId)}:${operationId}`\n}\n\nfunction sourceKeyId(identity: WorkspaceKeyIdentity): string | null {\n return identity.sourceKeyId?.trim() || null\n}\n\nfunction sameIdentity(row: DurableWorkspaceKeyRecord, identity: WorkspaceKeyIdentity, product: string): boolean {\n return row.workspaceId === identity.workspaceId\n && row.ownerUserId === identity.ownerUserId\n && row.product === product\n && row.platformUserId === identity.platformUserId\n && row.sourceKeyId === sourceKeyId(identity)\n && row.sourceKeyFingerprint === identity.sourceKeyFingerprint\n}\n\nfunction sameRowIdentity(a: DurableWorkspaceKeyRecord, b: DurableWorkspaceKeyRecord): boolean {\n return a.workspaceId === b.workspaceId\n && a.ownerUserId === b.ownerUserId\n && a.product === b.product\n && a.platformUserId === b.platformUserId\n && a.sourceKeyId === b.sourceKeyId\n && a.sourceKeyFingerprint === b.sourceKeyFingerprint\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return (error instanceof Error ? error.message : fallback)\n .replace(/Bearer\\s+[^\\s,;]+/gi, 'Bearer [redacted]')\n .replace(/((?:authorization|api[-_ ]?key|token|secret|password)\\s*[:=]\\s*)(?!Bearer\\b)[^\\s,;]+/gi, '$1[redacted]')\n .slice(0, 240)\n}\n\nfunction isProvisioningId(value: string): boolean {\n return value.startsWith('provisioning:')\n}\n\nfunction usageFromRemote(\n row: DurableWorkspaceKeyRecord,\n remote: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['getKey']>>,\n now: Date,\n): WorkspaceKeyUsage {\n const budgetUsd = remote.budgetUsd ?? row.budgetUsd\n const budgetSpent = remote.budgetSpent ?? 0\n if (!Number.isFinite(budgetUsd) || budgetUsd < 0) throw new Error('workspace child key budget is malformed')\n if (!Number.isFinite(budgetSpent) || budgetSpent < 0) throw new Error('workspace child key usage is malformed')\n const expiresAt = remote.expiresAt ?? row.expiresAt.toISOString()\n const expiryMs = Date.parse(expiresAt)\n if (!Number.isFinite(expiryMs)) throw new Error('workspace child key expiry is malformed')\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: row.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt,\n exhausted: budgetRemaining <= 0 || expiryMs <= now.getTime(),\n }\n}\n\nfunction defaultRemoteMissing(error: unknown): boolean {\n const value = error as { status?: unknown; code?: unknown } | null\n return value?.status === 404 || value?.code === 'not_found'\n}\n\n/**\n * Create a product-neutral manager for child-key issuance and cleanup.\n *\n * The provisioner must already be authenticated for the current source\n * credential. The manager persists only the source id and its non-secret\n * fingerprint, then refuses to reuse a row from another identity.\n */\nexport function createIdentityBoundWorkspaceKeyManager(\n options: DurableWorkspaceKeyManagerOptions,\n): DurableWorkspaceKeyManager {\n const product = options.product.trim()\n if (!product) throw new Error('workspace child key product is required')\n const defaultBudgetUsd = requirePositive(options.defaultBudgetUsd, 'defaultBudgetUsd')\n const keyLifetimeMs = requirePositive(options.keyLifetimeMs ?? DEFAULT_KEY_LIFETIME_MS, 'keyLifetimeMs')\n const leaseMs = requirePositive(options.leaseMs ?? DEFAULT_LEASE_MS, 'leaseMs')\n const leaseWaitMs = requireNonNegative(options.leaseWaitMs ?? DEFAULT_LEASE_WAIT_MS, 'leaseWaitMs')\n const leasePollMs = requirePositive(options.leasePollMs ?? DEFAULT_LEASE_POLL_MS, 'leasePollMs')\n const leaseRenewIntervalMs = requirePositive(\n options.leaseRenewIntervalMs ?? Math.min(DEFAULT_LEASE_RENEW_INTERVAL_MS, Math.floor(leaseMs / 3)),\n 'leaseRenewIntervalMs',\n )\n if (leaseRenewIntervalMs >= leaseMs) throw new Error('leaseRenewIntervalMs must be shorter than leaseMs')\n const revocationRetryBaseMs = requirePositive(\n options.revocationRetryBaseMs ?? DEFAULT_REVOCATION_RETRY_BASE_MS,\n 'revocationRetryBaseMs',\n )\n const revocationRetryMaxMs = requirePositive(\n options.revocationRetryMaxMs ?? DEFAULT_REVOCATION_RETRY_MAX_MS,\n 'revocationRetryMaxMs',\n )\n if (revocationRetryMaxMs < revocationRetryBaseMs) throw new Error('revocationRetryMaxMs must cover the base delay')\n\n const now = options.now ?? (() => new Date())\n const isRemoteMissing = options.isRemoteMissing ?? defaultRemoteMissing\n const localLocks = new Map<string, Promise<void>>()\n\n async function withLocalLock<T>(scope: string, work: () => Promise<T>): Promise<T> {\n const previous = localLocks.get(scope) ?? Promise.resolve()\n let release!: () => void\n const current = new Promise<void>((resolve) => { release = resolve })\n localLocks.set(scope, current)\n await previous\n try {\n return await work()\n } finally {\n release()\n if (localLocks.get(scope) === current) localLocks.delete(scope)\n }\n }\n\n async function withDurableLease<T>(\n scope: string,\n work: () => Promise<T>,\n onLeaseLost?: () => Promise<void>,\n ): Promise<T> {\n const leaseOperationId = operationId()\n const startedAt = Date.now()\n let acquired = false\n while (!acquired && Date.now() - startedAt <= leaseWaitMs) {\n acquired = await options.store.acquireLease(scope, leaseOperationId, now(), leaseMs)\n if (!acquired) await sleep(leasePollMs)\n }\n if (!acquired) throw new Error('workspace child-key issuance is busy; retry the request')\n\n let stopped = false\n let leaseLost: Error | null = null\n let leaseTimer: ReturnType<typeof setTimeout> | undefined\n let renewalPromise: Promise<void> | undefined\n const scheduleRenewal = () => {\n if (stopped || leaseLost) return\n leaseTimer = setTimeout(() => {\n leaseTimer = undefined\n const renewal = (async () => {\n try {\n if (!await options.store.renewLease(scope, leaseOperationId, now(), leaseMs)) {\n leaseLost = new Error('workspace child-key lease was lost during reconciliation')\n return\n }\n scheduleRenewal()\n } catch (error) {\n leaseLost = error instanceof Error ? error : new Error('workspace child-key lease renewal failed')\n }\n })()\n renewalPromise = renewal\n void renewal.finally(() => {\n if (renewalPromise === renewal) renewalPromise = undefined\n }).catch(() => undefined)\n }, leaseRenewIntervalMs)\n }\n scheduleRenewal()\n\n let workError: unknown\n let leaseLossCleanupError: unknown\n try {\n const result = await work()\n if (leaseLost) throw leaseLost\n return result\n } catch (error) {\n workError = error\n throw error\n } finally {\n stopped = true\n if (leaseTimer) clearTimeout(leaseTimer)\n if (renewalPromise) await renewalPromise\n if (leaseLost && onLeaseLost) {\n try {\n await onLeaseLost()\n } catch (error) {\n leaseLossCleanupError = error\n }\n }\n try {\n await options.store.releaseLease(scope, leaseOperationId)\n } catch (error) {\n if (workError === undefined) throw error\n }\n if (workError === undefined && leaseLossCleanupError) {\n throw new Error('workspace child-key lease was lost and cleanup failed', { cause: leaseLossCleanupError })\n }\n if (workError === undefined && leaseLost) throw leaseLost\n }\n }\n\n function retryDelay(attempts: number): number {\n return Math.min(revocationRetryMaxMs, revocationRetryBaseMs * 2 ** Math.min(Math.max(attempts - 1, 0), 5))\n }\n\n async function markPending(id: string, error: unknown, incrementAttempts: boolean): Promise<void> {\n const delay = incrementAttempts ? retryDelay(1) : 0\n await options.store.markRevocationPending({\n id,\n error: errorMessage(error, 'remote key cleanup failed'),\n nextAttemptAt: new Date(now().getTime() + delay),\n incrementAttempts,\n })\n }\n\n async function revokePending(row: DurableWorkspaceKeyRecord, identity?: WorkspaceKeyIdentity): Promise<boolean> {\n if (!row.keyId || isProvisioningId(row.keyId)) return cleanupProvisioning(row, identity)\n const active = await options.store.getActive({\n workspaceId: row.workspaceId,\n ownerUserId: row.ownerUserId,\n product: row.product,\n })\n if (active && active.id !== row.id && active.keyId === row.keyId) {\n await options.store.markRevoked(row.id, now())\n return true\n }\n try {\n await options.provisioner.revokeKey(row.keyId)\n await options.store.markRevoked(row.id, now())\n return true\n } catch (error) {\n if (isRemoteMissing(error)) {\n await options.store.markRevoked(row.id, now())\n return true\n }\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote key revocation failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n }\n\n async function cleanupProvisioning(row: DurableWorkspaceKeyRecord, identity?: WorkspaceKeyIdentity): Promise<boolean> {\n let candidates: Array<{ id: string }>\n if (row.keyId && !isProvisioningId(row.keyId)) {\n candidates = [{ id: row.keyId }]\n } else {\n const name = row.name?.trim() || options.legacyNameForRecord?.(row)?.trim()\n if (!name) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'provisioning row has no recoverable remote name; migrate the row or configure legacyNameForRecord',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return false\n }\n candidates = await options.provisioner.findCreatedKeys({\n name,\n product: row.product,\n sourceKeyId: row.sourceKeyId,\n })\n }\n\n const scope = { workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product: row.product }\n const active = await options.store.getActive(scope)\n const activeMatches = active && (identity ? sameIdentity(active, identity, product) : sameRowIdentity(active, row))\n const activeKeyId = activeMatches ? active.keyId : null\n const toRevoke = candidates\n .map((candidate) => candidate.id.trim())\n .filter((candidateId) => candidateId && candidateId !== activeKeyId)\n\n for (const keyId of toRevoke) {\n try {\n await options.provisioner.revokeKey(keyId)\n } catch (error) {\n if (isRemoteMissing(error)) continue\n await options.store.markRevocationPending({\n id: row.id,\n error: errorMessage(error, 'remote provisioning cleanup failed'),\n nextAttemptAt: new Date(now().getTime() + retryDelay(row.revocationAttempts + 1)),\n incrementAttempts: true,\n })\n return false\n }\n }\n\n if (toRevoke.length > 0 || candidates.length === 0 || activeKeyId !== null) {\n await options.store.markRevoked(row.id, now())\n } else {\n await options.store.markOrphaned(row.id, 'no unowned remote child matched the durable provisioning record')\n }\n return true\n }\n\n async function reconcileProvisioning(identity: WorkspaceKeyIdentity): Promise<void> {\n const scope = scopeFor(identity, product)\n const rows = await options.store.listProvisioning(scope)\n for (const row of rows) {\n if (!(await cleanupProvisioning(row, identity))) {\n throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n }\n }\n }\n\n async function retryPendingRevocationsUnlocked(\n scopeFilter?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>,\n activeIdentity?: WorkspaceKeyIdentity,\n ): Promise<{ completed: number; pending: number }> {\n const rows = await options.store.listPendingRevocations({\n product,\n workspaceId: scopeFilter?.workspaceId,\n ownerUserId: scopeFilter?.ownerUserId,\n now: now(),\n limit: PENDING_REVOCATION_BATCH_SIZE,\n })\n let completed = 0\n let pending = 0\n for (const row of rows) {\n if (row.product !== product) continue\n if (scopeFilter && (row.workspaceId !== scopeFilter.workspaceId || row.ownerUserId !== scopeFilter.ownerUserId)) continue\n if (await revokePending(row, activeIdentity)) completed += 1\n else pending += 1\n }\n return { completed, pending }\n }\n\n async function assertNoPendingRevocations(identity: WorkspaceKeyIdentity): Promise<void> {\n const rows = await options.store.listPendingRevocations({\n product,\n workspaceId: identity.workspaceId,\n ownerUserId: identity.ownerUserId,\n now: now(),\n limit: PENDING_REVOCATION_BATCH_SIZE,\n includeFuture: true,\n })\n if (rows.some((row) => row.product === product\n && row.workspaceId === identity.workspaceId\n && row.ownerUserId === identity.ownerUserId)) {\n throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n }\n }\n\n async function retryPendingRevocations(scopeInput?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>): Promise<number> {\n if (scopeInput) {\n const workspaceId = scopeInput.workspaceId.trim()\n const ownerUserId = scopeInput.ownerUserId.trim()\n if (!workspaceId || !ownerUserId) throw new Error('workspace child key retry scope is incomplete')\n const scope = scopeKey({ workspaceId, ownerUserId, product })\n return withLocalLock(scope, () => withDurableLease(scope, async () => (\n await retryPendingRevocationsUnlocked({ workspaceId, ownerUserId })).completed))\n }\n\n const rows = await options.store.listPendingRevocations({ product, now: now(), limit: PENDING_REVOCATION_BATCH_SIZE })\n let completed = 0\n const scopes = new Set<string>()\n for (const row of rows) {\n if (row.product !== product) continue\n const scope = scopeKey({ workspaceId: row.workspaceId, ownerUserId: row.ownerUserId, product })\n if (scopes.has(scope)) continue\n scopes.add(scope)\n completed += await withLocalLock(scope, () => withDurableLease(scope, async () => (await retryPendingRevocationsUnlocked({\n workspaceId: row.workspaceId,\n ownerUserId: row.ownerUserId,\n })).completed))\n }\n return completed\n }\n\n async function readActive(identity: WorkspaceKeyIdentity): Promise<{ row: DurableWorkspaceKeyRecord; usage: WorkspaceKeyUsage } | null> {\n const row = await options.store.getActive(scopeFor(identity, product))\n if (!row) return null\n if (!sameIdentity(row, identity, product)) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'active child key identity does not match the authenticated owner and source',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return null\n }\n\n let remote: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['getKey']>>\n try {\n remote = await options.provisioner.getKey(row.keyId)\n } catch (error) {\n if (isRemoteMissing(error)) {\n await options.store.markRevoked(row.id, now())\n return null\n }\n throw error\n }\n const usage = usageFromRemote(row, remote, now())\n if (usage.exhausted) {\n await options.store.markRevocationPending({\n id: row.id,\n error: 'active child key is expired or exhausted',\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n return null\n }\n return { row, usage }\n }\n\n async function mint(\n identity: WorkspaceKeyIdentity,\n budgetUsd: number,\n onRemoteKey?: (row: DurableWorkspaceKeyProvisioningRecord, remoteId: string) => void,\n ): Promise<WorkspaceRuntimeKey> {\n await assertWorkspaceKeyCryptoUsable(options.crypto)\n const mintOperationId = operationId()\n const createdAt = now()\n const expiresAt = new Date(createdAt.getTime() + keyLifetimeMs)\n const name = (options.nameForIdentity?.(identity, mintOperationId) ?? defaultKeyName(identity, product, mintOperationId)).trim()\n if (!name) throw new Error('workspace child key remote name is required')\n const rowId = operationId()\n const provisioningRow: DurableWorkspaceKeyProvisioningRecord = {\n id: rowId,\n workspaceId: identity.workspaceId,\n ownerUserId: identity.ownerUserId,\n product,\n platformUserId: identity.platformUserId,\n sourceKeyId: sourceKeyId(identity),\n sourceKeyFingerprint: identity.sourceKeyFingerprint,\n name,\n keyId: `provisioning:${mintOperationId}`,\n keyEncrypted: '',\n budgetUsd,\n expiresAt,\n status: 'provisioning',\n revocationAttempts: 0,\n nextRevocationAt: null,\n lastRevocationError: null,\n createdAt,\n }\n await options.store.insertProvisioning(provisioningRow)\n\n let created: Awaited<ReturnType<DurableWorkspaceKeyProvisioner['createKey']>>\n try {\n created = await options.provisioner.createKey({\n name,\n product,\n budgetUsd,\n expiresAt: expiresAt.toISOString(),\n })\n } catch (error) {\n // Keep cleanup running even when the state write fails. The remote\n // request may have committed before its response failed.\n try {\n await markPending(rowId, error, false)\n } catch {\n // cleanupProvisioning below can still find the remote name.\n }\n try {\n await cleanupProvisioning({ ...provisioningRow, status: 'revocation_pending' }, identity)\n } catch (cleanupError) {\n throw new Error('remote create failed and its cleanup could not be completed', { cause: cleanupError })\n }\n throw error\n }\n\n const remoteId = created.id?.trim() ?? ''\n const secret = created.key?.trim() ?? ''\n if (!remoteId || !secret) {\n if (remoteId) {\n try {\n await options.store.markProvisioningRemote({ id: rowId, keyId: remoteId })\n } catch {\n // revokePending uses the returned id even when this state write fails.\n }\n }\n try {\n await markPending(rowId, new Error('remote create returned no usable child key'), false)\n } catch {\n // Always attempt remote cleanup below.\n }\n const pendingRow = { ...provisioningRow, keyId: remoteId || provisioningRow.keyId, status: 'revocation_pending' as const }\n await revokePending(pendingRow, identity)\n throw new Error('remote create returned no usable child key')\n }\n\n onRemoteKey?.(provisioningRow, remoteId)\n\n try {\n await options.store.markProvisioningRemote({ id: rowId, keyId: remoteId })\n const keyEncrypted = await options.crypto.encrypt(secret)\n const remoteExpiresAt = created.expiresAt?.trim()\n const activeExpiresAt = remoteExpiresAt ? new Date(remoteExpiresAt) : expiresAt\n if (!Number.isFinite(activeExpiresAt.getTime())) throw new Error('remote child key expiry is malformed')\n const activeBudgetUsd = created.budgetUsd ?? budgetUsd\n if (!Number.isFinite(activeBudgetUsd) || activeBudgetUsd < 0) throw new Error('remote child key budget is malformed')\n await options.store.markActive({\n id: rowId,\n keyId: remoteId,\n keyEncrypted,\n expiresAt: activeExpiresAt,\n budgetUsd: activeBudgetUsd,\n })\n } catch (error) {\n // A persistence failure must not strand the already-created remote key.\n try {\n await markPending(rowId, error, false)\n } catch {\n // revokePending below still has the remote id and attempts cleanup.\n }\n await revokePending({ ...provisioningRow, keyId: remoteId, status: 'revocation_pending' }, identity)\n throw error\n }\n\n const row = await options.store.getActive(scopeFor(identity, product))\n if (!row || row.keyId !== remoteId || !sameIdentity(row, identity, product)) {\n const error = new Error('workspace child key was not persisted as active')\n try {\n await options.store.markRevocationPending({\n id: rowId,\n error: error.message,\n nextAttemptAt: now(),\n incrementAttempts: false,\n })\n } catch {\n // The remote id is still available for the cleanup attempt below.\n }\n await revokePending({ ...provisioningRow, keyId: remoteId, status: 'revocation_pending' }, identity)\n throw error\n }\n const usage = usageFromRemote(row, {\n budgetUsd: created.budgetUsd,\n budgetSpent: 0,\n expiresAt: created.expiresAt ?? expiresAt.toISOString(),\n }, now())\n return { key: secret, usage, refreshed: true }\n }\n\n async function ensureKey(identityInput: WorkspaceKeyIdentity, keyOptions?: { budgetUsd?: number }): Promise<WorkspaceRuntimeKey> {\n const identity = normalizeIdentity(identityInput)\n const budgetUsd = keyOptions?.budgetUsd ?? defaultBudgetUsd\n requirePositive(budgetUsd, 'budgetUsd')\n const scope = scopeKey(scopeFor(identity, product))\n let mintedRemote: { row: DurableWorkspaceKeyProvisioningRecord; remoteId: string } | null = null\n return withLocalLock(scope, () => withDurableLease(scope, async () => {\n const initialCleanup = await retryPendingRevocationsUnlocked(identity, identity)\n if (initialCleanup.pending > 0) throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n await reconcileProvisioning(identity)\n const active = await readActive(identity)\n if (active) {\n const key = await options.crypto.decrypt(active.row.keyEncrypted)\n if (!key.trim()) throw new Error('workspace child key secret is empty')\n return { key, usage: active.usage, refreshed: false }\n }\n const finalCleanup = await retryPendingRevocationsUnlocked(identity, identity)\n if (finalCleanup.pending > 0) throw new Error('a previous workspace child-key cleanup is pending; issuance is blocked until it completes')\n await assertNoPendingRevocations(identity)\n return mint(identity, budgetUsd, (row, remoteId) => {\n mintedRemote = { row, remoteId }\n })\n }, async () => {\n const remote = mintedRemote\n if (!remote) return\n const cleaned = await revokePending({\n ...remote.row,\n keyId: remote.remoteId,\n status: 'revocation_pending',\n }, identity)\n if (!cleaned) throw new Error('lease-loss child-key cleanup is pending')\n }))\n }\n\n async function getUsage(identityInput: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null> {\n const identity = normalizeIdentity(identityInput)\n const scope = scopeKey(scopeFor(identity, product))\n return withLocalLock(scope, () => withDurableLease(scope, async () => {\n const active = await readActive(identity)\n return active?.usage ?? null\n }))\n }\n\n return { ensureKey, getUsage, retryPendingRevocations }\n}\n","import { assertWorkspaceKeyCryptoUsable } from './crypto-probe'\n\n/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations the key manager needs. Wire it from the\n * platform via {@link createTcloudKeyProvisioner} rather than casting. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/**\n * The subset of the `@tangle-network/tcloud` `TCloudClient` the provisioner uses\n * — declared with METHOD syntax so the real client (whose `product` is a narrow\n * union and whose budgets are `number | null`) is assignable bivariantly. The\n * real SDK client satisfies this; pass it straight in.\n */\nexport interface TcloudKeyClient {\n createKey(opts: {\n name: string\n product?: string\n budgetUsd?: number\n expiresAt?: string\n parentKeyId?: string\n allowedModels?: string[]\n rpmLimit?: number\n }): Promise<{ id: string; key: string }>\n getKey(id: string): Promise<{ budgetUsd?: number | null; budgetSpent?: number; expiresAt?: string | null }>\n revokeKey(id: string): Promise<unknown>\n}\n\n/**\n * Adapt the tcloud SDK client to {@link KeyProvisioner} — the typed seam that\n * replaces the `as unknown as KeyProvisioner` cast every consumer otherwise\n * repeats. The platform already exposes child-key minting (parent→child key,\n * per-key USD budget, expiry); this maps its shapes (`product` union,\n * `number | null` budgets) onto the manager's contract (`null → undefined`).\n */\nexport function createTcloudKeyProvisioner(client: TcloudKeyClient): KeyProvisioner {\n return {\n createKey: async (input) => {\n const created = await client.createKey(input)\n return { id: created.id, key: created.key }\n },\n revokeKey: (keyId) => client.revokeKey(keyId),\n getKey: async (keyId) => {\n const info = await client.getKey(keyId)\n return {\n budgetUsd: info.budgetUsd ?? undefined,\n budgetSpent: info.budgetSpent ?? undefined,\n expiresAt: info.expiresAt ?? null,\n }\n },\n }\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\n/** Define configuration options for managing workspace keys including provisioning, storage, and cryptography */\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\n/** Describe usage and budget details for a workspace model key including expiration and exhaustion status */\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\n/** Manage workspace keys by ensuring, rotating, and tracking usage of active child-key secrets */\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\n// ---------------------------------------------------------------------------\n// Shared-platform-balance billing\n//\n// A DIFFERENT model from the per-workspace child-key manager above: here every\n// user runs against a SHARED platform balance (id.tangle.tools), keyed by the\n// user's platform identity. The app owns no key minting — it reads the balance,\n// gates a billable turn, and deducts spend through the platform billing API.\n// Plan limits, the platform transport, and identity resolution are SEAMS the\n// product supplies; this module imports no DB and no HTTP client.\n// ---------------------------------------------------------------------------\n\n/** A user's resolved platform identity (from the app's SSO account store). */\nexport interface PlatformIdentity {\n platformUserId: string\n /** The user's per-user platform API key (reads), or null when unlinked. */\n apiKey: string | null\n}\n\n/** Spendable balance for a platform user. */\nexport interface PlatformBalanceInfo {\n balance: number\n lifetimeSpent: number\n}\n\n/** Per-product spend aggregate. */\nexport interface PlatformProductUsage {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Plan limits — a PARAMETER per product (dollar allowance, concurrency,\n * overage policy). Never baked into the framework. */\nexport interface PlanLimit {\n monthlyBalanceUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * The platform billing transport — the product wires these to id.tangle.tools\n * (or any balance backend). Reads authenticate as the user (their `apiKey`);\n * the deduct write is a service-token call naming the target user. This module\n * never touches HTTP — it only sequences these calls.\n */\nexport interface PlatformBillingClient<Plan extends string> {\n /** Resolve the user's platform identity, or null when there is no SSO account. */\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n /** Subscription plan for the user (via their platform key). */\n getPlan(apiKey: string): Promise<Plan>\n /** Spendable balance for the user (via their platform key). */\n getBalance(apiKey: string): Promise<PlatformBalanceInfo>\n /** Per-product usage rows for the user (via their platform key). */\n getUsageByProduct(apiKey: string): Promise<PlatformProductUsage[]>\n /** Deduct spend against the user's balance (service-token write). */\n deduct(input: { platformUserId: string; amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n}\n\n/** Define shared billing state including user ID, plan, balances, concurrency, and overage permission */\nexport interface SharedBillingState<Plan extends string> {\n /** Platform user id, or null when the user has no Tangle SSO account. */\n platformUserId: string | null\n plan: Plan\n monthlyBalanceUsd: number\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define configuration options for managing platform balance based on billing plans */\nexport interface PlatformBalanceManagerOptions<Plan extends string> {\n client: PlatformBillingClient<Plan>\n /** Plan → limits map (the product's pricing). */\n planLimits: Record<Plan, PlanLimit>\n /** The plan an unlinked / outage user falls to (fails CLOSED). */\n freePlan: Plan\n /** The product slug to attribute usage to (for `getProductUsage`). */\n productSlug: string\n}\n\n/** Manage user plans and balances including state retrieval, billing authorization, deduction, and usage tracking */\nexport interface PlatformBalanceManager<Plan extends string> {\n /** Resolve the user's plan + balance. Unlinked or platform-outage users fail\n * CLOSED: free plan, zero remaining balance — a billable run is never started\n * against an unknown balance. */\n getState(userId: string): Promise<SharedBillingState<Plan>>\n /** Gate a billable turn: allowed when the plan permits overage or remaining\n * balance is positive. Returns the state so the caller deducts against it. */\n canStartBillableTurn(userId: string): Promise<{ allowed: boolean; state: SharedBillingState<Plan> }>\n /** Deduct `amountUsd` against the user's platform balance. Throws when the\n * user is not platform-linked. */\n deduct(userId: string, params: { amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n /** This product's spend for the user (drives a usage panel). */\n getProductUsage(userId: string): Promise<{ spentUsd: number; transactionCount: number }>\n}\n\n/** Create a platform balance manager to handle user plan limits and state based on provided options */\nexport function createPlatformBalanceManager<Plan extends string>(\n opts: PlatformBalanceManagerOptions<Plan>,\n): PlatformBalanceManager<Plan> {\n const { client, planLimits, freePlan, productSlug } = opts\n\n const getState: PlatformBalanceManager<Plan>['getState'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n // No SSO account, or linked without a platform key: unlinked free tier with\n // zero balance. Reads require the user's key — never call them empty.\n if (!identity || !identity.apiKey) {\n const limits = planLimits[freePlan]\n return {\n platformUserId: identity?.platformUserId ?? null,\n plan: freePlan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n const [plan, balance] = await Promise.all([client.getPlan(identity.apiKey), client.getBalance(identity.apiKey)])\n const limits = planLimits[plan]\n return {\n platformUserId: identity.platformUserId,\n plan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n\n const canStartBillableTurn: PlatformBalanceManager<Plan>['canStartBillableTurn'] = async (userId) => {\n const state = await getState(userId)\n if (!state.platformUserId) return { allowed: false, state }\n const allowed = state.overageAllowed || state.remainingBalanceUsd > 0\n return { allowed, state }\n }\n\n const deduct: PlatformBalanceManager<Plan>['deduct'] = async (userId, params) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity) throw new Error('Shared billing requires a platform-linked user')\n await client.deduct({\n platformUserId: identity.platformUserId,\n amountUsd: params.amountUsd,\n type: params.type,\n description: params.description,\n referenceId: params.referenceId,\n })\n }\n\n const getProductUsage: PlatformBalanceManager<Plan>['getProductUsage'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity?.apiKey) return { spentUsd: 0, transactionCount: 0 }\n const rows = await client.getUsageByProduct(identity.apiKey)\n const product = rows.find((row) => row.product === productSlug)\n return { spentUsd: product?.totalSpent ?? 0, transactionCount: product?.count ?? 0 }\n }\n\n return { getState, canStartBillableTurn, deduct, getProductUsage }\n}\n\n/** Create a workspace key manager that handles key provisioning and budget tracking */\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n // Prove the at-rest crypto seam is usable BEFORE spending a platform mint.\n // The `KeyCrypto` impl may close over an env-resolved key (createFieldCrypto)\n // that is empty/invalid and only throws at encrypt time — after the mint —\n // orphaning the child key. A round-trip probe makes a misconfigured consumer\n // fail loud with ZERO platform spend.\n await assertWorkspaceKeyCryptoUsable(opts.crypto)\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const mintedKeyId = created.id\n\n // Past this point the platform has minted a budget-bearing child key. ANY\n // failure before the row is persisted (encrypt, insert) MUST revoke the\n // just-minted key, else it leaks as a zombie holding parent-key headroom —\n // a missing/invalid encryption key would otherwise burn the child budget\n // per mint attempt until the parent can no longer mint (fleet-wide outage).\n let keyEncrypted: string\n let priors: Array<{ id: string; keyId: string }>\n try {\n keyEncrypted = await opts.crypto.encrypt(created.key)\n priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: mintedKeyId, keyEncrypted, budgetUsd, expiresAt })\n } catch (err) {\n // Best-effort revoke of the orphan. A revoke failure must NOT mask the\n // original cause — log it loudly and rethrow the original error.\n try {\n await opts.provisioner.revokeKey(mintedKeyId)\n } catch (revokeErr) {\n console.error(\n `[workspace-key-manager] FAILED to revoke orphaned child key ${mintedKeyId} ` +\n `for workspace ${workspaceId} after a post-mint failure — it now leaks parent-key budget. ` +\n `Revoke error:`,\n revokeErr,\n )\n }\n throw err\n }\n\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n\nexport {\n createIdentityBoundWorkspaceKeyManager,\n type DurableWorkspaceKeyManager,\n type DurableWorkspaceKeyManagerOptions,\n type DurableWorkspaceKeyProvisioner,\n type DurableWorkspaceKeyProvisioningRecord,\n type DurableWorkspaceKeyRecord,\n type DurableWorkspaceKeyScope,\n type DurableWorkspaceKeyStore,\n type WorkspaceKeyIdentity,\n type WorkspaceKeyProduct,\n type WorkspaceKeyStatus,\n type WorkspaceKeyUsage,\n type WorkspaceRuntimeKey,\n} from './identity-bound'\n"],"mappings":"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","names":["sourceKeyId","operationId","limits"]}