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/agent_identity.js
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// Cryptographic agent identity for delegated-spend credentials.
|
|
4
|
-
//
|
|
5
|
-
// A spend credential may be bound to an agent's ed25519 public key (the same
|
|
6
|
-
// SPKI scheme xoonya's proof engine uses). When it is, presenting the bearer
|
|
7
|
-
// token is no longer sufficient: the caller must also prove control of the
|
|
8
|
-
// private key by signing a fresh challenge. This turns the credential from a
|
|
9
|
-
// pure bearer secret into a key-bound, verifiable delegated authority.
|
|
10
|
-
//
|
|
11
|
-
// Backward-compatible: a credential with no bound key keeps the existing
|
|
12
|
-
// bearer-only behavior. Dependency-free (node crypto; ed25519 verify is native).
|
|
13
|
-
|
|
14
|
-
const crypto = require("crypto");
|
|
15
|
-
|
|
16
|
-
// Accept an ed25519 public key as SPKI PEM or base64-encoded SPKI DER.
|
|
17
|
-
// Returns { key: KeyObject, spki_der_b64, key_ref } or null.
|
|
18
|
-
function normalizeAgentPublicKey(input) {
|
|
19
|
-
if (typeof input !== "string" || !input.trim()) return null;
|
|
20
|
-
const s = input.trim();
|
|
21
|
-
let key = null;
|
|
22
|
-
try {
|
|
23
|
-
if (s.includes("BEGIN PUBLIC KEY")) {
|
|
24
|
-
key = crypto.createPublicKey({ key: s, format: "pem" });
|
|
25
|
-
} else {
|
|
26
|
-
const der = Buffer.from(s.replace(/\s+/g, ""), "base64");
|
|
27
|
-
if (der.length === 0) return null;
|
|
28
|
-
key = crypto.createPublicKey({ key: der, format: "der", type: "spki" });
|
|
29
|
-
}
|
|
30
|
-
} catch (_) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
if (!key || key.asymmetricKeyType !== "ed25519") return null;
|
|
34
|
-
const spki = key.export({ type: "spki", format: "der" });
|
|
35
|
-
const keyRef = "ed25519-spki-sha256:" + crypto.createHash("sha256").update(spki).digest("hex");
|
|
36
|
-
return { key, spki_der_b64: spki.toString("base64"), key_ref: keyRef };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// The stable, public fingerprint of an agent key (matches xoonya's ref format).
|
|
40
|
-
function deriveAgentKeyRef(input) {
|
|
41
|
-
const m = normalizeAgentPublicKey(input);
|
|
42
|
-
return m ? m.key_ref : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// A deterministic digest committing a spend proof to the specific request it authorizes
|
|
46
|
-
// (account + units + task). A signature over a message carrying this digest can only be
|
|
47
|
-
// used for that exact account/amount/task — closing the "fresh signature repurposed for a
|
|
48
|
-
// different (within-cap) action" gap. Returns null when there is nothing to bind.
|
|
49
|
-
function buildSpendBindingDigest(binding) {
|
|
50
|
-
if (!binding || typeof binding !== "object") return null;
|
|
51
|
-
const toStr = (v) => (v === undefined || v === null ? "" : String(v).trim());
|
|
52
|
-
const account = toStr(binding.account_id);
|
|
53
|
-
const units = toStr(binding.units);
|
|
54
|
-
const task = toStr(binding.task_ref);
|
|
55
|
-
if (!account && !units && !task) return null;
|
|
56
|
-
// JSON array of fixed-order fields: unambiguous / injection-free (JSON escapes the field
|
|
57
|
-
// values), unlike a delimiter-joined string where a value containing the delimiter could
|
|
58
|
-
// collide two different (account, units, task) tuples onto the same digest.
|
|
59
|
-
const canonical = JSON.stringify(["naxytra-spend-binding/v1", account, units, task]);
|
|
60
|
-
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// The canonical challenge an agent signs to prove key control for a spend action. When a
|
|
64
|
-
// bindingDigest is supplied the message additionally commits to the request (account/amount/
|
|
65
|
-
// task); without it, the legacy unbound message (credential id + timestamp) is produced.
|
|
66
|
-
function buildSpendAuthMessage(credentialId, timestamp, bindingDigest) {
|
|
67
|
-
const base = `naxytra-spend-auth:${credentialId}:${timestamp}`;
|
|
68
|
-
return bindingDigest ? `${base}:${bindingDigest}` : base;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Verify an ed25519 signature (base64) over `message` against the bound key.
|
|
72
|
-
function verifyAgentSignature(publicKeyInput, message, signatureB64) {
|
|
73
|
-
const m = normalizeAgentPublicKey(publicKeyInput);
|
|
74
|
-
if (!m) return false;
|
|
75
|
-
let sig;
|
|
76
|
-
try {
|
|
77
|
-
sig = Buffer.from(String(signatureB64 || ""), "base64");
|
|
78
|
-
} catch (_) {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
if (sig.length === 0) return false;
|
|
82
|
-
try {
|
|
83
|
-
return crypto.verify(null, Buffer.from(String(message), "utf8"), m.key, sig);
|
|
84
|
-
} catch (_) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Verify a spend-auth proof for a credential that is key-bound.
|
|
90
|
-
// proof = { signature, timestamp(ms) }. Returns
|
|
91
|
-
// { ok:true, canonical, message } | { ok:false, reason }.
|
|
92
|
-
// Freshness is bounded in BOTH directions (too old, or too far in the future).
|
|
93
|
-
// `canonical` is the decoded-signature fingerprint callers MUST use as the
|
|
94
|
-
// single-use replay key (so a re-encoded base64 of the same bytes cannot slip
|
|
95
|
-
// past the guard).
|
|
96
|
-
//
|
|
97
|
-
// options = { maxPastSeconds=120, maxFutureSeconds=30, requestBinding, requireBinding }.
|
|
98
|
-
// Freshness is bounded in BOTH directions. When options.requestBinding is supplied, a proof
|
|
99
|
-
// that signed the BOUND message (credential + timestamp + request digest) verifies and is
|
|
100
|
-
// reported as bound:true — that signature is usable ONLY for this exact account/amount/task,
|
|
101
|
-
// so a captured fresh signature cannot be repurposed for a different (even within-cap)
|
|
102
|
-
// request. An unbound signature still verifies (back-compat) UNLESS options.requireBinding is
|
|
103
|
-
// set (strict mode), which refuses anything not request-bound. `canonical` is the decoded-
|
|
104
|
-
// signature fingerprint callers MUST use as the single-use replay key.
|
|
105
|
-
function verifySpendAuthProof(agentPublicKey, credentialId, proof, options = {}) {
|
|
106
|
-
const opts = options && typeof options === "object" ? options : {};
|
|
107
|
-
const maxPastSeconds = Number.isFinite(opts.maxPastSeconds) ? opts.maxPastSeconds : 120;
|
|
108
|
-
const maxFutureSeconds = Number.isFinite(opts.maxFutureSeconds) ? opts.maxFutureSeconds : 30;
|
|
109
|
-
const requireBinding = Boolean(opts.requireBinding);
|
|
110
|
-
const bindingDigest = buildSpendBindingDigest(opts.requestBinding);
|
|
111
|
-
|
|
112
|
-
const signature = proof && typeof proof.signature === "string" ? proof.signature.trim() : "";
|
|
113
|
-
const timestampRaw = proof && (proof.timestamp !== undefined && proof.timestamp !== null) ? String(proof.timestamp).trim() : "";
|
|
114
|
-
if (!signature || !timestampRaw) return { ok: false, reason: "agent_signature_required" };
|
|
115
|
-
const timestamp = Number(timestampRaw);
|
|
116
|
-
if (!Number.isFinite(timestamp)) return { ok: false, reason: "agent_signature_timestamp_invalid" };
|
|
117
|
-
// Timestamp is milliseconds since epoch. Positive age = in the past.
|
|
118
|
-
const ageSeconds = (Date.now() - timestamp) / 1000;
|
|
119
|
-
if (ageSeconds > Math.max(1, maxPastSeconds)) return { ok: false, reason: "agent_signature_expired" };
|
|
120
|
-
if (ageSeconds < -Math.max(0, maxFutureSeconds)) return { ok: false, reason: "agent_signature_future" };
|
|
121
|
-
if (requireBinding && !bindingDigest) return { ok: false, reason: "spend_binding_required" };
|
|
122
|
-
|
|
123
|
-
// Prefer the request-bound message; fall back to the unbound message only when binding is
|
|
124
|
-
// not strictly required. A bound signature whose digest mismatches the actual request
|
|
125
|
-
// verifies against neither message -> rejected.
|
|
126
|
-
let message = null;
|
|
127
|
-
let bound = false;
|
|
128
|
-
if (bindingDigest) {
|
|
129
|
-
const boundMessage = buildSpendAuthMessage(credentialId, timestampRaw, bindingDigest);
|
|
130
|
-
if (verifyAgentSignature(agentPublicKey, boundMessage, signature)) {
|
|
131
|
-
message = boundMessage;
|
|
132
|
-
bound = true;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!message && !requireBinding) {
|
|
136
|
-
const unboundMessage = buildSpendAuthMessage(credentialId, timestampRaw);
|
|
137
|
-
if (verifyAgentSignature(agentPublicKey, unboundMessage, signature)) {
|
|
138
|
-
message = unboundMessage;
|
|
139
|
-
bound = false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (!message) {
|
|
143
|
-
return { ok: false, reason: requireBinding ? "agent_signature_binding_required_or_invalid" : "agent_signature_invalid" };
|
|
144
|
-
}
|
|
145
|
-
// Canonical replay key = hex of the decoded signature bytes (encoding-agnostic).
|
|
146
|
-
let canonical;
|
|
147
|
-
try {
|
|
148
|
-
canonical = Buffer.from(signature, "base64").toString("hex");
|
|
149
|
-
} catch (_) {
|
|
150
|
-
return { ok: false, reason: "agent_signature_invalid" };
|
|
151
|
-
}
|
|
152
|
-
return { ok: true, canonical, message, bound, expires_at_ms: timestamp + Math.max(1, maxPastSeconds) * 1000 };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
module.exports = {
|
|
156
|
-
normalizeAgentPublicKey,
|
|
157
|
-
deriveAgentKeyRef,
|
|
158
|
-
buildSpendAuthMessage,
|
|
159
|
-
buildSpendBindingDigest,
|
|
160
|
-
verifyAgentSignature,
|
|
161
|
-
verifySpendAuthProof
|
|
162
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Cryptographic agent identity for delegated-spend credentials.
|
|
4
|
+
//
|
|
5
|
+
// A spend credential may be bound to an agent's ed25519 public key (the same
|
|
6
|
+
// SPKI scheme xoonya's proof engine uses). When it is, presenting the bearer
|
|
7
|
+
// token is no longer sufficient: the caller must also prove control of the
|
|
8
|
+
// private key by signing a fresh challenge. This turns the credential from a
|
|
9
|
+
// pure bearer secret into a key-bound, verifiable delegated authority.
|
|
10
|
+
//
|
|
11
|
+
// Backward-compatible: a credential with no bound key keeps the existing
|
|
12
|
+
// bearer-only behavior. Dependency-free (node crypto; ed25519 verify is native).
|
|
13
|
+
|
|
14
|
+
const crypto = require("crypto");
|
|
15
|
+
|
|
16
|
+
// Accept an ed25519 public key as SPKI PEM or base64-encoded SPKI DER.
|
|
17
|
+
// Returns { key: KeyObject, spki_der_b64, key_ref } or null.
|
|
18
|
+
function normalizeAgentPublicKey(input) {
|
|
19
|
+
if (typeof input !== "string" || !input.trim()) return null;
|
|
20
|
+
const s = input.trim();
|
|
21
|
+
let key = null;
|
|
22
|
+
try {
|
|
23
|
+
if (s.includes("BEGIN PUBLIC KEY")) {
|
|
24
|
+
key = crypto.createPublicKey({ key: s, format: "pem" });
|
|
25
|
+
} else {
|
|
26
|
+
const der = Buffer.from(s.replace(/\s+/g, ""), "base64");
|
|
27
|
+
if (der.length === 0) return null;
|
|
28
|
+
key = crypto.createPublicKey({ key: der, format: "der", type: "spki" });
|
|
29
|
+
}
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (!key || key.asymmetricKeyType !== "ed25519") return null;
|
|
34
|
+
const spki = key.export({ type: "spki", format: "der" });
|
|
35
|
+
const keyRef = "ed25519-spki-sha256:" + crypto.createHash("sha256").update(spki).digest("hex");
|
|
36
|
+
return { key, spki_der_b64: spki.toString("base64"), key_ref: keyRef };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The stable, public fingerprint of an agent key (matches xoonya's ref format).
|
|
40
|
+
function deriveAgentKeyRef(input) {
|
|
41
|
+
const m = normalizeAgentPublicKey(input);
|
|
42
|
+
return m ? m.key_ref : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A deterministic digest committing a spend proof to the specific request it authorizes
|
|
46
|
+
// (account + units + task). A signature over a message carrying this digest can only be
|
|
47
|
+
// used for that exact account/amount/task — closing the "fresh signature repurposed for a
|
|
48
|
+
// different (within-cap) action" gap. Returns null when there is nothing to bind.
|
|
49
|
+
function buildSpendBindingDigest(binding) {
|
|
50
|
+
if (!binding || typeof binding !== "object") return null;
|
|
51
|
+
const toStr = (v) => (v === undefined || v === null ? "" : String(v).trim());
|
|
52
|
+
const account = toStr(binding.account_id);
|
|
53
|
+
const units = toStr(binding.units);
|
|
54
|
+
const task = toStr(binding.task_ref);
|
|
55
|
+
if (!account && !units && !task) return null;
|
|
56
|
+
// JSON array of fixed-order fields: unambiguous / injection-free (JSON escapes the field
|
|
57
|
+
// values), unlike a delimiter-joined string where a value containing the delimiter could
|
|
58
|
+
// collide two different (account, units, task) tuples onto the same digest.
|
|
59
|
+
const canonical = JSON.stringify(["naxytra-spend-binding/v1", account, units, task]);
|
|
60
|
+
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The canonical challenge an agent signs to prove key control for a spend action. When a
|
|
64
|
+
// bindingDigest is supplied the message additionally commits to the request (account/amount/
|
|
65
|
+
// task); without it, the legacy unbound message (credential id + timestamp) is produced.
|
|
66
|
+
function buildSpendAuthMessage(credentialId, timestamp, bindingDigest) {
|
|
67
|
+
const base = `naxytra-spend-auth:${credentialId}:${timestamp}`;
|
|
68
|
+
return bindingDigest ? `${base}:${bindingDigest}` : base;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify an ed25519 signature (base64) over `message` against the bound key.
|
|
72
|
+
function verifyAgentSignature(publicKeyInput, message, signatureB64) {
|
|
73
|
+
const m = normalizeAgentPublicKey(publicKeyInput);
|
|
74
|
+
if (!m) return false;
|
|
75
|
+
let sig;
|
|
76
|
+
try {
|
|
77
|
+
sig = Buffer.from(String(signatureB64 || ""), "base64");
|
|
78
|
+
} catch (_) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (sig.length === 0) return false;
|
|
82
|
+
try {
|
|
83
|
+
return crypto.verify(null, Buffer.from(String(message), "utf8"), m.key, sig);
|
|
84
|
+
} catch (_) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Verify a spend-auth proof for a credential that is key-bound.
|
|
90
|
+
// proof = { signature, timestamp(ms) }. Returns
|
|
91
|
+
// { ok:true, canonical, message } | { ok:false, reason }.
|
|
92
|
+
// Freshness is bounded in BOTH directions (too old, or too far in the future).
|
|
93
|
+
// `canonical` is the decoded-signature fingerprint callers MUST use as the
|
|
94
|
+
// single-use replay key (so a re-encoded base64 of the same bytes cannot slip
|
|
95
|
+
// past the guard).
|
|
96
|
+
//
|
|
97
|
+
// options = { maxPastSeconds=120, maxFutureSeconds=30, requestBinding, requireBinding }.
|
|
98
|
+
// Freshness is bounded in BOTH directions. When options.requestBinding is supplied, a proof
|
|
99
|
+
// that signed the BOUND message (credential + timestamp + request digest) verifies and is
|
|
100
|
+
// reported as bound:true — that signature is usable ONLY for this exact account/amount/task,
|
|
101
|
+
// so a captured fresh signature cannot be repurposed for a different (even within-cap)
|
|
102
|
+
// request. An unbound signature still verifies (back-compat) UNLESS options.requireBinding is
|
|
103
|
+
// set (strict mode), which refuses anything not request-bound. `canonical` is the decoded-
|
|
104
|
+
// signature fingerprint callers MUST use as the single-use replay key.
|
|
105
|
+
function verifySpendAuthProof(agentPublicKey, credentialId, proof, options = {}) {
|
|
106
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
107
|
+
const maxPastSeconds = Number.isFinite(opts.maxPastSeconds) ? opts.maxPastSeconds : 120;
|
|
108
|
+
const maxFutureSeconds = Number.isFinite(opts.maxFutureSeconds) ? opts.maxFutureSeconds : 30;
|
|
109
|
+
const requireBinding = Boolean(opts.requireBinding);
|
|
110
|
+
const bindingDigest = buildSpendBindingDigest(opts.requestBinding);
|
|
111
|
+
|
|
112
|
+
const signature = proof && typeof proof.signature === "string" ? proof.signature.trim() : "";
|
|
113
|
+
const timestampRaw = proof && (proof.timestamp !== undefined && proof.timestamp !== null) ? String(proof.timestamp).trim() : "";
|
|
114
|
+
if (!signature || !timestampRaw) return { ok: false, reason: "agent_signature_required" };
|
|
115
|
+
const timestamp = Number(timestampRaw);
|
|
116
|
+
if (!Number.isFinite(timestamp)) return { ok: false, reason: "agent_signature_timestamp_invalid" };
|
|
117
|
+
// Timestamp is milliseconds since epoch. Positive age = in the past.
|
|
118
|
+
const ageSeconds = (Date.now() - timestamp) / 1000;
|
|
119
|
+
if (ageSeconds > Math.max(1, maxPastSeconds)) return { ok: false, reason: "agent_signature_expired" };
|
|
120
|
+
if (ageSeconds < -Math.max(0, maxFutureSeconds)) return { ok: false, reason: "agent_signature_future" };
|
|
121
|
+
if (requireBinding && !bindingDigest) return { ok: false, reason: "spend_binding_required" };
|
|
122
|
+
|
|
123
|
+
// Prefer the request-bound message; fall back to the unbound message only when binding is
|
|
124
|
+
// not strictly required. A bound signature whose digest mismatches the actual request
|
|
125
|
+
// verifies against neither message -> rejected.
|
|
126
|
+
let message = null;
|
|
127
|
+
let bound = false;
|
|
128
|
+
if (bindingDigest) {
|
|
129
|
+
const boundMessage = buildSpendAuthMessage(credentialId, timestampRaw, bindingDigest);
|
|
130
|
+
if (verifyAgentSignature(agentPublicKey, boundMessage, signature)) {
|
|
131
|
+
message = boundMessage;
|
|
132
|
+
bound = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!message && !requireBinding) {
|
|
136
|
+
const unboundMessage = buildSpendAuthMessage(credentialId, timestampRaw);
|
|
137
|
+
if (verifyAgentSignature(agentPublicKey, unboundMessage, signature)) {
|
|
138
|
+
message = unboundMessage;
|
|
139
|
+
bound = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!message) {
|
|
143
|
+
return { ok: false, reason: requireBinding ? "agent_signature_binding_required_or_invalid" : "agent_signature_invalid" };
|
|
144
|
+
}
|
|
145
|
+
// Canonical replay key = hex of the decoded signature bytes (encoding-agnostic).
|
|
146
|
+
let canonical;
|
|
147
|
+
try {
|
|
148
|
+
canonical = Buffer.from(signature, "base64").toString("hex");
|
|
149
|
+
} catch (_) {
|
|
150
|
+
return { ok: false, reason: "agent_signature_invalid" };
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, canonical, message, bound, expires_at_ms: timestamp + Math.max(1, maxPastSeconds) * 1000 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
normalizeAgentPublicKey,
|
|
157
|
+
deriveAgentKeyRef,
|
|
158
|
+
buildSpendAuthMessage,
|
|
159
|
+
buildSpendBindingDigest,
|
|
160
|
+
verifyAgentSignature,
|
|
161
|
+
verifySpendAuthProof
|
|
162
|
+
};
|