@supercuts-walrus/backend 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +249 -2
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +157 -0
- package/dist/backend.d.ts +27 -0
- package/dist/backend.js +62 -0
- package/dist/buyer-setup.d.ts +6 -0
- package/dist/buyer-setup.js +241 -0
- package/dist/capability.d.ts +32 -0
- package/dist/capability.js +87 -0
- package/dist/claude.d.ts +44 -0
- package/dist/claude.js +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +58 -0
- package/dist/dashboard.d.ts +5 -0
- package/dist/dashboard.js +21 -0
- package/dist/deal-adapter.d.ts +3 -0
- package/dist/deal-adapter.js +39 -0
- package/dist/deal-engine.d.ts +62 -0
- package/dist/deal-engine.js +211 -0
- package/dist/deal-wire.d.ts +16 -0
- package/dist/deal-wire.js +65 -0
- package/dist/deal.d.ts +79 -0
- package/dist/deal.js +228 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +511 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +13 -0
- package/dist/events.d.ts +27 -0
- package/dist/events.js +68 -0
- package/dist/facilitator.d.ts +21 -0
- package/dist/facilitator.js +173 -0
- package/dist/finality.d.ts +2 -0
- package/dist/finality.js +27 -0
- package/dist/funding.d.ts +38 -0
- package/dist/funding.js +78 -0
- package/dist/hosted-exit-cli.d.ts +2 -0
- package/dist/hosted-exit-cli.js +37 -0
- package/dist/hosted-exit.d.ts +24 -0
- package/dist/hosted-exit.js +70 -0
- package/dist/hosted-seller-import.d.ts +2 -0
- package/dist/hosted-seller-import.js +70 -0
- package/dist/hosted-seller-repository.d.ts +31 -0
- package/dist/hosted-seller-repository.js +214 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/indexer.d.ts +63 -0
- package/dist/indexer.js +60 -0
- package/dist/live-deal-ports.d.ts +41 -0
- package/dist/live-deal-ports.js +212 -0
- package/dist/live-index.d.ts +28 -0
- package/dist/live-index.js +149 -0
- package/dist/live-runtime.d.ts +85 -0
- package/dist/live-runtime.js +644 -0
- package/dist/live-seller-transport.d.ts +30 -0
- package/dist/live-seller-transport.js +169 -0
- package/dist/live-seller-verifier.d.ts +11 -0
- package/dist/live-seller-verifier.js +65 -0
- package/dist/mandate.d.ts +34 -0
- package/dist/mandate.js +179 -0
- package/dist/mcp-bootstrap.d.ts +2 -0
- package/dist/mcp-bootstrap.js +61 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +207 -0
- package/dist/memory-worker.d.ts +2 -0
- package/dist/memory-worker.js +38 -0
- package/dist/memory.d.ts +15 -0
- package/dist/memory.js +17 -0
- package/dist/memwal.d.ts +35 -0
- package/dist/memwal.js +83 -0
- package/dist/negotiation.d.ts +23 -0
- package/dist/negotiation.js +58 -0
- package/dist/notes.d.ts +11 -0
- package/dist/notes.js +26 -0
- package/dist/onboarding-doctor.d.ts +1 -0
- package/dist/onboarding-doctor.js +87 -0
- package/dist/phase3.d.ts +100 -0
- package/dist/phase3.js +1174 -0
- package/dist/procurement.d.ts +8 -0
- package/dist/procurement.js +19 -0
- package/dist/protocol-engine.d.ts +121 -0
- package/dist/protocol-engine.js +149 -0
- package/dist/prover.d.ts +28 -0
- package/dist/prover.js +70 -0
- package/dist/relayer.d.ts +32 -0
- package/dist/relayer.js +43 -0
- package/dist/remote-buyer.d.ts +85 -0
- package/dist/remote-buyer.js +285 -0
- package/dist/resource-server.d.ts +2 -0
- package/dist/resource-server.js +24 -0
- package/dist/resource.d.ts +22 -0
- package/dist/resource.js +55 -0
- package/dist/role-lock.d.ts +8 -0
- package/dist/role-lock.js +54 -0
- package/dist/seam.d.ts +2 -0
- package/dist/seam.js +117 -0
- package/dist/seller-catalog-generate.d.ts +2 -0
- package/dist/seller-catalog-generate.js +68 -0
- package/dist/seller-database.d.ts +41 -0
- package/dist/seller-database.js +80 -0
- package/dist/seller-directory-cli.d.ts +2 -0
- package/dist/seller-directory-cli.js +24 -0
- package/dist/seller-directory.d.ts +5 -0
- package/dist/seller-directory.js +50 -0
- package/dist/seller-live-cli.d.ts +2 -0
- package/dist/seller-live-cli.js +53 -0
- package/dist/seller-package.d.ts +9 -0
- package/dist/seller-package.js +50 -0
- package/dist/seller-provision.d.ts +2 -0
- package/dist/seller-provision.js +84 -0
- package/dist/seller-register-cli.d.ts +2 -0
- package/dist/seller-register-cli.js +40 -0
- package/dist/seller-runtime.d.ts +60 -0
- package/dist/seller-runtime.js +96 -0
- package/dist/seller-service.d.ts +34 -0
- package/dist/seller-service.js +422 -0
- package/dist/seller-state.d.ts +19 -0
- package/dist/seller-state.js +60 -0
- package/dist/seller-stdio.d.ts +6 -0
- package/dist/seller-stdio.js +29 -0
- package/dist/seller-v2-provision.d.ts +2 -0
- package/dist/seller-v2-provision.js +71 -0
- package/dist/seller-watch.d.ts +2 -0
- package/dist/seller-watch.js +45 -0
- package/dist/settlement.d.ts +95 -0
- package/dist/settlement.js +141 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +193 -0
- package/dist/sui.d.ts +49 -0
- package/dist/sui.js +77 -0
- package/dist/tatum.d.ts +17 -0
- package/dist/tatum.js +83 -0
- package/dist/transcript.d.ts +31 -0
- package/dist/transcript.js +52 -0
- package/dist/v2-exit-relayer.d.ts +34 -0
- package/dist/v2-exit-relayer.js +102 -0
- package/dist/v2-exit-worker.d.ts +2 -0
- package/dist/v2-exit-worker.js +97 -0
- package/dist/v2-exits.d.ts +30 -0
- package/dist/v2-exits.js +78 -0
- package/dist/v2-prefund.d.ts +19 -0
- package/dist/v2-prefund.js +43 -0
- package/dist/v2-relayer-cli.d.ts +2 -0
- package/dist/v2-relayer-cli.js +39 -0
- package/dist/wallet.d.ts +32 -0
- package/dist/wallet.js +90 -0
- package/dist/workflow-memory.d.ts +22 -0
- package/dist/workflow-memory.js +49 -0
- package/dist/x402-resource-service.d.ts +36 -0
- package/dist/x402-resource-service.js +173 -0
- package/dist/x402.d.ts +46 -0
- package/dist/x402.js +136 -0
- package/package.json +60 -3
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { fail, SupercutsError } from "./errors.js";
|
|
4
|
+
import { paymentBindingBytes, PaymentConsumptionLedger, requirementsHash, verifyBinding, verifyExactPayment } from "./x402.js";
|
|
5
|
+
import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
|
|
6
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64");
|
|
7
|
+
function decode(value, code) {
|
|
8
|
+
const encoded = value ?? fail(code, "required x402 header is missing");
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64").toString());
|
|
11
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
12
|
+
fail(code, "x402 header must encode an object");
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return fail(code, "x402 header is not valid base64 JSON", error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function safeUrl(value) { try {
|
|
20
|
+
return new URL(value);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return fail("X402_CONFIG_INVALID", "x402 service URL is invalid", error);
|
|
24
|
+
} }
|
|
25
|
+
/** Seller-owned standard x402 v2 exact resource with additional request-binding replay protection. */
|
|
26
|
+
export class X402ResourceService {
|
|
27
|
+
config;
|
|
28
|
+
http;
|
|
29
|
+
persist;
|
|
30
|
+
claimExternal;
|
|
31
|
+
#ledger;
|
|
32
|
+
#artifacts = new Map();
|
|
33
|
+
#origin;
|
|
34
|
+
#facilitator;
|
|
35
|
+
#prefix;
|
|
36
|
+
constructor(store, config, http = fetch, persist, claimExternal) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.http = http;
|
|
39
|
+
this.persist = persist;
|
|
40
|
+
this.claimExternal = claimExternal;
|
|
41
|
+
this.#ledger = new PaymentConsumptionLedger(store);
|
|
42
|
+
this.#origin = safeUrl(config.origin);
|
|
43
|
+
this.#facilitator = safeUrl(config.facilitatorUrl);
|
|
44
|
+
this.#prefix = (config.pathPrefix ?? "/capability").replace(/\/$/, "");
|
|
45
|
+
if (this.#origin.protocol !== "https:" && !(this.#origin.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(this.#origin.hostname)))
|
|
46
|
+
fail("X402_CONFIG_INVALID", "resource origin must use HTTPS except on loopback");
|
|
47
|
+
if (this.#facilitator.protocol !== "https:" && !(this.#facilitator.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(this.#facilitator.hostname)))
|
|
48
|
+
fail("X402_CONFIG_INVALID", "facilitator must use HTTPS except on loopback");
|
|
49
|
+
if (!/^0x[0-9a-f]{64}$/i.test(config.payTo) || !/^0x[0-9a-f]+::[A-Za-z0-9_]+::[A-Za-z0-9_]+$/.test(config.asset) || !/^[1-9][0-9]*$/.test(config.amount))
|
|
50
|
+
fail("X402_CONFIG_INVALID", "payTo, asset, or amount is invalid");
|
|
51
|
+
if (!config.facilitatorKey || config.facilitatorKey.length < 24 || config.facilitatorKey.length > 1024)
|
|
52
|
+
fail("X402_CONFIG_INVALID", "facilitatorKey must be an owner-only secret");
|
|
53
|
+
}
|
|
54
|
+
register(requestId, operationId, body) {
|
|
55
|
+
if (!requestId || !operationId || requestId.length > 256 || operationId.length > 256 || !body.length || body.length > 10 * 1024 * 1024)
|
|
56
|
+
fail("RESOURCE_INVALID", "artifact registration is invalid");
|
|
57
|
+
const existing = this.#artifacts.get(requestId), next = Buffer.from(body);
|
|
58
|
+
if (existing && (existing.operationId !== operationId || !existing.body.equals(next)))
|
|
59
|
+
fail("RESOURCE_REQUEST_CONFLICT", "request already identifies another artifact or operation");
|
|
60
|
+
this.#artifacts.set(requestId, { requestId, operationId, body: next });
|
|
61
|
+
return { url: new URL(`${this.#prefix}/${encodeURIComponent(requestId)}`, this.#origin).toString(), artifactSha256: (awaitHash(next)) };
|
|
62
|
+
}
|
|
63
|
+
requirements(artifact) {
|
|
64
|
+
return { scheme: "exact", network: this.config.network, amount: this.config.amount, asset: this.config.asset, payTo: this.config.payTo, maxTimeoutSeconds: this.config.maxTimeoutSeconds ?? 300, extra: { requestId: artifact.requestId, operationId: artifact.operationId } };
|
|
65
|
+
}
|
|
66
|
+
resourceUrl(requestId) { return new URL(`${this.#prefix}/${encodeURIComponent(requestId)}`, this.#origin).toString(); }
|
|
67
|
+
challenge(artifact) {
|
|
68
|
+
// The live Sui exact facilitator requires the x402 nonce to be exactly
|
|
69
|
+
// 32 random bytes (hex encoded), not merely an application nonce.
|
|
70
|
+
const requirements = this.requirements(artifact), baseHash = requirementsHash(requirements), nonce = this.#ledger.challenge(artifact.requestId, baseHash, () => randomBytes(32).toString("hex"));
|
|
71
|
+
const stored = this.#ledger.getChallenge(artifact.requestId, baseHash) ?? fail("X402_CHALLENGE_MISSING", "resource challenge was not persisted");
|
|
72
|
+
const expiresAt = stored.createdAt + requirements.maxTimeoutSeconds * 1_000;
|
|
73
|
+
requirements.extra = { ...requirements.extra, nonce, expiresAt };
|
|
74
|
+
// The nonce changes the signed requirement set and is persisted per request.
|
|
75
|
+
const finalHash = requirementsHash(requirements);
|
|
76
|
+
return { x402Version: 2, resource: { url: this.resourceUrl(artifact.requestId), description: "Supercuts seller capability artifact", mimeType: "application/json" }, accepts: [{ ...requirements, extra: { ...requirements.extra, requirementsHash: finalHash } }] };
|
|
77
|
+
}
|
|
78
|
+
async handle(request, response) {
|
|
79
|
+
try {
|
|
80
|
+
if (request.method !== "GET")
|
|
81
|
+
return this.send(response, 405, Buffer.from("Method Not Allowed"), { allow: "GET" });
|
|
82
|
+
const url = new URL(request.url ?? "/", this.#origin), prefix = `${this.#prefix}/`;
|
|
83
|
+
if (!url.pathname.startsWith(prefix))
|
|
84
|
+
return this.send(response, 404, Buffer.from("Not Found"));
|
|
85
|
+
const requestId = decodeURIComponent(url.pathname.slice(prefix.length)), artifact = this.#artifacts.get(requestId);
|
|
86
|
+
if (!artifact)
|
|
87
|
+
return this.send(response, 404, Buffer.from("Not Found"));
|
|
88
|
+
const challenge = this.challenge(artifact), requirements = challenge.accepts instanceof Array ? challenge.accepts[0] : fail("X402_STATE_INVALID", "challenge omitted requirements");
|
|
89
|
+
const paymentHeader = Array.isArray(request.headers["payment-signature"]) ? request.headers["payment-signature"][0] : request.headers["payment-signature"];
|
|
90
|
+
if (!paymentHeader) {
|
|
91
|
+
await this.persist?.();
|
|
92
|
+
return this.send(response, 402, Buffer.from("Payment Required"), { "payment-required": encode(challenge) });
|
|
93
|
+
}
|
|
94
|
+
const stored = this.#ledger.getChallenge(artifact.requestId, requirementsHash(this.requirements(artifact))) ?? fail("X402_CHALLENGE_MISSING", "resource challenge was not persisted");
|
|
95
|
+
if (!this.#ledger.isDeliveryComplete(artifact.requestId) && Date.now() > stored.createdAt + requirements.maxTimeoutSeconds * 1_000)
|
|
96
|
+
fail("X402_CHALLENGE_EXPIRED", "payment challenge has expired");
|
|
97
|
+
const payload = decode(paymentHeader, "X402_PAYMENT_INVALID"), bindingValue = Array.isArray(request.headers["x-supercuts-binding"]) ? request.headers["x-supercuts-binding"][0] : request.headers["x-supercuts-binding"], binding = decode(bindingValue, "X402_REQUEST_BINDING_INVALID");
|
|
98
|
+
const offered = payload.accepted;
|
|
99
|
+
if (!offered || requirementsHash(offered) !== requirementsHash(requirements))
|
|
100
|
+
fail("X402_REQUIREMENTS_MISMATCH", "payment payload changed the challenged requirements");
|
|
101
|
+
const extra = requirements.extra, nonce = String(extra.nonce ?? ""), declaredHash = String(extra.requirementsHash ?? ""), expiresAt = Number(extra.expiresAt);
|
|
102
|
+
const unsignedRequirements = { ...requirements, extra: { requestId: extra.requestId, operationId: extra.operationId, nonce, expiresAt } };
|
|
103
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt !== stored.createdAt + requirements.maxTimeoutSeconds * 1_000 || declaredHash !== requirementsHash(unsignedRequirements))
|
|
104
|
+
fail("X402_REQUIREMENTS_MISMATCH", "challenge requirements hash or expiry is invalid");
|
|
105
|
+
verifyBinding(binding, artifact.requestId, nonce, unsignedRequirements, binding.transactionDigest);
|
|
106
|
+
const { payerSignature: _signature, ...unsignedBinding } = binding;
|
|
107
|
+
if (!(await isValidPersonalMessageSignature(paymentBindingBytes(unsignedBinding), binding.payerSignature, { address: binding.payer })))
|
|
108
|
+
fail("X402_PAYER_SIGNATURE_INVALID", "payer did not bind payment to this resource request");
|
|
109
|
+
const body = { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements };
|
|
110
|
+
const verified = await this.facilitator("verify", body);
|
|
111
|
+
if (verified.isValid !== true)
|
|
112
|
+
fail("X402_FACILITATOR_REJECTED", "facilitator rejected the signed payment");
|
|
113
|
+
const settled = await this.facilitator("settle", body), digest = String(settled.transaction ?? "");
|
|
114
|
+
if (settled.success !== true || settled.network !== this.config.network || digest !== binding.transactionDigest)
|
|
115
|
+
fail("X402_SETTLEMENT_INVALID", "facilitator returned mismatched settlement evidence");
|
|
116
|
+
const payment = { digest, network: this.config.network, asset: this.config.asset, amount: BigInt(this.config.amount), payTo: this.config.payTo, finalized: true };
|
|
117
|
+
verifyExactPayment(payment, requirements);
|
|
118
|
+
await this.claimExternal?.(digest, artifact.requestId);
|
|
119
|
+
this.#ledger.claim(payment, artifact.requestId);
|
|
120
|
+
let delivery = this.#ledger.getDelivery(artifact.requestId);
|
|
121
|
+
if (!this.#ledger.isDeliveryComplete(artifact.requestId)) {
|
|
122
|
+
if (!this.#ledger.acquireDelivery(artifact.requestId))
|
|
123
|
+
fail("DELIVERY_IN_PROGRESS", "resource delivery is already being produced");
|
|
124
|
+
this.#ledger.completeDelivery(artifact.requestId, artifact.body);
|
|
125
|
+
delivery = artifact.body;
|
|
126
|
+
}
|
|
127
|
+
await this.persist?.();
|
|
128
|
+
return this.send(response, 200, delivery ?? fail("DELIVERY_STATE_INVALID", "completed delivery has no bytes"), { "content-type": "application/json", "payment-response": encode({ success: true, transaction: digest, network: this.config.network }) });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const known = error instanceof SupercutsError ? error : new SupercutsError("INTERNAL_ERROR", "resource service failed");
|
|
132
|
+
return this.send(response, known.code === "PAYMENT_CONSUMED" ? 409 : 400, Buffer.from(JSON.stringify({ error: known.code, message: known.message })), { "content-type": "application/json" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
createServer() { return createServer((request, response) => void this.handle(request, response)); }
|
|
136
|
+
async facilitator(path, body) {
|
|
137
|
+
const endpoint = new URL(path, `${this.#facilitator.toString().replace(/\/$/, "")}/`);
|
|
138
|
+
const delays = this.config.facilitatorRetryDelaysMs ?? [0, 2_000, 5_000, 10_000];
|
|
139
|
+
let lastCause;
|
|
140
|
+
for (const delay of delays) {
|
|
141
|
+
if (delay > 0)
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
response = await this.http(endpoint, { method: "POST", headers: { "content-type": "application/json", "x-supercuts-facilitator-key": this.config.facilitatorKey }, body: JSON.stringify(body), signal: AbortSignal.timeout(60_000) });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
lastCause = error;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const raw = Buffer.from(await response.arrayBuffer());
|
|
152
|
+
let value;
|
|
153
|
+
try {
|
|
154
|
+
value = JSON.parse(raw.toString("utf8"));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
lastCause = error;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (response.ok && value && typeof value === "object" && !Array.isArray(value))
|
|
161
|
+
return value;
|
|
162
|
+
const message = value && typeof value === "object" && !Array.isArray(value) && typeof value.message === "string"
|
|
163
|
+
? `: ${value.message}` : "";
|
|
164
|
+
if (response.status < 500)
|
|
165
|
+
fail("X402_FACILITATOR_INVALID", `facilitator ${path} failed with ${response.status}${message}`);
|
|
166
|
+
lastCause = new Error(`facilitator ${path} returned ${response.status}${message}`);
|
|
167
|
+
}
|
|
168
|
+
return fail("X402_FACILITATOR_UNAVAILABLE", `facilitator ${path} remained unavailable after bounded retries`, lastCause);
|
|
169
|
+
}
|
|
170
|
+
send(response, status, body, headers = {}) { response.writeHead(status, { "content-length": String(body.length), ...headers }); response.end(body); }
|
|
171
|
+
}
|
|
172
|
+
import { createHash } from "node:crypto";
|
|
173
|
+
function awaitHash(value) { return createHash("sha256").update(value).digest("hex"); }
|
package/dist/x402.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RoleStore } from "./store.js";
|
|
2
|
+
export interface ExactRequirements {
|
|
3
|
+
scheme: "exact";
|
|
4
|
+
network: string;
|
|
5
|
+
amount: string;
|
|
6
|
+
asset: string;
|
|
7
|
+
payTo: string;
|
|
8
|
+
maxTimeoutSeconds: number;
|
|
9
|
+
extra: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface PaymentBinding {
|
|
12
|
+
requestId: string;
|
|
13
|
+
nonce: string;
|
|
14
|
+
requirementsHash: string;
|
|
15
|
+
transactionDigest: string;
|
|
16
|
+
payer: string;
|
|
17
|
+
payerSignature: string;
|
|
18
|
+
}
|
|
19
|
+
export interface FinalPayment {
|
|
20
|
+
digest: string;
|
|
21
|
+
network: string;
|
|
22
|
+
asset: string;
|
|
23
|
+
amount: bigint;
|
|
24
|
+
payTo: string;
|
|
25
|
+
finalized: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function requirementsHash(requirements: ExactRequirements): string;
|
|
28
|
+
export declare function paymentBindingBytes(binding: Omit<PaymentBinding, "payerSignature">): Uint8Array;
|
|
29
|
+
export declare class PaymentConsumptionLedger {
|
|
30
|
+
private readonly store;
|
|
31
|
+
constructor(store: RoleStore);
|
|
32
|
+
claim(payment: FinalPayment, requestId: string): void;
|
|
33
|
+
recordDelivery(requestId: string, response: Uint8Array): void;
|
|
34
|
+
getDelivery(requestId: string): Buffer | undefined;
|
|
35
|
+
isDeliveryComplete(requestId: string): boolean;
|
|
36
|
+
acquireDelivery(requestId: string, now?: number, leaseMs?: number): boolean;
|
|
37
|
+
releaseDelivery(requestId: string): void;
|
|
38
|
+
completeDelivery(requestId: string, response: Uint8Array): void;
|
|
39
|
+
challenge(requestId: string, requirementsHashValue: string, createNonce: () => string): string;
|
|
40
|
+
getChallenge(requestId: string, requirementsHashValue: string): {
|
|
41
|
+
nonce: string;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
} | undefined;
|
|
44
|
+
}
|
|
45
|
+
export declare function verifyBinding(binding: PaymentBinding, requestId: string, nonce: string, requirements: ExactRequirements, transactionDigest: string): void;
|
|
46
|
+
export declare function verifyExactPayment(payment: FinalPayment, requirements: ExactRequirements): void;
|
package/dist/x402.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { fail } from "./errors.js";
|
|
3
|
+
function canonical(value) {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
6
|
+
if (value && typeof value === "object")
|
|
7
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
|
|
8
|
+
const result = JSON.stringify(value);
|
|
9
|
+
if (result === undefined)
|
|
10
|
+
fail("X402_REQUIREMENTS_INVALID", "x402 values must be JSON-compatible");
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export function requirementsHash(requirements) { return createHash("sha256").update(canonical(requirements)).digest("hex"); }
|
|
14
|
+
export function paymentBindingBytes(binding) {
|
|
15
|
+
return Buffer.from(canonical({ domain: "supercuts-x402-binding-v1", ...binding }));
|
|
16
|
+
}
|
|
17
|
+
export class PaymentConsumptionLedger {
|
|
18
|
+
store;
|
|
19
|
+
constructor(store) {
|
|
20
|
+
this.store = store;
|
|
21
|
+
}
|
|
22
|
+
claim(payment, requestId) {
|
|
23
|
+
if (!payment.finalized)
|
|
24
|
+
fail("PAYMENT_PENDING", "payment is not finalized");
|
|
25
|
+
if (!payment.network || !payment.digest || payment.network.length > 256 || payment.digest.length > 256 || !requestId || requestId.length > 256)
|
|
26
|
+
fail("PAYMENT_ID_INVALID", "payment or request identity is invalid");
|
|
27
|
+
const key = createHash("sha256").update(canonical([payment.network, payment.digest])).digest("hex");
|
|
28
|
+
this.store.db.exec("BEGIN IMMEDIATE");
|
|
29
|
+
try {
|
|
30
|
+
const request = this.store.db.prepare("SELECT payment_key FROM deliveries WHERE request_id = ?").get(requestId);
|
|
31
|
+
if (request && request.payment_key !== key)
|
|
32
|
+
fail("REQUEST_PAYMENT_CONFLICT", "request is already bound to another payment");
|
|
33
|
+
const existing = this.store.db.prepare("SELECT request_id FROM deliveries WHERE payment_key = ?").get(key);
|
|
34
|
+
if (existing && existing.request_id !== requestId)
|
|
35
|
+
fail("PAYMENT_CONSUMED", "payment was already used for another request");
|
|
36
|
+
if (!existing)
|
|
37
|
+
this.store.db.prepare("INSERT INTO deliveries(request_id, payment_key, response, finalized) VALUES (?, ?, ?, 1)").run(requestId, key, Buffer.alloc(0));
|
|
38
|
+
this.store.db.exec("COMMIT");
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.store.db.exec("ROLLBACK");
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
recordDelivery(requestId, response) {
|
|
46
|
+
const result = this.store.db.prepare("UPDATE deliveries SET response = ?, finalized = 1 WHERE request_id = ?").run(Buffer.from(response), requestId);
|
|
47
|
+
if (result.changes !== 1)
|
|
48
|
+
fail("PAYMENT_NOT_CLAIMED", "resource cannot be delivered before payment consumption");
|
|
49
|
+
}
|
|
50
|
+
getDelivery(requestId) {
|
|
51
|
+
const row = this.store.db.prepare("SELECT response FROM deliveries WHERE request_id = ?").get(requestId);
|
|
52
|
+
return row ? Buffer.from(row.response) : undefined;
|
|
53
|
+
}
|
|
54
|
+
isDeliveryComplete(requestId) {
|
|
55
|
+
const row = this.store.db.prepare("SELECT status FROM delivery_jobs WHERE request_id = ?").get(requestId);
|
|
56
|
+
return row?.status === "delivered";
|
|
57
|
+
}
|
|
58
|
+
acquireDelivery(requestId, now = Date.now(), leaseMs = 30_000) {
|
|
59
|
+
this.store.db.exec("BEGIN IMMEDIATE");
|
|
60
|
+
try {
|
|
61
|
+
const row = this.store.db.prepare("SELECT status, lease_until FROM delivery_jobs WHERE request_id = ?").get(requestId);
|
|
62
|
+
if (row?.status === "delivered" || (row?.status === "producing" && row.lease_until > now)) {
|
|
63
|
+
this.store.db.exec("COMMIT");
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
this.store.db.prepare("INSERT INTO delivery_jobs(request_id, status, lease_until) VALUES (?, 'producing', ?) ON CONFLICT(request_id) DO UPDATE SET status='producing', lease_until=excluded.lease_until").run(requestId, now + leaseMs);
|
|
67
|
+
this.store.db.exec("COMMIT");
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.store.db.exec("ROLLBACK");
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
releaseDelivery(requestId) { this.store.db.prepare("UPDATE delivery_jobs SET status = 'pending', lease_until = 0 WHERE request_id = ? AND status = 'producing'").run(requestId); }
|
|
76
|
+
completeDelivery(requestId, response) {
|
|
77
|
+
this.store.db.exec("BEGIN IMMEDIATE");
|
|
78
|
+
try {
|
|
79
|
+
this.recordDelivery(requestId, response);
|
|
80
|
+
this.store.db.prepare("INSERT INTO delivery_jobs(request_id, status, lease_until) VALUES (?, 'delivered', 0) ON CONFLICT(request_id) DO UPDATE SET status='delivered', lease_until=0").run(requestId);
|
|
81
|
+
this.store.db.exec("COMMIT");
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.store.db.exec("ROLLBACK");
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
challenge(requestId, requirementsHashValue, createNonce) {
|
|
89
|
+
const existing = this.store.db.prepare("SELECT nonce, requirements_hash FROM x402_challenges WHERE request_id = ?").get(requestId);
|
|
90
|
+
if (existing) {
|
|
91
|
+
if (existing.requirements_hash !== requirementsHashValue)
|
|
92
|
+
fail("X402_REQUEST_CONFLICT", "request identity was reused with different requirements");
|
|
93
|
+
return existing.nonce;
|
|
94
|
+
}
|
|
95
|
+
const nonce = createNonce();
|
|
96
|
+
this.store.db.prepare("INSERT INTO x402_challenges(request_id, nonce, requirements_hash, created_at) VALUES (?, ?, ?, ?)").run(requestId, nonce, requirementsHashValue, Date.now());
|
|
97
|
+
return nonce;
|
|
98
|
+
}
|
|
99
|
+
getChallenge(requestId, requirementsHashValue) {
|
|
100
|
+
const row = this.store.db.prepare("SELECT nonce, requirements_hash, created_at FROM x402_challenges WHERE request_id = ?").get(requestId);
|
|
101
|
+
if (row && row.requirements_hash !== requirementsHashValue)
|
|
102
|
+
fail("X402_REQUEST_CONFLICT", "request identity was reused with different requirements");
|
|
103
|
+
return row ? { nonce: row.nonce, createdAt: row.created_at } : undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function verifyBinding(binding, requestId, nonce, requirements, transactionDigest) {
|
|
107
|
+
if (!binding.payer || !binding.payerSignature || binding.payer.length > 256 || binding.payerSignature.length > 2048)
|
|
108
|
+
fail("X402_REQUEST_BINDING_INVALID", "payer binding identity or signature is invalid");
|
|
109
|
+
if (binding.requestId !== requestId || binding.nonce !== nonce || binding.transactionDigest !== transactionDigest || binding.requirementsHash !== requirementsHash(requirements))
|
|
110
|
+
fail("X402_REQUEST_BINDING_INVALID", "payment does not bind to this request");
|
|
111
|
+
}
|
|
112
|
+
export function verifyExactPayment(payment, requirements) {
|
|
113
|
+
if (requirements.scheme !== "exact" || !Number.isSafeInteger(requirements.maxTimeoutSeconds) || requirements.maxTimeoutSeconds <= 0)
|
|
114
|
+
fail("X402_REQUIREMENTS_INVALID", "exact requirements need a positive integer timeout");
|
|
115
|
+
if (!requirements.network || !requirements.asset || !requirements.payTo || requirements.network.length > 256 || requirements.asset.length > 512 || requirements.payTo.length > 256)
|
|
116
|
+
fail("X402_REQUIREMENTS_INVALID", "payment network, asset, or recipient is invalid");
|
|
117
|
+
if (!payment.finalized)
|
|
118
|
+
fail("PAYMENT_PENDING", "payment is not finalized");
|
|
119
|
+
let required;
|
|
120
|
+
try {
|
|
121
|
+
required = BigInt(requirements.amount);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return fail("X402_REQUIREMENTS_INVALID", "payment amount is not an integer", error);
|
|
125
|
+
}
|
|
126
|
+
if (payment.network !== requirements.network)
|
|
127
|
+
fail("X402_NETWORK_MISMATCH", "payment used the wrong network");
|
|
128
|
+
if (payment.asset !== requirements.asset)
|
|
129
|
+
fail("X402_ASSET_MISMATCH", "payment used the wrong asset");
|
|
130
|
+
if (required <= 0n)
|
|
131
|
+
fail("X402_REQUIREMENTS_INVALID", "payment amount must be positive");
|
|
132
|
+
if (payment.amount !== required)
|
|
133
|
+
fail("X402_AMOUNT_MISMATCH", "payment used the wrong exact amount");
|
|
134
|
+
if (payment.payTo !== requirements.payTo)
|
|
135
|
+
fail("X402_PAYEE_MISMATCH", "payment used the wrong recipient");
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supercuts-walrus/backend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./seam": "./dist/seam.js",
|
|
8
|
+
"./package.json": "./package.json"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"supercuts": "dist/cli.js",
|
|
12
|
+
"supercuts-demo": "dist/demo.js",
|
|
13
|
+
"supercuts-mcp": "dist/mcp-bootstrap.js",
|
|
14
|
+
"supercuts-resource": "dist/resource-server.js",
|
|
15
|
+
"supercuts-seller-service": "dist/seller-service.js",
|
|
16
|
+
"supercuts-seller-provision": "dist/seller-provision.js",
|
|
17
|
+
"supercuts-seller-watch": "dist/seller-watch.js",
|
|
18
|
+
"supercuts-seller-catalog": "dist/seller-catalog-generate.js",
|
|
19
|
+
"supercuts-facilitator": "dist/facilitator.js",
|
|
20
|
+
"supercuts-seam": "dist/seam.js",
|
|
21
|
+
"supercuts-v2-relayer": "dist/v2-relayer-cli.js",
|
|
22
|
+
"supercuts-memory-worker": "dist/memory-worker.js",
|
|
23
|
+
"supercuts-seller-live": "dist/seller-live-cli.js",
|
|
24
|
+
"supercuts-seller-register": "dist/seller-register-cli.js",
|
|
25
|
+
"supercuts-seller-v2-provision": "dist/seller-v2-provision.js",
|
|
26
|
+
"supercuts-hosted-exit": "dist/hosted-exit-cli.js",
|
|
27
|
+
"supercuts-hosted-seller-import": "dist/hosted-seller-import.js",
|
|
28
|
+
"supercuts-seller-directory": "dist/seller-directory-cli.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.13"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@mysten-incubation/memwal": "0.1.11",
|
|
40
|
+
"@mysten/seal": "1.5.2",
|
|
41
|
+
"@mysten/sui": "2.38.0",
|
|
42
|
+
"@mysten/walrus": "1.2.37",
|
|
43
|
+
"ajv": "8.17.1",
|
|
44
|
+
"circomlibjs": "0.1.7",
|
|
45
|
+
"pg": "^8.23.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.0",
|
|
49
|
+
"@types/pg": "^8.23.1",
|
|
50
|
+
"tsx": "^4.19.2",
|
|
51
|
+
"typescript": "^5.7.2"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.build.json",
|
|
55
|
+
"check": "tsc --noEmit",
|
|
56
|
+
"test": "tsx --test test/**/*.test.ts",
|
|
57
|
+
"test:e2e": "tsx --test test/phase2.e2e.test.ts",
|
|
58
|
+
"test:phase3": "tsx --test test/phase3.e2e.test.ts",
|
|
59
|
+
"test:sdk": "pnpm --dir packages/sdk test",
|
|
60
|
+
"release:gate": "node scripts/release-gate.mjs",
|
|
61
|
+
"test:seal-live": "tsx scripts/seal-live-e2e.ts"
|
|
62
|
+
}
|
|
6
63
|
}
|