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.
- package/.env.example +8 -1
- package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/RELEASE_NOTES.md +6 -4
- package/lib/a2a_rpc.js +280 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +722 -722
- package/lib/commerce_artifacts.js +591 -591
- package/lib/commerce_bridge.js +274 -274
- package/lib/commerce_cases.js +399 -399
- package/lib/commerce_client.js +7694 -7694
- package/lib/commerce_quotes.js +29 -0
- package/lib/commerce_runtime.js +322 -322
- package/lib/execution_dedupe.js +86 -86
- package/lib/external_credit_bridge.js +142 -142
- package/lib/http_transport.js +81 -81
- package/lib/l402_adapter.js +241 -241
- package/lib/l402_payment_front.js +265 -265
- package/lib/mpp_adapter.js +237 -237
- package/lib/payment_config.js +253 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +842 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +491 -369
- package/lib/settlement_bsv_live.js +600 -600
- package/lib/settlement_bsv_teranode.js +117 -117
- package/lib/stripe_mpp_payment_front.js +258 -258
- package/lib/x402_facilitator.js +191 -191
- package/package.json +5 -3
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +105 -105
- package/scripts/verify_agent_identity.js +153 -153
- package/scripts/verify_all.js +1 -1
- package/scripts/verify_bolt_adapter.js +128 -128
- package/scripts/verify_cdp_facilitator.js +110 -110
- package/scripts/verify_checkout_processor.js +109 -109
- package/scripts/verify_commerce_bridge.js +177 -177
- package/scripts/verify_execution_dedupe.js +189 -189
- package/scripts/verify_execution_surfacing.js +76 -76
- package/scripts/verify_final_activation.js +191 -191
- package/scripts/verify_l402_adapter.js +86 -86
- package/scripts/verify_mpp_adapter.js +128 -128
- package/scripts/verify_production_readiness.js +268 -268
- package/scripts/verify_rate_limiter.js +52 -52
- package/scripts/verify_refund_credit.js +106 -106
- package/scripts/verify_release_candidate.js +81 -81
- package/scripts/verify_runtime_state_guard.js +117 -0
- package/scripts/verify_service.js +15421 -15421
- package/scripts/verify_verify_execute_settle.js +208 -208
- package/scripts/verify_x402_facilitator.js +286 -286
- package/server.js +11976 -11974
package/lib/account_auth.js
CHANGED
|
@@ -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
|
+
};
|