xytara 3.1.13 → 3.1.14

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.
Files changed (55) hide show
  1. package/.env.example +8 -1
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/RELEASE_NOTES.md +6 -4
  4. package/lib/a2a_rpc.js +280 -280
  5. package/lib/account_auth.js +585 -585
  6. package/lib/agent_identity.js +162 -162
  7. package/lib/bolt_adapter.js +187 -187
  8. package/lib/bolt_payment_front.js +224 -224
  9. package/lib/checkout_processor_adapter.js +217 -217
  10. package/lib/command_flow.js +722 -722
  11. package/lib/commerce_artifacts.js +591 -591
  12. package/lib/commerce_bridge.js +274 -274
  13. package/lib/commerce_cases.js +399 -399
  14. package/lib/commerce_client.js +7694 -7694
  15. package/lib/commerce_quotes.js +29 -0
  16. package/lib/commerce_runtime.js +322 -322
  17. package/lib/execution_dedupe.js +86 -86
  18. package/lib/external_credit_bridge.js +142 -142
  19. package/lib/http_transport.js +81 -81
  20. package/lib/l402_adapter.js +241 -241
  21. package/lib/l402_payment_front.js +265 -265
  22. package/lib/mpp_adapter.js +237 -237
  23. package/lib/payment_config.js +253 -253
  24. package/lib/payment_verification.js +357 -357
  25. package/lib/provider_payout_execution.js +155 -155
  26. package/lib/provider_registry.js +842 -842
  27. package/lib/rate_limiter.js +81 -81
  28. package/lib/runtime_state_store.js +491 -369
  29. package/lib/settlement_bsv_live.js +600 -600
  30. package/lib/settlement_bsv_teranode.js +117 -117
  31. package/lib/stripe_mpp_payment_front.js +258 -258
  32. package/lib/x402_facilitator.js +191 -191
  33. package/package.json +5 -3
  34. package/scripts/state_backup_offsite.js +196 -0
  35. package/scripts/verify_a2a_rpc.js +105 -105
  36. package/scripts/verify_agent_identity.js +153 -153
  37. package/scripts/verify_all.js +1 -1
  38. package/scripts/verify_bolt_adapter.js +128 -128
  39. package/scripts/verify_cdp_facilitator.js +110 -110
  40. package/scripts/verify_checkout_processor.js +109 -109
  41. package/scripts/verify_commerce_bridge.js +177 -177
  42. package/scripts/verify_execution_dedupe.js +189 -189
  43. package/scripts/verify_execution_surfacing.js +76 -76
  44. package/scripts/verify_final_activation.js +191 -191
  45. package/scripts/verify_l402_adapter.js +86 -86
  46. package/scripts/verify_mpp_adapter.js +128 -128
  47. package/scripts/verify_production_readiness.js +268 -268
  48. package/scripts/verify_rate_limiter.js +52 -52
  49. package/scripts/verify_refund_credit.js +106 -106
  50. package/scripts/verify_release_candidate.js +81 -81
  51. package/scripts/verify_runtime_state_guard.js +117 -0
  52. package/scripts/verify_service.js +15421 -15421
  53. package/scripts/verify_verify_execute_settle.js +208 -208
  54. package/scripts/verify_x402_facilitator.js +286 -286
  55. package/server.js +11976 -11974
@@ -1,585 +1,585 @@
1
- "use strict";
2
-
3
- const crypto = require("crypto");
4
- const { normalizeAgentPublicKey, verifySpendAuthProof } = require("./agent_identity");
5
-
6
- function normalizeString(value, fallback = null) {
7
- const trimmed = typeof value === "string" ? value.trim() : "";
8
- return trimmed || fallback;
9
- }
10
-
11
- function ensureObject(value) {
12
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
13
- }
14
-
15
- function ensureArray(value) {
16
- return Array.isArray(value) ? value : [];
17
- }
18
-
19
- function nowIso() {
20
- return new Date().toISOString();
21
- }
22
-
23
- function buildCredentialPublicView(credential, includeToken = false) {
24
- if (!credential) return null;
25
- const view = {
26
- credential_id: credential.credential_id,
27
- account_id: credential.account_id,
28
- status: credential.status,
29
- spend_posture: credential.spend_posture,
30
- effective_pool_binding_mode: credential.entitlement_id ? "entitlement_bound" : credential.pack_id ? "pack_bound" : "unbound",
31
- scope: credential.scope,
32
- agent_id: credential.agent_id,
33
- budget_id: credential.budget_id,
34
- pack_id: credential.pack_id || null,
35
- entitlement_id: credential.entitlement_id || null,
36
- expires_at_iso: credential.expires_at_iso,
37
- created_at_iso: credential.created_at_iso,
38
- updated_at_iso: credential.updated_at_iso,
39
- issued_by: credential.issued_by || null,
40
- labels: ensureArray(credential.labels),
41
- // Cryptographic identity: when a credential is bound to an agent ed25519 key,
42
- // using it requires a fresh signed proof (not just the bearer token).
43
- identity_bound: Boolean(credential.agent_public_key),
44
- agent_key_ref: credential.agent_key_ref || null
45
- };
46
- if (includeToken) {
47
- view.bearer_token = credential.bearer_token;
48
- }
49
- return view;
50
- }
51
-
52
- function buildAuthorityBindingPublicView(binding, credential, includeToken = false) {
53
- if (!binding) return null;
54
- const view = {
55
- binding_id: binding.binding_id,
56
- account_id: binding.account_id,
57
- status: binding.status,
58
- authority_kind: binding.authority_kind,
59
- spend_posture: binding.spend_posture,
60
- scope: binding.scope,
61
- authority_scope: ensureArray(binding.authority_scope),
62
- allowed_consequence_families: ensureArray(binding.allowed_consequence_families),
63
- consequence_bounds: binding.consequence_bounds || {
64
- max_commit_units: null,
65
- max_reserve_units: null,
66
- max_reversal_units: null
67
- },
68
- agent_id: binding.agent_id,
69
- budget_id: binding.budget_id,
70
- pack_id: binding.pack_id || null,
71
- entitlement_id: binding.entitlement_id || null,
72
- credential_id: binding.credential_id || null,
73
- issued_by: binding.issued_by || null,
74
- labels: ensureArray(binding.labels),
75
- expires_at_iso: binding.expires_at_iso || null,
76
- rotated_from_binding_id: binding.rotated_from_binding_id || null,
77
- replaced_by_binding_id: binding.replaced_by_binding_id || null,
78
- created_at_iso: binding.created_at_iso,
79
- updated_at_iso: binding.updated_at_iso,
80
- revoked_at_iso: binding.revoked_at_iso || null,
81
- revoke_reason: binding.revoke_reason || null,
82
- spend_credential: buildCredentialPublicView(credential, includeToken)
83
- };
84
- return view;
85
- }
86
-
87
- function buildAuthorityBindingContractSummary(binding, credential) {
88
- if (!binding) return null;
89
- const publicView = buildAuthorityBindingPublicView(binding, credential, false);
90
- return {
91
- summary_version: "xytara-authority-binding-contract-summary-v1",
92
- binding_id: binding.binding_id,
93
- account_id: binding.account_id,
94
- authority_kind: binding.authority_kind,
95
- spend_posture: binding.spend_posture,
96
- authority_scope: ensureArray(binding.authority_scope),
97
- allowed_consequence_families: ensureArray(binding.allowed_consequence_families),
98
- consequence_bounds: publicView.consequence_bounds,
99
- linked_credential_id: binding.credential_id || null,
100
- contract_state: binding.status === "active" ? "active_contract" : "inactive_contract",
101
- linked_surfaces: {
102
- binding_ref: `/v1/account-auth/authority-bindings/${encodeURIComponent(binding.binding_id)}`,
103
- spend_credential_ref: binding.credential_id ? `/v1/account-auth/spend-credentials/${encodeURIComponent(binding.credential_id)}` : null
104
- }
105
- };
106
- }
107
-
108
- function buildAuthorityBindingRotationSummary(binding, credential) {
109
- if (!binding) return null;
110
- const active = binding.status === "active";
111
- return {
112
- summary_version: "xytara-authority-binding-rotation-summary-v1",
113
- binding_id: binding.binding_id,
114
- account_id: binding.account_id,
115
- status: binding.status,
116
- renewable_state: active ? "renewable" : "inactive",
117
- rotatable_state: active ? "rotatable" : "inactive",
118
- expires_at_iso: binding.expires_at_iso || null,
119
- rotated_from_binding_id: binding.rotated_from_binding_id || null,
120
- replaced_by_binding_id: binding.replaced_by_binding_id || null,
121
- linked_credential_id: binding.credential_id || null,
122
- linked_surfaces: {
123
- binding_ref: `/v1/account-auth/authority-bindings/${encodeURIComponent(binding.binding_id)}`,
124
- contract_summary_ref: `/v1/account-auth/authority-bindings/${encodeURIComponent(binding.binding_id)}/contract-summary`,
125
- spend_credential_ref: binding.credential_id ? `/v1/account-auth/spend-credentials/${encodeURIComponent(binding.credential_id)}` : null
126
- }
127
- };
128
- }
129
-
130
- function isCredentialExpired(credential) {
131
- if (!credential || !credential.expires_at_iso) return false;
132
- const expiresAt = Date.parse(credential.expires_at_iso);
133
- return Number.isFinite(expiresAt) ? Date.now() >= expiresAt : false;
134
- }
135
-
136
- function createSpendCredential(state, body) {
137
- const payload = ensureObject(body);
138
- const credentialId = normalizeString(payload.credential_id, `spend_credential_${state.accountSpendCredentials.size + 1}`);
139
- const accountId = normalizeString(payload.account_id, "acct_demo");
140
- const agentId = normalizeString(payload.agent_id, null);
141
- const budgetId = normalizeString(payload.budget_id, null);
142
- const packId = normalizeString(payload.pack_id, null);
143
- const entitlementId = normalizeString(payload.entitlement_id, null);
144
- const createdAtIso = nowIso();
145
- const expiresAtIso = normalizeString(payload.expires_at_iso, null);
146
- // Optional cryptographic identity binding: an ed25519 agent public key. When
147
- // set, the credential can only be USED with a fresh signature from that key.
148
- // Only a VALID key is bound — an unparseable key is ignored (no brick / no
149
- // inconsistent identity_bound=true with a null ref); routes should reject an
150
- // invalid key up front so the operator sees the error.
151
- const agentPublicKeyRaw = normalizeString(payload.agent_public_key, null);
152
- const agentKeyMaterial = agentPublicKeyRaw ? normalizeAgentPublicKey(agentPublicKeyRaw) : null;
153
- const agentPublicKey = agentKeyMaterial ? agentPublicKeyRaw : null;
154
- const agentKeyRef = agentKeyMaterial ? agentKeyMaterial.key_ref : null;
155
- const credential = {
156
- credential_id: credentialId,
157
- bearer_token: normalizeString(payload.bearer_token, `xytara_spend_${crypto.randomBytes(18).toString("hex")}`),
158
- agent_public_key: agentPublicKey,
159
- agent_key_ref: agentKeyRef,
160
- account_id: accountId,
161
- agent_id: agentId,
162
- budget_id: budgetId,
163
- pack_id: packId,
164
- entitlement_id: entitlementId,
165
- spend_posture: agentId || budgetId ? "bounded_spend" : "open_spend",
166
- scope: normalizeString(payload.scope, "runtime_spend"),
167
- labels: ensureArray(payload.labels).map((value) => String(value || "").trim()).filter(Boolean),
168
- status: normalizeString(payload.status, "active"),
169
- issued_by: normalizeString(payload.issued_by, "operator"),
170
- expires_at_iso: expiresAtIso,
171
- created_at_iso: createdAtIso,
172
- updated_at_iso: createdAtIso,
173
- revoked_at_iso: null,
174
- revoke_reason: null
175
- };
176
- state.accountSpendCredentials.set(credentialId, credential);
177
- state.accountSpendCredentialsByToken.set(credential.bearer_token, credentialId);
178
- return buildCredentialPublicView(credential, true);
179
- }
180
-
181
- function createAuthorityBinding(state, body) {
182
- const payload = ensureObject(body);
183
- const bindingId = normalizeString(payload.binding_id, `authority_binding_${state.accountAuthorityBindings.size + 1}`);
184
- const credential = createSpendCredential(state, {
185
- credential_id: normalizeString(payload.credential_id, `spend_for_${bindingId}`),
186
- bearer_token: normalizeString(payload.bearer_token, null),
187
- agent_public_key: normalizeString(payload.agent_public_key, null),
188
- account_id: normalizeString(payload.account_id, "acct_demo"),
189
- agent_id: normalizeString(payload.agent_id, null),
190
- budget_id: normalizeString(payload.budget_id, null),
191
- pack_id: normalizeString(payload.pack_id, null),
192
- entitlement_id: normalizeString(payload.entitlement_id, null),
193
- scope: normalizeString(payload.scope, "runtime_spend"),
194
- labels: ensureArray(payload.labels),
195
- status: normalizeString(payload.status, "active"),
196
- issued_by: normalizeString(payload.issued_by, "operator"),
197
- expires_at_iso: normalizeString(payload.expires_at_iso, null)
198
- });
199
- const createdAtIso = nowIso();
200
- const binding = {
201
- binding_id: bindingId,
202
- account_id: credential.account_id,
203
- credential_id: credential.credential_id,
204
- authority_kind: normalizeString(payload.authority_kind, "delegated_runtime_spend"),
205
- spend_posture: credential.spend_posture,
206
- scope: credential.scope,
207
- authority_scope: ensureArray(payload.authority_scope).map((value) => String(value || "").trim()).filter(Boolean),
208
- allowed_consequence_families: ensureArray(payload.allowed_consequence_families).map((value) => String(value || "").trim()).filter(Boolean),
209
- consequence_bounds: {
210
- max_commit_units: Number.isFinite(Number(payload.max_commit_units)) ? Number(payload.max_commit_units) : null,
211
- max_reserve_units: Number.isFinite(Number(payload.max_reserve_units)) ? Number(payload.max_reserve_units) : null,
212
- max_reversal_units: Number.isFinite(Number(payload.max_reversal_units)) ? Number(payload.max_reversal_units) : null
213
- },
214
- agent_id: credential.agent_id,
215
- budget_id: credential.budget_id,
216
- pack_id: credential.pack_id || null,
217
- entitlement_id: credential.entitlement_id || null,
218
- status: normalizeString(payload.status, "active"),
219
- issued_by: normalizeString(payload.issued_by, "operator"),
220
- labels: ensureArray(payload.labels),
221
- expires_at_iso: credential.expires_at_iso,
222
- rotated_from_binding_id: normalizeString(payload.rotated_from_binding_id, null),
223
- replaced_by_binding_id: normalizeString(payload.replaced_by_binding_id, null),
224
- created_at_iso: createdAtIso,
225
- updated_at_iso: createdAtIso,
226
- revoked_at_iso: null,
227
- revoke_reason: null
228
- };
229
- state.accountAuthorityBindings.set(bindingId, binding);
230
- return buildAuthorityBindingPublicView(binding, state.accountSpendCredentials.get(credential.credential_id), true);
231
- }
232
-
233
- function listSpendCredentials(state, filters) {
234
- const payload = ensureObject(filters);
235
- const accountId = normalizeString(payload.account_id, null);
236
- const status = normalizeString(payload.status, null);
237
- return Array.from(state.accountSpendCredentials.values())
238
- .filter((credential) => {
239
- if (accountId && credential.account_id !== accountId) return false;
240
- if (status && credential.status !== status) return false;
241
- return true;
242
- })
243
- .map((credential) => buildCredentialPublicView(credential, false));
244
- }
245
-
246
- function getSpendCredential(state, credentialId) {
247
- if (!credentialId || !state.accountSpendCredentials.has(credentialId)) return null;
248
- return buildCredentialPublicView(state.accountSpendCredentials.get(credentialId), false);
249
- }
250
-
251
- function listAuthorityBindings(state, filters) {
252
- const payload = ensureObject(filters);
253
- const accountId = normalizeString(payload.account_id, null);
254
- const status = normalizeString(payload.status, null);
255
- return Array.from(state.accountAuthorityBindings.values())
256
- .filter((binding) => {
257
- if (accountId && binding.account_id !== accountId) return false;
258
- if (status && binding.status !== status) return false;
259
- return true;
260
- })
261
- .map((binding) => buildAuthorityBindingPublicView(binding, state.accountSpendCredentials.get(binding.credential_id), false));
262
- }
263
-
264
- function getAuthorityBinding(state, bindingId) {
265
- if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
266
- const binding = state.accountAuthorityBindings.get(bindingId);
267
- return buildAuthorityBindingPublicView(binding, state.accountSpendCredentials.get(binding.credential_id), false);
268
- }
269
-
270
- function getAuthorityBindingContractSummary(state, bindingId) {
271
- if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
272
- const binding = state.accountAuthorityBindings.get(bindingId);
273
- return buildAuthorityBindingContractSummary(binding, state.accountSpendCredentials.get(binding.credential_id));
274
- }
275
-
276
- function getAuthorityBindingRotationSummary(state, bindingId) {
277
- if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
278
- const binding = state.accountAuthorityBindings.get(bindingId);
279
- return buildAuthorityBindingRotationSummary(binding, state.accountSpendCredentials.get(binding.credential_id));
280
- }
281
-
282
- function evaluateAuthorityBindingAction(binding, action, units) {
283
- const target = binding && typeof binding === "object" ? binding : null;
284
- const numericUnits = Number.isFinite(Number(units)) ? Number(units) : 0;
285
- const allowedScopes = target ? ensureArray(target.authority_scope) : [];
286
- const allowedFamilies = target ? ensureArray(target.allowed_consequence_families) : [];
287
- const bounds = target && target.consequence_bounds ? target.consequence_bounds : {};
288
-
289
- if (!target) {
290
- return { ok: true };
291
- }
292
- if (target.status !== "active") {
293
- return { ok: false, reason: "authority_binding_inactive", binding_id: target.binding_id };
294
- }
295
-
296
- const scopeMap = {
297
- delegated_credit_spend: "runtime.execute",
298
- reserve: "economics.reserve",
299
- release: "economics.release",
300
- reverse: "economics.reverse"
301
- };
302
- const familyMap = {
303
- delegated_credit_spend: "commit",
304
- reserve: "reserve",
305
- release: "release",
306
- reverse: "reverse"
307
- };
308
- const boundKeyMap = {
309
- delegated_credit_spend: "max_commit_units",
310
- reserve: "max_reserve_units",
311
- reverse: "max_reversal_units"
312
- };
313
-
314
- const requiredScope = scopeMap[action] || null;
315
- if (requiredScope && allowedScopes.length > 0 && !allowedScopes.includes(requiredScope)) {
316
- return {
317
- ok: false,
318
- reason: "authority_scope_not_allowed",
319
- binding_id: target.binding_id,
320
- required_scope: requiredScope,
321
- authority_scope: allowedScopes
322
- };
323
- }
324
-
325
- const requiredFamily = familyMap[action] || null;
326
- if (requiredFamily && allowedFamilies.length > 0 && !allowedFamilies.includes(requiredFamily)) {
327
- return {
328
- ok: false,
329
- reason: "authority_consequence_family_not_allowed",
330
- binding_id: target.binding_id,
331
- required_consequence_family: requiredFamily,
332
- allowed_consequence_families: allowedFamilies
333
- };
334
- }
335
-
336
- const boundKey = boundKeyMap[action] || null;
337
- if (boundKey && Number.isFinite(Number(bounds[boundKey])) && numericUnits > Number(bounds[boundKey])) {
338
- return {
339
- ok: false,
340
- reason: "authority_consequence_bound_exceeded",
341
- binding_id: target.binding_id,
342
- bound_key: boundKey,
343
- max_units: Number(bounds[boundKey]),
344
- attempted_units: numericUnits
345
- };
346
- }
347
-
348
- return {
349
- ok: true,
350
- binding_id: target.binding_id,
351
- authority_scope: allowedScopes,
352
- allowed_consequence_families: allowedFamilies,
353
- consequence_bounds: {
354
- max_commit_units: Number.isFinite(Number(bounds.max_commit_units)) ? Number(bounds.max_commit_units) : null,
355
- max_reserve_units: Number.isFinite(Number(bounds.max_reserve_units)) ? Number(bounds.max_reserve_units) : null,
356
- max_reversal_units: Number.isFinite(Number(bounds.max_reversal_units)) ? Number(bounds.max_reversal_units) : null
357
- }
358
- };
359
- }
360
-
361
- function revokeSpendCredential(state, credentialId, body) {
362
- if (!credentialId || !state.accountSpendCredentials.has(credentialId)) return null;
363
- const payload = ensureObject(body);
364
- const current = state.accountSpendCredentials.get(credentialId);
365
- const next = {
366
- ...current,
367
- status: "revoked",
368
- revoked_at_iso: nowIso(),
369
- revoke_reason: normalizeString(payload.reason, "operator_revoked"),
370
- updated_at_iso: nowIso()
371
- };
372
- state.accountSpendCredentials.set(credentialId, next);
373
- return buildCredentialPublicView(next, false);
374
- }
375
-
376
- function revokeAuthorityBinding(state, bindingId, body) {
377
- if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
378
- const payload = ensureObject(body);
379
- const current = state.accountAuthorityBindings.get(bindingId);
380
- const next = {
381
- ...current,
382
- status: "revoked",
383
- revoked_at_iso: nowIso(),
384
- revoke_reason: normalizeString(payload.reason, "operator_revoked"),
385
- updated_at_iso: nowIso()
386
- };
387
- state.accountAuthorityBindings.set(bindingId, next);
388
- if (current.credential_id) {
389
- revokeSpendCredential(state, current.credential_id, payload);
390
- }
391
- return buildAuthorityBindingPublicView(next, current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null, false);
392
- }
393
-
394
- function renewAuthorityBinding(state, bindingId, body) {
395
- if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
396
- const payload = ensureObject(body);
397
- const current = state.accountAuthorityBindings.get(bindingId);
398
- const nextExpiresAtIso = normalizeString(payload.expires_at_iso, current.expires_at_iso || null);
399
- const updatedAtIso = nowIso();
400
- const next = {
401
- ...current,
402
- expires_at_iso: nextExpiresAtIso,
403
- updated_at_iso: updatedAtIso
404
- };
405
- state.accountAuthorityBindings.set(bindingId, next);
406
- if (current.credential_id && state.accountSpendCredentials.has(current.credential_id)) {
407
- const credential = state.accountSpendCredentials.get(current.credential_id);
408
- state.accountSpendCredentials.set(current.credential_id, {
409
- ...credential,
410
- expires_at_iso: nextExpiresAtIso,
411
- updated_at_iso: updatedAtIso
412
- });
413
- }
414
- return buildAuthorityBindingPublicView(next, current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null, false);
415
- }
416
-
417
- function rotateAuthorityBinding(state, bindingId, body) {
418
- if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
419
- const payload = ensureObject(body);
420
- const current = state.accountAuthorityBindings.get(bindingId);
421
- const currentCredential = current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null;
422
- const successorBindingId = normalizeString(payload.successor_binding_id, `${bindingId}_rotated`);
423
- const successorCredentialId = normalizeString(payload.successor_credential_id, current.credential_id ? `${current.credential_id}_rotated` : `spend_for_${successorBindingId}`);
424
- const successor = createAuthorityBinding(state, {
425
- binding_id: successorBindingId,
426
- credential_id: successorCredentialId,
427
- bearer_token: normalizeString(payload.bearer_token, null),
428
- // Carry the cryptographic identity binding forward on rotation — otherwise a
429
- // key-bound credential would silently downgrade to bearer-only.
430
- agent_public_key: normalizeString(payload.agent_public_key, currentCredential ? currentCredential.agent_public_key : null),
431
- account_id: current.account_id,
432
- agent_id: normalizeString(payload.agent_id, current.agent_id),
433
- budget_id: normalizeString(payload.budget_id, current.budget_id),
434
- pack_id: normalizeString(payload.pack_id, current.pack_id),
435
- entitlement_id: normalizeString(payload.entitlement_id, current.entitlement_id),
436
- scope: normalizeString(payload.scope, current.scope),
437
- authority_kind: normalizeString(payload.authority_kind, current.authority_kind),
438
- authority_scope: ensureArray(payload.authority_scope).length > 0 ? ensureArray(payload.authority_scope) : ensureArray(current.authority_scope),
439
- allowed_consequence_families: ensureArray(payload.allowed_consequence_families).length > 0 ? ensureArray(payload.allowed_consequence_families) : ensureArray(current.allowed_consequence_families),
440
- max_commit_units: Number.isFinite(Number(payload.max_commit_units)) ? Number(payload.max_commit_units) : current.consequence_bounds.max_commit_units,
441
- max_reserve_units: Number.isFinite(Number(payload.max_reserve_units)) ? Number(payload.max_reserve_units) : current.consequence_bounds.max_reserve_units,
442
- max_reversal_units: Number.isFinite(Number(payload.max_reversal_units)) ? Number(payload.max_reversal_units) : current.consequence_bounds.max_reversal_units,
443
- labels: ensureArray(payload.labels).length > 0 ? ensureArray(payload.labels) : ensureArray(current.labels),
444
- issued_by: normalizeString(payload.issued_by, current.issued_by || "operator"),
445
- expires_at_iso: normalizeString(payload.expires_at_iso, current.expires_at_iso || (currentCredential ? currentCredential.expires_at_iso : null)),
446
- rotated_from_binding_id: current.binding_id
447
- });
448
- const revoked = revokeAuthorityBinding(state, bindingId, {
449
- reason: normalizeString(payload.reason, "rotated_to_successor")
450
- });
451
- const successorRecord = state.accountAuthorityBindings.get(successor.binding_id);
452
- state.accountAuthorityBindings.set(successor.binding_id, {
453
- ...successorRecord,
454
- rotated_from_binding_id: current.binding_id
455
- });
456
- state.accountAuthorityBindings.set(bindingId, {
457
- ...state.accountAuthorityBindings.get(bindingId),
458
- replaced_by_binding_id: successor.binding_id
459
- });
460
- return {
461
- rotated_binding: buildAuthorityBindingPublicView(state.accountAuthorityBindings.get(bindingId), current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null, false),
462
- successor_authority_binding: buildAuthorityBindingPublicView(state.accountAuthorityBindings.get(successor.binding_id), state.accountSpendCredentials.get(successor.credential_id), true),
463
- previous_view: revoked
464
- };
465
- }
466
-
467
- function resolveSpendCredential(state, bearerToken, proof, requestBinding) {
468
- const token = normalizeString(bearerToken, null);
469
- if (!token) {
470
- return {
471
- ok: false,
472
- reason: "missing_spend_credential"
473
- };
474
- }
475
- const credentialId = state.accountSpendCredentialsByToken.get(token);
476
- if (!credentialId || !state.accountSpendCredentials.has(credentialId)) {
477
- return {
478
- ok: false,
479
- reason: "spend_credential_not_found"
480
- };
481
- }
482
- const credential = state.accountSpendCredentials.get(credentialId);
483
- if (credential.status !== "active") {
484
- return {
485
- ok: false,
486
- reason: "spend_credential_inactive",
487
- credential: buildCredentialPublicView(credential, false)
488
- };
489
- }
490
- if (isCredentialExpired(credential)) {
491
- return {
492
- ok: false,
493
- reason: "spend_credential_expired",
494
- credential: buildCredentialPublicView(credential, false)
495
- };
496
- }
497
- // Cryptographic identity gate: a key-bound credential requires a fresh signed
498
- // proof of control of the agent key — the bearer token alone is not enough.
499
- if (credential.agent_public_key) {
500
- // Bind the proof to the actual request (account/amount/task) when the caller supplies a
501
- // request binding. `require` is set BY THE CALLER (requestBinding.require) so strict mode
502
- // applies only where a binding is actually supplied — the execute path. The reserve/
503
- // release/reverse lifecycle paths pass no binding, so they are unaffected by strict mode
504
- // (and remain capped by the authority-binding bounds). Default keeps back-compat: an
505
- // unbound proof still authorizes.
506
- const requireBinding = Boolean(requestBinding && requestBinding.require);
507
- const verdict = verifySpendAuthProof(credential.agent_public_key, credential.credential_id, proof || {}, {
508
- requestBinding: requestBinding || null,
509
- requireBinding
510
- });
511
- if (!verdict.ok) {
512
- return {
513
- ok: false,
514
- reason: verdict.reason,
515
- credential: buildCredentialPublicView(credential, false)
516
- };
517
- }
518
- // Replay protection is MANDATORY for a key-bound credential — fail closed if
519
- // the replay store is unavailable rather than silently skipping it.
520
- const replayStore = state && state.acceptedAgentSignatures instanceof Map ? state.acceptedAgentSignatures : null;
521
- if (!replayStore) {
522
- return {
523
- ok: false,
524
- reason: "agent_replay_store_unavailable",
525
- credential: buildCredentialPublicView(credential, false)
526
- };
527
- }
528
- const now = Date.now();
529
- // Opportunistic prune: a signature past its freshness window can never be a
530
- // valid replay again, so forget it (bounds memory + snapshot size). The key is
531
- // the canonical decoded-signature fingerprint (re-encoding cannot evade it).
532
- for (const [key, expiresAt] of replayStore) {
533
- if (!Number.isFinite(expiresAt) || expiresAt <= now) replayStore.delete(key);
534
- }
535
- if (replayStore.has(verdict.canonical)) {
536
- return {
537
- ok: false,
538
- reason: "agent_signature_replayed",
539
- credential: buildCredentialPublicView(credential, false)
540
- };
541
- }
542
- replayStore.set(verdict.canonical, Number.isFinite(verdict.expires_at_ms) ? verdict.expires_at_ms : now + 120000);
543
- }
544
- const binding = Array.from(state.accountAuthorityBindings.values()).find((entry) => entry && entry.credential_id === credentialId && entry.status === "active") || null;
545
- return {
546
- ok: true,
547
- credential,
548
- binding,
549
- authorization_context: {
550
- verification_mode: "delegated_spend_credential",
551
- wallet_id: credential.credential_id,
552
- payment_payload: {
553
- binding_id: binding ? binding.binding_id : null,
554
- credential_id: credential.credential_id,
555
- spend_posture: credential.spend_posture,
556
- pack_id: credential.pack_id || null,
557
- entitlement_id: credential.entitlement_id || null,
558
- authority_scope: binding ? ensureArray(binding.authority_scope) : [],
559
- allowed_consequence_families: binding ? ensureArray(binding.allowed_consequence_families) : [],
560
- consequence_bounds: binding && binding.consequence_bounds ? binding.consequence_bounds : {
561
- max_commit_units: null,
562
- max_reserve_units: null,
563
- max_reversal_units: null
564
- }
565
- }
566
- }
567
- };
568
- }
569
-
570
- module.exports = {
571
- createAuthorityBinding,
572
- createSpendCredential,
573
- evaluateAuthorityBindingAction,
574
- getAuthorityBinding,
575
- getAuthorityBindingContractSummary,
576
- getAuthorityBindingRotationSummary,
577
- getSpendCredential,
578
- listAuthorityBindings,
579
- listSpendCredentials,
580
- renewAuthorityBinding,
581
- resolveSpendCredential,
582
- rotateAuthorityBinding,
583
- revokeAuthorityBinding,
584
- revokeSpendCredential
585
- };
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const { normalizeAgentPublicKey, verifySpendAuthProof } = require("./agent_identity");
5
+
6
+ function normalizeString(value, fallback = null) {
7
+ const trimmed = typeof value === "string" ? value.trim() : "";
8
+ return trimmed || fallback;
9
+ }
10
+
11
+ function ensureObject(value) {
12
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
13
+ }
14
+
15
+ function ensureArray(value) {
16
+ return Array.isArray(value) ? value : [];
17
+ }
18
+
19
+ function nowIso() {
20
+ return new Date().toISOString();
21
+ }
22
+
23
+ function buildCredentialPublicView(credential, includeToken = false) {
24
+ if (!credential) return null;
25
+ const view = {
26
+ credential_id: credential.credential_id,
27
+ account_id: credential.account_id,
28
+ status: credential.status,
29
+ spend_posture: credential.spend_posture,
30
+ effective_pool_binding_mode: credential.entitlement_id ? "entitlement_bound" : credential.pack_id ? "pack_bound" : "unbound",
31
+ scope: credential.scope,
32
+ agent_id: credential.agent_id,
33
+ budget_id: credential.budget_id,
34
+ pack_id: credential.pack_id || null,
35
+ entitlement_id: credential.entitlement_id || null,
36
+ expires_at_iso: credential.expires_at_iso,
37
+ created_at_iso: credential.created_at_iso,
38
+ updated_at_iso: credential.updated_at_iso,
39
+ issued_by: credential.issued_by || null,
40
+ labels: ensureArray(credential.labels),
41
+ // Cryptographic identity: when a credential is bound to an agent ed25519 key,
42
+ // using it requires a fresh signed proof (not just the bearer token).
43
+ identity_bound: Boolean(credential.agent_public_key),
44
+ agent_key_ref: credential.agent_key_ref || null
45
+ };
46
+ if (includeToken) {
47
+ view.bearer_token = credential.bearer_token;
48
+ }
49
+ return view;
50
+ }
51
+
52
+ function buildAuthorityBindingPublicView(binding, credential, includeToken = false) {
53
+ if (!binding) return null;
54
+ const view = {
55
+ binding_id: binding.binding_id,
56
+ account_id: binding.account_id,
57
+ status: binding.status,
58
+ authority_kind: binding.authority_kind,
59
+ spend_posture: binding.spend_posture,
60
+ scope: binding.scope,
61
+ authority_scope: ensureArray(binding.authority_scope),
62
+ allowed_consequence_families: ensureArray(binding.allowed_consequence_families),
63
+ consequence_bounds: binding.consequence_bounds || {
64
+ max_commit_units: null,
65
+ max_reserve_units: null,
66
+ max_reversal_units: null
67
+ },
68
+ agent_id: binding.agent_id,
69
+ budget_id: binding.budget_id,
70
+ pack_id: binding.pack_id || null,
71
+ entitlement_id: binding.entitlement_id || null,
72
+ credential_id: binding.credential_id || null,
73
+ issued_by: binding.issued_by || null,
74
+ labels: ensureArray(binding.labels),
75
+ expires_at_iso: binding.expires_at_iso || null,
76
+ rotated_from_binding_id: binding.rotated_from_binding_id || null,
77
+ replaced_by_binding_id: binding.replaced_by_binding_id || null,
78
+ created_at_iso: binding.created_at_iso,
79
+ updated_at_iso: binding.updated_at_iso,
80
+ revoked_at_iso: binding.revoked_at_iso || null,
81
+ revoke_reason: binding.revoke_reason || null,
82
+ spend_credential: buildCredentialPublicView(credential, includeToken)
83
+ };
84
+ return view;
85
+ }
86
+
87
+ function buildAuthorityBindingContractSummary(binding, credential) {
88
+ if (!binding) return null;
89
+ const publicView = buildAuthorityBindingPublicView(binding, credential, false);
90
+ return {
91
+ summary_version: "xytara-authority-binding-contract-summary-v1",
92
+ binding_id: binding.binding_id,
93
+ account_id: binding.account_id,
94
+ authority_kind: binding.authority_kind,
95
+ spend_posture: binding.spend_posture,
96
+ authority_scope: ensureArray(binding.authority_scope),
97
+ allowed_consequence_families: ensureArray(binding.allowed_consequence_families),
98
+ consequence_bounds: publicView.consequence_bounds,
99
+ linked_credential_id: binding.credential_id || null,
100
+ contract_state: binding.status === "active" ? "active_contract" : "inactive_contract",
101
+ linked_surfaces: {
102
+ binding_ref: `/v1/account-auth/authority-bindings/${encodeURIComponent(binding.binding_id)}`,
103
+ spend_credential_ref: binding.credential_id ? `/v1/account-auth/spend-credentials/${encodeURIComponent(binding.credential_id)}` : null
104
+ }
105
+ };
106
+ }
107
+
108
+ function buildAuthorityBindingRotationSummary(binding, credential) {
109
+ if (!binding) return null;
110
+ const active = binding.status === "active";
111
+ return {
112
+ summary_version: "xytara-authority-binding-rotation-summary-v1",
113
+ binding_id: binding.binding_id,
114
+ account_id: binding.account_id,
115
+ status: binding.status,
116
+ renewable_state: active ? "renewable" : "inactive",
117
+ rotatable_state: active ? "rotatable" : "inactive",
118
+ expires_at_iso: binding.expires_at_iso || null,
119
+ rotated_from_binding_id: binding.rotated_from_binding_id || null,
120
+ replaced_by_binding_id: binding.replaced_by_binding_id || null,
121
+ linked_credential_id: binding.credential_id || null,
122
+ linked_surfaces: {
123
+ binding_ref: `/v1/account-auth/authority-bindings/${encodeURIComponent(binding.binding_id)}`,
124
+ contract_summary_ref: `/v1/account-auth/authority-bindings/${encodeURIComponent(binding.binding_id)}/contract-summary`,
125
+ spend_credential_ref: binding.credential_id ? `/v1/account-auth/spend-credentials/${encodeURIComponent(binding.credential_id)}` : null
126
+ }
127
+ };
128
+ }
129
+
130
+ function isCredentialExpired(credential) {
131
+ if (!credential || !credential.expires_at_iso) return false;
132
+ const expiresAt = Date.parse(credential.expires_at_iso);
133
+ return Number.isFinite(expiresAt) ? Date.now() >= expiresAt : false;
134
+ }
135
+
136
+ function createSpendCredential(state, body) {
137
+ const payload = ensureObject(body);
138
+ const credentialId = normalizeString(payload.credential_id, `spend_credential_${state.accountSpendCredentials.size + 1}`);
139
+ const accountId = normalizeString(payload.account_id, "acct_demo");
140
+ const agentId = normalizeString(payload.agent_id, null);
141
+ const budgetId = normalizeString(payload.budget_id, null);
142
+ const packId = normalizeString(payload.pack_id, null);
143
+ const entitlementId = normalizeString(payload.entitlement_id, null);
144
+ const createdAtIso = nowIso();
145
+ const expiresAtIso = normalizeString(payload.expires_at_iso, null);
146
+ // Optional cryptographic identity binding: an ed25519 agent public key. When
147
+ // set, the credential can only be USED with a fresh signature from that key.
148
+ // Only a VALID key is bound — an unparseable key is ignored (no brick / no
149
+ // inconsistent identity_bound=true with a null ref); routes should reject an
150
+ // invalid key up front so the operator sees the error.
151
+ const agentPublicKeyRaw = normalizeString(payload.agent_public_key, null);
152
+ const agentKeyMaterial = agentPublicKeyRaw ? normalizeAgentPublicKey(agentPublicKeyRaw) : null;
153
+ const agentPublicKey = agentKeyMaterial ? agentPublicKeyRaw : null;
154
+ const agentKeyRef = agentKeyMaterial ? agentKeyMaterial.key_ref : null;
155
+ const credential = {
156
+ credential_id: credentialId,
157
+ bearer_token: normalizeString(payload.bearer_token, `xytara_spend_${crypto.randomBytes(18).toString("hex")}`),
158
+ agent_public_key: agentPublicKey,
159
+ agent_key_ref: agentKeyRef,
160
+ account_id: accountId,
161
+ agent_id: agentId,
162
+ budget_id: budgetId,
163
+ pack_id: packId,
164
+ entitlement_id: entitlementId,
165
+ spend_posture: agentId || budgetId ? "bounded_spend" : "open_spend",
166
+ scope: normalizeString(payload.scope, "runtime_spend"),
167
+ labels: ensureArray(payload.labels).map((value) => String(value || "").trim()).filter(Boolean),
168
+ status: normalizeString(payload.status, "active"),
169
+ issued_by: normalizeString(payload.issued_by, "operator"),
170
+ expires_at_iso: expiresAtIso,
171
+ created_at_iso: createdAtIso,
172
+ updated_at_iso: createdAtIso,
173
+ revoked_at_iso: null,
174
+ revoke_reason: null
175
+ };
176
+ state.accountSpendCredentials.set(credentialId, credential);
177
+ state.accountSpendCredentialsByToken.set(credential.bearer_token, credentialId);
178
+ return buildCredentialPublicView(credential, true);
179
+ }
180
+
181
+ function createAuthorityBinding(state, body) {
182
+ const payload = ensureObject(body);
183
+ const bindingId = normalizeString(payload.binding_id, `authority_binding_${state.accountAuthorityBindings.size + 1}`);
184
+ const credential = createSpendCredential(state, {
185
+ credential_id: normalizeString(payload.credential_id, `spend_for_${bindingId}`),
186
+ bearer_token: normalizeString(payload.bearer_token, null),
187
+ agent_public_key: normalizeString(payload.agent_public_key, null),
188
+ account_id: normalizeString(payload.account_id, "acct_demo"),
189
+ agent_id: normalizeString(payload.agent_id, null),
190
+ budget_id: normalizeString(payload.budget_id, null),
191
+ pack_id: normalizeString(payload.pack_id, null),
192
+ entitlement_id: normalizeString(payload.entitlement_id, null),
193
+ scope: normalizeString(payload.scope, "runtime_spend"),
194
+ labels: ensureArray(payload.labels),
195
+ status: normalizeString(payload.status, "active"),
196
+ issued_by: normalizeString(payload.issued_by, "operator"),
197
+ expires_at_iso: normalizeString(payload.expires_at_iso, null)
198
+ });
199
+ const createdAtIso = nowIso();
200
+ const binding = {
201
+ binding_id: bindingId,
202
+ account_id: credential.account_id,
203
+ credential_id: credential.credential_id,
204
+ authority_kind: normalizeString(payload.authority_kind, "delegated_runtime_spend"),
205
+ spend_posture: credential.spend_posture,
206
+ scope: credential.scope,
207
+ authority_scope: ensureArray(payload.authority_scope).map((value) => String(value || "").trim()).filter(Boolean),
208
+ allowed_consequence_families: ensureArray(payload.allowed_consequence_families).map((value) => String(value || "").trim()).filter(Boolean),
209
+ consequence_bounds: {
210
+ max_commit_units: Number.isFinite(Number(payload.max_commit_units)) ? Number(payload.max_commit_units) : null,
211
+ max_reserve_units: Number.isFinite(Number(payload.max_reserve_units)) ? Number(payload.max_reserve_units) : null,
212
+ max_reversal_units: Number.isFinite(Number(payload.max_reversal_units)) ? Number(payload.max_reversal_units) : null
213
+ },
214
+ agent_id: credential.agent_id,
215
+ budget_id: credential.budget_id,
216
+ pack_id: credential.pack_id || null,
217
+ entitlement_id: credential.entitlement_id || null,
218
+ status: normalizeString(payload.status, "active"),
219
+ issued_by: normalizeString(payload.issued_by, "operator"),
220
+ labels: ensureArray(payload.labels),
221
+ expires_at_iso: credential.expires_at_iso,
222
+ rotated_from_binding_id: normalizeString(payload.rotated_from_binding_id, null),
223
+ replaced_by_binding_id: normalizeString(payload.replaced_by_binding_id, null),
224
+ created_at_iso: createdAtIso,
225
+ updated_at_iso: createdAtIso,
226
+ revoked_at_iso: null,
227
+ revoke_reason: null
228
+ };
229
+ state.accountAuthorityBindings.set(bindingId, binding);
230
+ return buildAuthorityBindingPublicView(binding, state.accountSpendCredentials.get(credential.credential_id), true);
231
+ }
232
+
233
+ function listSpendCredentials(state, filters) {
234
+ const payload = ensureObject(filters);
235
+ const accountId = normalizeString(payload.account_id, null);
236
+ const status = normalizeString(payload.status, null);
237
+ return Array.from(state.accountSpendCredentials.values())
238
+ .filter((credential) => {
239
+ if (accountId && credential.account_id !== accountId) return false;
240
+ if (status && credential.status !== status) return false;
241
+ return true;
242
+ })
243
+ .map((credential) => buildCredentialPublicView(credential, false));
244
+ }
245
+
246
+ function getSpendCredential(state, credentialId) {
247
+ if (!credentialId || !state.accountSpendCredentials.has(credentialId)) return null;
248
+ return buildCredentialPublicView(state.accountSpendCredentials.get(credentialId), false);
249
+ }
250
+
251
+ function listAuthorityBindings(state, filters) {
252
+ const payload = ensureObject(filters);
253
+ const accountId = normalizeString(payload.account_id, null);
254
+ const status = normalizeString(payload.status, null);
255
+ return Array.from(state.accountAuthorityBindings.values())
256
+ .filter((binding) => {
257
+ if (accountId && binding.account_id !== accountId) return false;
258
+ if (status && binding.status !== status) return false;
259
+ return true;
260
+ })
261
+ .map((binding) => buildAuthorityBindingPublicView(binding, state.accountSpendCredentials.get(binding.credential_id), false));
262
+ }
263
+
264
+ function getAuthorityBinding(state, bindingId) {
265
+ if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
266
+ const binding = state.accountAuthorityBindings.get(bindingId);
267
+ return buildAuthorityBindingPublicView(binding, state.accountSpendCredentials.get(binding.credential_id), false);
268
+ }
269
+
270
+ function getAuthorityBindingContractSummary(state, bindingId) {
271
+ if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
272
+ const binding = state.accountAuthorityBindings.get(bindingId);
273
+ return buildAuthorityBindingContractSummary(binding, state.accountSpendCredentials.get(binding.credential_id));
274
+ }
275
+
276
+ function getAuthorityBindingRotationSummary(state, bindingId) {
277
+ if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
278
+ const binding = state.accountAuthorityBindings.get(bindingId);
279
+ return buildAuthorityBindingRotationSummary(binding, state.accountSpendCredentials.get(binding.credential_id));
280
+ }
281
+
282
+ function evaluateAuthorityBindingAction(binding, action, units) {
283
+ const target = binding && typeof binding === "object" ? binding : null;
284
+ const numericUnits = Number.isFinite(Number(units)) ? Number(units) : 0;
285
+ const allowedScopes = target ? ensureArray(target.authority_scope) : [];
286
+ const allowedFamilies = target ? ensureArray(target.allowed_consequence_families) : [];
287
+ const bounds = target && target.consequence_bounds ? target.consequence_bounds : {};
288
+
289
+ if (!target) {
290
+ return { ok: true };
291
+ }
292
+ if (target.status !== "active") {
293
+ return { ok: false, reason: "authority_binding_inactive", binding_id: target.binding_id };
294
+ }
295
+
296
+ const scopeMap = {
297
+ delegated_credit_spend: "runtime.execute",
298
+ reserve: "economics.reserve",
299
+ release: "economics.release",
300
+ reverse: "economics.reverse"
301
+ };
302
+ const familyMap = {
303
+ delegated_credit_spend: "commit",
304
+ reserve: "reserve",
305
+ release: "release",
306
+ reverse: "reverse"
307
+ };
308
+ const boundKeyMap = {
309
+ delegated_credit_spend: "max_commit_units",
310
+ reserve: "max_reserve_units",
311
+ reverse: "max_reversal_units"
312
+ };
313
+
314
+ const requiredScope = scopeMap[action] || null;
315
+ if (requiredScope && allowedScopes.length > 0 && !allowedScopes.includes(requiredScope)) {
316
+ return {
317
+ ok: false,
318
+ reason: "authority_scope_not_allowed",
319
+ binding_id: target.binding_id,
320
+ required_scope: requiredScope,
321
+ authority_scope: allowedScopes
322
+ };
323
+ }
324
+
325
+ const requiredFamily = familyMap[action] || null;
326
+ if (requiredFamily && allowedFamilies.length > 0 && !allowedFamilies.includes(requiredFamily)) {
327
+ return {
328
+ ok: false,
329
+ reason: "authority_consequence_family_not_allowed",
330
+ binding_id: target.binding_id,
331
+ required_consequence_family: requiredFamily,
332
+ allowed_consequence_families: allowedFamilies
333
+ };
334
+ }
335
+
336
+ const boundKey = boundKeyMap[action] || null;
337
+ if (boundKey && Number.isFinite(Number(bounds[boundKey])) && numericUnits > Number(bounds[boundKey])) {
338
+ return {
339
+ ok: false,
340
+ reason: "authority_consequence_bound_exceeded",
341
+ binding_id: target.binding_id,
342
+ bound_key: boundKey,
343
+ max_units: Number(bounds[boundKey]),
344
+ attempted_units: numericUnits
345
+ };
346
+ }
347
+
348
+ return {
349
+ ok: true,
350
+ binding_id: target.binding_id,
351
+ authority_scope: allowedScopes,
352
+ allowed_consequence_families: allowedFamilies,
353
+ consequence_bounds: {
354
+ max_commit_units: Number.isFinite(Number(bounds.max_commit_units)) ? Number(bounds.max_commit_units) : null,
355
+ max_reserve_units: Number.isFinite(Number(bounds.max_reserve_units)) ? Number(bounds.max_reserve_units) : null,
356
+ max_reversal_units: Number.isFinite(Number(bounds.max_reversal_units)) ? Number(bounds.max_reversal_units) : null
357
+ }
358
+ };
359
+ }
360
+
361
+ function revokeSpendCredential(state, credentialId, body) {
362
+ if (!credentialId || !state.accountSpendCredentials.has(credentialId)) return null;
363
+ const payload = ensureObject(body);
364
+ const current = state.accountSpendCredentials.get(credentialId);
365
+ const next = {
366
+ ...current,
367
+ status: "revoked",
368
+ revoked_at_iso: nowIso(),
369
+ revoke_reason: normalizeString(payload.reason, "operator_revoked"),
370
+ updated_at_iso: nowIso()
371
+ };
372
+ state.accountSpendCredentials.set(credentialId, next);
373
+ return buildCredentialPublicView(next, false);
374
+ }
375
+
376
+ function revokeAuthorityBinding(state, bindingId, body) {
377
+ if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
378
+ const payload = ensureObject(body);
379
+ const current = state.accountAuthorityBindings.get(bindingId);
380
+ const next = {
381
+ ...current,
382
+ status: "revoked",
383
+ revoked_at_iso: nowIso(),
384
+ revoke_reason: normalizeString(payload.reason, "operator_revoked"),
385
+ updated_at_iso: nowIso()
386
+ };
387
+ state.accountAuthorityBindings.set(bindingId, next);
388
+ if (current.credential_id) {
389
+ revokeSpendCredential(state, current.credential_id, payload);
390
+ }
391
+ return buildAuthorityBindingPublicView(next, current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null, false);
392
+ }
393
+
394
+ function renewAuthorityBinding(state, bindingId, body) {
395
+ if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
396
+ const payload = ensureObject(body);
397
+ const current = state.accountAuthorityBindings.get(bindingId);
398
+ const nextExpiresAtIso = normalizeString(payload.expires_at_iso, current.expires_at_iso || null);
399
+ const updatedAtIso = nowIso();
400
+ const next = {
401
+ ...current,
402
+ expires_at_iso: nextExpiresAtIso,
403
+ updated_at_iso: updatedAtIso
404
+ };
405
+ state.accountAuthorityBindings.set(bindingId, next);
406
+ if (current.credential_id && state.accountSpendCredentials.has(current.credential_id)) {
407
+ const credential = state.accountSpendCredentials.get(current.credential_id);
408
+ state.accountSpendCredentials.set(current.credential_id, {
409
+ ...credential,
410
+ expires_at_iso: nextExpiresAtIso,
411
+ updated_at_iso: updatedAtIso
412
+ });
413
+ }
414
+ return buildAuthorityBindingPublicView(next, current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null, false);
415
+ }
416
+
417
+ function rotateAuthorityBinding(state, bindingId, body) {
418
+ if (!bindingId || !state.accountAuthorityBindings.has(bindingId)) return null;
419
+ const payload = ensureObject(body);
420
+ const current = state.accountAuthorityBindings.get(bindingId);
421
+ const currentCredential = current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null;
422
+ const successorBindingId = normalizeString(payload.successor_binding_id, `${bindingId}_rotated`);
423
+ const successorCredentialId = normalizeString(payload.successor_credential_id, current.credential_id ? `${current.credential_id}_rotated` : `spend_for_${successorBindingId}`);
424
+ const successor = createAuthorityBinding(state, {
425
+ binding_id: successorBindingId,
426
+ credential_id: successorCredentialId,
427
+ bearer_token: normalizeString(payload.bearer_token, null),
428
+ // Carry the cryptographic identity binding forward on rotation — otherwise a
429
+ // key-bound credential would silently downgrade to bearer-only.
430
+ agent_public_key: normalizeString(payload.agent_public_key, currentCredential ? currentCredential.agent_public_key : null),
431
+ account_id: current.account_id,
432
+ agent_id: normalizeString(payload.agent_id, current.agent_id),
433
+ budget_id: normalizeString(payload.budget_id, current.budget_id),
434
+ pack_id: normalizeString(payload.pack_id, current.pack_id),
435
+ entitlement_id: normalizeString(payload.entitlement_id, current.entitlement_id),
436
+ scope: normalizeString(payload.scope, current.scope),
437
+ authority_kind: normalizeString(payload.authority_kind, current.authority_kind),
438
+ authority_scope: ensureArray(payload.authority_scope).length > 0 ? ensureArray(payload.authority_scope) : ensureArray(current.authority_scope),
439
+ allowed_consequence_families: ensureArray(payload.allowed_consequence_families).length > 0 ? ensureArray(payload.allowed_consequence_families) : ensureArray(current.allowed_consequence_families),
440
+ max_commit_units: Number.isFinite(Number(payload.max_commit_units)) ? Number(payload.max_commit_units) : current.consequence_bounds.max_commit_units,
441
+ max_reserve_units: Number.isFinite(Number(payload.max_reserve_units)) ? Number(payload.max_reserve_units) : current.consequence_bounds.max_reserve_units,
442
+ max_reversal_units: Number.isFinite(Number(payload.max_reversal_units)) ? Number(payload.max_reversal_units) : current.consequence_bounds.max_reversal_units,
443
+ labels: ensureArray(payload.labels).length > 0 ? ensureArray(payload.labels) : ensureArray(current.labels),
444
+ issued_by: normalizeString(payload.issued_by, current.issued_by || "operator"),
445
+ expires_at_iso: normalizeString(payload.expires_at_iso, current.expires_at_iso || (currentCredential ? currentCredential.expires_at_iso : null)),
446
+ rotated_from_binding_id: current.binding_id
447
+ });
448
+ const revoked = revokeAuthorityBinding(state, bindingId, {
449
+ reason: normalizeString(payload.reason, "rotated_to_successor")
450
+ });
451
+ const successorRecord = state.accountAuthorityBindings.get(successor.binding_id);
452
+ state.accountAuthorityBindings.set(successor.binding_id, {
453
+ ...successorRecord,
454
+ rotated_from_binding_id: current.binding_id
455
+ });
456
+ state.accountAuthorityBindings.set(bindingId, {
457
+ ...state.accountAuthorityBindings.get(bindingId),
458
+ replaced_by_binding_id: successor.binding_id
459
+ });
460
+ return {
461
+ rotated_binding: buildAuthorityBindingPublicView(state.accountAuthorityBindings.get(bindingId), current.credential_id ? state.accountSpendCredentials.get(current.credential_id) : null, false),
462
+ successor_authority_binding: buildAuthorityBindingPublicView(state.accountAuthorityBindings.get(successor.binding_id), state.accountSpendCredentials.get(successor.credential_id), true),
463
+ previous_view: revoked
464
+ };
465
+ }
466
+
467
+ function resolveSpendCredential(state, bearerToken, proof, requestBinding) {
468
+ const token = normalizeString(bearerToken, null);
469
+ if (!token) {
470
+ return {
471
+ ok: false,
472
+ reason: "missing_spend_credential"
473
+ };
474
+ }
475
+ const credentialId = state.accountSpendCredentialsByToken.get(token);
476
+ if (!credentialId || !state.accountSpendCredentials.has(credentialId)) {
477
+ return {
478
+ ok: false,
479
+ reason: "spend_credential_not_found"
480
+ };
481
+ }
482
+ const credential = state.accountSpendCredentials.get(credentialId);
483
+ if (credential.status !== "active") {
484
+ return {
485
+ ok: false,
486
+ reason: "spend_credential_inactive",
487
+ credential: buildCredentialPublicView(credential, false)
488
+ };
489
+ }
490
+ if (isCredentialExpired(credential)) {
491
+ return {
492
+ ok: false,
493
+ reason: "spend_credential_expired",
494
+ credential: buildCredentialPublicView(credential, false)
495
+ };
496
+ }
497
+ // Cryptographic identity gate: a key-bound credential requires a fresh signed
498
+ // proof of control of the agent key — the bearer token alone is not enough.
499
+ if (credential.agent_public_key) {
500
+ // Bind the proof to the actual request (account/amount/task) when the caller supplies a
501
+ // request binding. `require` is set BY THE CALLER (requestBinding.require) so strict mode
502
+ // applies only where a binding is actually supplied — the execute path. The reserve/
503
+ // release/reverse lifecycle paths pass no binding, so they are unaffected by strict mode
504
+ // (and remain capped by the authority-binding bounds). Default keeps back-compat: an
505
+ // unbound proof still authorizes.
506
+ const requireBinding = Boolean(requestBinding && requestBinding.require);
507
+ const verdict = verifySpendAuthProof(credential.agent_public_key, credential.credential_id, proof || {}, {
508
+ requestBinding: requestBinding || null,
509
+ requireBinding
510
+ });
511
+ if (!verdict.ok) {
512
+ return {
513
+ ok: false,
514
+ reason: verdict.reason,
515
+ credential: buildCredentialPublicView(credential, false)
516
+ };
517
+ }
518
+ // Replay protection is MANDATORY for a key-bound credential — fail closed if
519
+ // the replay store is unavailable rather than silently skipping it.
520
+ const replayStore = state && state.acceptedAgentSignatures instanceof Map ? state.acceptedAgentSignatures : null;
521
+ if (!replayStore) {
522
+ return {
523
+ ok: false,
524
+ reason: "agent_replay_store_unavailable",
525
+ credential: buildCredentialPublicView(credential, false)
526
+ };
527
+ }
528
+ const now = Date.now();
529
+ // Opportunistic prune: a signature past its freshness window can never be a
530
+ // valid replay again, so forget it (bounds memory + snapshot size). The key is
531
+ // the canonical decoded-signature fingerprint (re-encoding cannot evade it).
532
+ for (const [key, expiresAt] of replayStore) {
533
+ if (!Number.isFinite(expiresAt) || expiresAt <= now) replayStore.delete(key);
534
+ }
535
+ if (replayStore.has(verdict.canonical)) {
536
+ return {
537
+ ok: false,
538
+ reason: "agent_signature_replayed",
539
+ credential: buildCredentialPublicView(credential, false)
540
+ };
541
+ }
542
+ replayStore.set(verdict.canonical, Number.isFinite(verdict.expires_at_ms) ? verdict.expires_at_ms : now + 120000);
543
+ }
544
+ const binding = Array.from(state.accountAuthorityBindings.values()).find((entry) => entry && entry.credential_id === credentialId && entry.status === "active") || null;
545
+ return {
546
+ ok: true,
547
+ credential,
548
+ binding,
549
+ authorization_context: {
550
+ verification_mode: "delegated_spend_credential",
551
+ wallet_id: credential.credential_id,
552
+ payment_payload: {
553
+ binding_id: binding ? binding.binding_id : null,
554
+ credential_id: credential.credential_id,
555
+ spend_posture: credential.spend_posture,
556
+ pack_id: credential.pack_id || null,
557
+ entitlement_id: credential.entitlement_id || null,
558
+ authority_scope: binding ? ensureArray(binding.authority_scope) : [],
559
+ allowed_consequence_families: binding ? ensureArray(binding.allowed_consequence_families) : [],
560
+ consequence_bounds: binding && binding.consequence_bounds ? binding.consequence_bounds : {
561
+ max_commit_units: null,
562
+ max_reserve_units: null,
563
+ max_reversal_units: null
564
+ }
565
+ }
566
+ }
567
+ };
568
+ }
569
+
570
+ module.exports = {
571
+ createAuthorityBinding,
572
+ createSpendCredential,
573
+ evaluateAuthorityBindingAction,
574
+ getAuthorityBinding,
575
+ getAuthorityBindingContractSummary,
576
+ getAuthorityBindingRotationSummary,
577
+ getSpendCredential,
578
+ listAuthorityBindings,
579
+ listSpendCredentials,
580
+ renewAuthorityBinding,
581
+ resolveSpendCredential,
582
+ rotateAuthorityBinding,
583
+ revokeAuthorityBinding,
584
+ revokeSpendCredential
585
+ };