@supercuts-walrus/backend 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +249 -2
  3. package/dist/audit.d.ts +52 -0
  4. package/dist/audit.js +157 -0
  5. package/dist/backend.d.ts +27 -0
  6. package/dist/backend.js +62 -0
  7. package/dist/buyer-setup.d.ts +6 -0
  8. package/dist/buyer-setup.js +241 -0
  9. package/dist/capability.d.ts +32 -0
  10. package/dist/capability.js +87 -0
  11. package/dist/claude.d.ts +44 -0
  12. package/dist/claude.js +98 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +29 -0
  15. package/dist/config.d.ts +20 -0
  16. package/dist/config.js +58 -0
  17. package/dist/dashboard.d.ts +5 -0
  18. package/dist/dashboard.js +21 -0
  19. package/dist/deal-adapter.d.ts +3 -0
  20. package/dist/deal-adapter.js +39 -0
  21. package/dist/deal-engine.d.ts +62 -0
  22. package/dist/deal-engine.js +211 -0
  23. package/dist/deal-wire.d.ts +16 -0
  24. package/dist/deal-wire.js +65 -0
  25. package/dist/deal.d.ts +79 -0
  26. package/dist/deal.js +228 -0
  27. package/dist/demo.d.ts +2 -0
  28. package/dist/demo.js +449 -0
  29. package/dist/errors.d.ts +6 -0
  30. package/dist/errors.js +13 -0
  31. package/dist/events.d.ts +27 -0
  32. package/dist/events.js +68 -0
  33. package/dist/facilitator.d.ts +21 -0
  34. package/dist/facilitator.js +173 -0
  35. package/dist/finality.d.ts +2 -0
  36. package/dist/finality.js +27 -0
  37. package/dist/hosted-exit-cli.d.ts +2 -0
  38. package/dist/hosted-exit-cli.js +37 -0
  39. package/dist/hosted-exit.d.ts +24 -0
  40. package/dist/hosted-exit.js +70 -0
  41. package/dist/hosted-seller-import.d.ts +2 -0
  42. package/dist/hosted-seller-import.js +70 -0
  43. package/dist/hosted-seller-repository.d.ts +31 -0
  44. package/dist/hosted-seller-repository.js +214 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.js +37 -0
  47. package/dist/indexer.d.ts +63 -0
  48. package/dist/indexer.js +60 -0
  49. package/dist/live-deal-ports.d.ts +41 -0
  50. package/dist/live-deal-ports.js +212 -0
  51. package/dist/live-index.d.ts +28 -0
  52. package/dist/live-index.js +149 -0
  53. package/dist/live-runtime.d.ts +82 -0
  54. package/dist/live-runtime.js +570 -0
  55. package/dist/live-seller-transport.d.ts +30 -0
  56. package/dist/live-seller-transport.js +169 -0
  57. package/dist/live-seller-verifier.d.ts +11 -0
  58. package/dist/live-seller-verifier.js +65 -0
  59. package/dist/mandate.d.ts +34 -0
  60. package/dist/mandate.js +178 -0
  61. package/dist/mcp-bootstrap.d.ts +2 -0
  62. package/dist/mcp-bootstrap.js +39 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +187 -0
  65. package/dist/memory-worker.d.ts +2 -0
  66. package/dist/memory-worker.js +38 -0
  67. package/dist/memory.d.ts +15 -0
  68. package/dist/memory.js +17 -0
  69. package/dist/memwal.d.ts +35 -0
  70. package/dist/memwal.js +83 -0
  71. package/dist/negotiation.d.ts +23 -0
  72. package/dist/negotiation.js +58 -0
  73. package/dist/notes.d.ts +11 -0
  74. package/dist/notes.js +26 -0
  75. package/dist/onboarding-doctor.d.ts +1 -0
  76. package/dist/onboarding-doctor.js +76 -0
  77. package/dist/phase3.d.ts +100 -0
  78. package/dist/phase3.js +1144 -0
  79. package/dist/procurement.d.ts +8 -0
  80. package/dist/procurement.js +19 -0
  81. package/dist/protocol-engine.d.ts +121 -0
  82. package/dist/protocol-engine.js +149 -0
  83. package/dist/prover.d.ts +28 -0
  84. package/dist/prover.js +70 -0
  85. package/dist/relayer.d.ts +32 -0
  86. package/dist/relayer.js +43 -0
  87. package/dist/remote-buyer.d.ts +67 -0
  88. package/dist/remote-buyer.js +231 -0
  89. package/dist/resource-server.d.ts +2 -0
  90. package/dist/resource-server.js +24 -0
  91. package/dist/resource.d.ts +22 -0
  92. package/dist/resource.js +55 -0
  93. package/dist/seam.d.ts +2 -0
  94. package/dist/seam.js +117 -0
  95. package/dist/seller-catalog-generate.d.ts +2 -0
  96. package/dist/seller-catalog-generate.js +68 -0
  97. package/dist/seller-database.d.ts +41 -0
  98. package/dist/seller-database.js +80 -0
  99. package/dist/seller-directory-cli.d.ts +2 -0
  100. package/dist/seller-directory-cli.js +24 -0
  101. package/dist/seller-directory.d.ts +5 -0
  102. package/dist/seller-directory.js +50 -0
  103. package/dist/seller-live-cli.d.ts +2 -0
  104. package/dist/seller-live-cli.js +53 -0
  105. package/dist/seller-provision.d.ts +2 -0
  106. package/dist/seller-provision.js +84 -0
  107. package/dist/seller-register-cli.d.ts +2 -0
  108. package/dist/seller-register-cli.js +40 -0
  109. package/dist/seller-runtime.d.ts +60 -0
  110. package/dist/seller-runtime.js +96 -0
  111. package/dist/seller-service.d.ts +34 -0
  112. package/dist/seller-service.js +416 -0
  113. package/dist/seller-state.d.ts +19 -0
  114. package/dist/seller-state.js +60 -0
  115. package/dist/seller-stdio.d.ts +6 -0
  116. package/dist/seller-stdio.js +29 -0
  117. package/dist/seller-v2-provision.d.ts +2 -0
  118. package/dist/seller-v2-provision.js +71 -0
  119. package/dist/seller-watch.d.ts +2 -0
  120. package/dist/seller-watch.js +45 -0
  121. package/dist/settlement.d.ts +95 -0
  122. package/dist/settlement.js +141 -0
  123. package/dist/store.d.ts +36 -0
  124. package/dist/store.js +193 -0
  125. package/dist/sui.d.ts +49 -0
  126. package/dist/sui.js +77 -0
  127. package/dist/tatum.d.ts +17 -0
  128. package/dist/tatum.js +83 -0
  129. package/dist/transcript.d.ts +31 -0
  130. package/dist/transcript.js +52 -0
  131. package/dist/v2-exit-relayer.d.ts +34 -0
  132. package/dist/v2-exit-relayer.js +102 -0
  133. package/dist/v2-exit-worker.d.ts +2 -0
  134. package/dist/v2-exit-worker.js +97 -0
  135. package/dist/v2-exits.d.ts +30 -0
  136. package/dist/v2-exits.js +78 -0
  137. package/dist/v2-prefund.d.ts +19 -0
  138. package/dist/v2-prefund.js +43 -0
  139. package/dist/v2-relayer-cli.d.ts +2 -0
  140. package/dist/v2-relayer-cli.js +39 -0
  141. package/dist/wallet.d.ts +32 -0
  142. package/dist/wallet.js +90 -0
  143. package/dist/workflow-memory.d.ts +22 -0
  144. package/dist/workflow-memory.js +49 -0
  145. package/dist/x402-resource-service.d.ts +34 -0
  146. package/dist/x402-resource-service.js +154 -0
  147. package/dist/x402.d.ts +46 -0
  148. package/dist/x402.js +136 -0
  149. package/package.json +60 -3
@@ -0,0 +1,21 @@
1
+ import { type Server } from "node:http";
2
+ type Json = Record<string, unknown>;
3
+ interface SuiClient {
4
+ getTransaction(options: any): Promise<any>;
5
+ simulateTransaction(options: any): Promise<any>;
6
+ executeTransaction(options: any): Promise<any>;
7
+ waitForTransaction(options: any): Promise<any>;
8
+ }
9
+ /** Non-custodial x402 v2 facilitator: it validates and relays only buyer-signed Sui bytes. */
10
+ export declare class SuiTestnetFacilitator {
11
+ private readonly key;
12
+ private readonly client;
13
+ constructor(key: string, client: SuiClient);
14
+ private authorized;
15
+ verify(value: unknown): Promise<Json>;
16
+ settle(value: unknown): Promise<Json>;
17
+ createServer(): Server;
18
+ private handle;
19
+ private send;
20
+ }
21
+ export {};
@@ -0,0 +1,173 @@
1
+ import { createServer } from "node:http";
2
+ import { timingSafeEqual } from "node:crypto";
3
+ import { SuiGrpcClient } from "@mysten/sui/grpc";
4
+ import { Transaction } from "@mysten/sui/transactions";
5
+ import { isValidTransactionSignature } from "@mysten/sui/verify";
6
+ const NETWORK = "sui:testnet";
7
+ const SUI = "0x2::sui::SUI";
8
+ /** Sui RPC may expand a type's address to 32 bytes; policy identifiers use its canonical short form. */
9
+ function canonicalType(value) {
10
+ if (typeof value !== "string")
11
+ return "";
12
+ const match = /^(0x)([0-9a-f]+)(::.+)$/i.exec(value);
13
+ if (!match)
14
+ return value;
15
+ return `0x${match[2].replace(/^0+/, "") || "0"}${match[3]}`.toLowerCase();
16
+ }
17
+ function object(value, label) {
18
+ if (!value || typeof value !== "object" || Array.isArray(value))
19
+ throw new Error(`${label} must be an object`);
20
+ return value;
21
+ }
22
+ function string(value, label, expression) {
23
+ if (typeof value !== "string" || !value || value.length > 16_384 || (expression && !expression.test(value)))
24
+ throw new Error(`${label} is invalid`);
25
+ return value;
26
+ }
27
+ function exactRequirements(value) {
28
+ const requirements = object(value, "paymentRequirements");
29
+ if (requirements.scheme !== "exact" || requirements.network !== NETWORK || requirements.asset !== SUI)
30
+ throw new Error("only sui:testnet native SUI exact payments are supported");
31
+ string(requirements.payTo, "payTo", /^0x[0-9a-f]{64}$/i);
32
+ string(requirements.amount, "amount", /^[1-9][0-9]*$/);
33
+ if (!Number.isSafeInteger(requirements.maxTimeoutSeconds) || Number(requirements.maxTimeoutSeconds) <= 0)
34
+ throw new Error("maxTimeoutSeconds is invalid");
35
+ return requirements;
36
+ }
37
+ function sameJson(left, right) { return JSON.stringify(left) === JSON.stringify(right); }
38
+ async function parsePayment(value, client) {
39
+ const body = object(value, "request");
40
+ if (body.x402Version !== 2)
41
+ throw new Error("x402Version must be 2");
42
+ const paymentPayload = object(body.paymentPayload, "paymentPayload");
43
+ if (paymentPayload.x402Version !== 2)
44
+ throw new Error("payment payload x402Version must be 2");
45
+ const requirements = exactRequirements(body.paymentRequirements);
46
+ if (!sameJson(paymentPayload.accepted, requirements))
47
+ throw new Error("accepted requirements differ from payment requirements");
48
+ const payload = object(paymentPayload.payload, "payment payload");
49
+ const signature = string(payload.signature, "transaction signature");
50
+ const transactionBase64 = string(payload.transaction, "transaction");
51
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(transactionBase64))
52
+ throw new Error("transaction must be base64");
53
+ const transaction = Buffer.from(transactionBase64, "base64");
54
+ if (!transaction.length || transaction.length > 128 * 1024 || Buffer.from(transaction).toString("base64") !== transactionBase64)
55
+ throw new Error("transaction bytes are invalid");
56
+ if (!(await isValidTransactionSignature(transaction, signature)))
57
+ throw new Error("transaction signature is invalid");
58
+ let digest;
59
+ try {
60
+ digest = await Transaction.from(transaction).getDigest({ client: client });
61
+ }
62
+ catch {
63
+ throw new Error("transaction bytes cannot be decoded");
64
+ }
65
+ return { requirements, transaction, signature, digest };
66
+ }
67
+ function transaction(result) { return result?.Transaction ?? result?.FailedTransaction; }
68
+ function assertSuccessfulPayment(result, parsed, finalized) {
69
+ const tx = transaction(result);
70
+ if (!tx)
71
+ throw new Error("transaction result is missing");
72
+ const digest = tx.digest ?? tx.effects?.transactionDigest;
73
+ if (digest !== parsed.digest)
74
+ throw new Error("transaction digest differs from the signed bytes");
75
+ if (!tx.status?.success)
76
+ throw new Error(`transaction did not complete successfully: ${String(tx.status?.error ?? "unknown error")}`);
77
+ if (finalized && !tx.checkpoint)
78
+ throw new Error("transaction has not finalized");
79
+ const payTo = String(parsed.requirements.payTo), amount = BigInt(String(parsed.requirements.amount));
80
+ const matched = (tx.balanceChanges ?? []).some((change) => change.address === payTo && canonicalType(change.coinType) === SUI.toLowerCase() && BigInt(change.amount) === amount);
81
+ if (!matched)
82
+ throw new Error("transaction does not credit the exact requested native SUI amount");
83
+ }
84
+ /** Non-custodial x402 v2 facilitator: it validates and relays only buyer-signed Sui bytes. */
85
+ export class SuiTestnetFacilitator {
86
+ key;
87
+ client;
88
+ constructor(key, client) {
89
+ this.key = key;
90
+ this.client = client;
91
+ if (key.length < 24)
92
+ throw new Error("FACILITATOR_SHARED_KEY must be at least 24 characters");
93
+ }
94
+ authorized(request) {
95
+ const provided = request.headers["x-supercuts-facilitator-key"];
96
+ const value = Array.isArray(provided) ? provided[0] : provided;
97
+ if (!value)
98
+ return false;
99
+ const actual = Buffer.from(value), expected = Buffer.from(this.key);
100
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
101
+ }
102
+ async verify(value) {
103
+ const parsed = await parsePayment(value, this.client);
104
+ let known;
105
+ try {
106
+ known = await this.client.getTransaction({ digest: parsed.digest, include: { effects: true, balanceChanges: true } });
107
+ }
108
+ catch { /* Not finalized yet; simulate the signed transaction. */ }
109
+ if (known) {
110
+ assertSuccessfulPayment(known, parsed, true);
111
+ return { isValid: true, network: NETWORK, transaction: parsed.digest, replayed: true };
112
+ }
113
+ const simulated = await this.client.simulateTransaction({ transaction: parsed.transaction, include: { effects: true, balanceChanges: true } });
114
+ assertSuccessfulPayment(simulated, parsed, false);
115
+ return { isValid: true, network: NETWORK, transaction: parsed.digest };
116
+ }
117
+ async settle(value) {
118
+ const parsed = await parsePayment(value, this.client);
119
+ let known;
120
+ try {
121
+ known = await this.client.getTransaction({ digest: parsed.digest, include: { effects: true, balanceChanges: true } });
122
+ }
123
+ catch { /* The digest has not reached a node yet. */ }
124
+ if (known) {
125
+ assertSuccessfulPayment(known, parsed, true);
126
+ return { success: true, network: NETWORK, transaction: parsed.digest, replayed: true };
127
+ }
128
+ const simulated = await this.client.simulateTransaction({ transaction: parsed.transaction, include: { effects: true, balanceChanges: true } });
129
+ assertSuccessfulPayment(simulated, parsed, false);
130
+ try {
131
+ await this.client.executeTransaction({ transaction: parsed.transaction, signatures: [parsed.signature], include: { effects: true, balanceChanges: true } });
132
+ }
133
+ catch { /* broadcast may have won a concurrent race */ }
134
+ const finalized = await this.client.waitForTransaction({ digest: parsed.digest, timeout: 60_000, include: { effects: true, balanceChanges: true } });
135
+ assertSuccessfulPayment(finalized, parsed, true);
136
+ return { success: true, network: NETWORK, transaction: parsed.digest };
137
+ }
138
+ createServer() {
139
+ return createServer((request, response) => void this.handle(request, response));
140
+ }
141
+ async handle(request, response) {
142
+ const url = new URL(request.url ?? "/", "http://localhost");
143
+ if (request.method === "GET" && url.pathname === "/health")
144
+ return this.send(response, 200, { ok: true, network: NETWORK });
145
+ if (request.method === "GET" && url.pathname === "/supported")
146
+ return this.send(response, 200, { kinds: [{ x402Version: 2, scheme: "exact", network: NETWORK, asset: SUI }] });
147
+ if (request.method !== "POST" || !["/verify", "/settle"].includes(url.pathname))
148
+ return this.send(response, 404, { error: "NOT_FOUND" });
149
+ if (!this.authorized(request))
150
+ return this.send(response, 401, { error: "UNAUTHORIZED" });
151
+ try {
152
+ const chunks = [];
153
+ for await (const chunk of request) {
154
+ chunks.push(Buffer.from(chunk));
155
+ if (Buffer.concat(chunks).length > 256 * 1024)
156
+ throw new Error("request too large");
157
+ }
158
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
159
+ this.send(response, 200, url.pathname === "/verify" ? await this.verify(body) : await this.settle(body));
160
+ }
161
+ catch (error) {
162
+ this.send(response, 400, { error: "PAYMENT_INVALID", message: error instanceof Error ? error.message : "invalid request" });
163
+ }
164
+ }
165
+ send(response, status, body) { const encoded = Buffer.from(JSON.stringify(body)); response.writeHead(status, { "content-type": "application/json", "content-length": String(encoded.length) }); response.end(encoded); }
166
+ }
167
+ if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
168
+ const key = process.env.FACILITATOR_SHARED_KEY ?? "";
169
+ const grpc = process.env.SUI_TESTNET_GRPC ?? "https://fullnode.testnet.sui.io:443";
170
+ const port = Number(process.env.PORT ?? "3000");
171
+ const facilitator = new SuiTestnetFacilitator(key, new SuiGrpcClient({ network: "testnet", baseUrl: grpc }));
172
+ facilitator.createServer().listen(port, "0.0.0.0", () => process.stdout.write(`supercuts x402 facilitator listening on ${port}\n`));
173
+ }
@@ -0,0 +1,2 @@
1
+ export declare function assertSuiFinality(result: unknown, digest: string): string;
2
+ export declare function assertTatumFinality(result: unknown, digest: string, expectedCheckpoint: string): void;
@@ -0,0 +1,27 @@
1
+ import { fail } from "./errors.js";
2
+ function checkpoint(value) {
3
+ const raw = value && typeof value === "object" ? value.sequenceNumber : value;
4
+ if (raw === undefined || raw === null)
5
+ return undefined;
6
+ try {
7
+ const parsed = BigInt(raw);
8
+ return parsed >= 0n ? parsed.toString() : undefined;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ export function assertSuiFinality(result, digest) {
15
+ const envelope = result;
16
+ const tx = envelope?.Transaction ?? envelope?.FailedTransaction;
17
+ const observed = tx?.digest ?? tx?.effects?.transactionDigest;
18
+ const sequence = checkpoint(tx?.checkpoint);
19
+ if (!tx || observed !== digest || tx.effects?.transactionDigest && tx.effects.transactionDigest !== digest || tx.status?.success !== true || !sequence)
20
+ return fail("TRANSACTION_FINALITY_FAILED", "Sui did not confirm the journaled successful transaction at a checkpoint");
21
+ return sequence;
22
+ }
23
+ export function assertTatumFinality(result, digest, expectedCheckpoint) {
24
+ const tx = result?.transaction;
25
+ if (tx?.digest !== digest || checkpoint(tx.checkpoint) !== expectedCheckpoint || tx.effects?.status?.success !== true)
26
+ fail("TATUM_TRANSACTION_MISMATCH", "Tatum did not confirm the same successful finalized transaction");
27
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { DemoEventLog, readDemoEvents } from "./events.js";
5
+ import { fail } from "./errors.js";
6
+ import { queueHostedExit } from "./hosted-exit.js";
7
+ import { errorOutput, parseArgs } from "./phase3.js";
8
+ import { SellerDatabase } from "./seller-database.js";
9
+ async function main() {
10
+ const args = parseArgs(process.argv.slice(2));
11
+ const required = (name) => typeof args[name] === "string" ? args[name] : fail("ARGUMENT_REQUIRED", `--${name} is required`);
12
+ const secret = (name) => process.env[name] ?? fail("SELLER_CONFIG_MISSING", `${name} is required in the owner environment`);
13
+ const runPath = resolve(required("run-json"));
14
+ if ((statSync(runPath).mode & 0o077) !== 0)
15
+ fail("DEMO_PERMISSIONS", "run.json must be owner-only");
16
+ const run = JSON.parse(readFileSync(runPath, "utf8"));
17
+ const runId = run.runId || fail("DEMO_RUN_INVALID", "run.json is missing runId"), eventPath = run.eventPath || fail("DEMO_RUN_INVALID", "run.json is missing the event path"), sellerId = run.addresses?.confidentialSeller || fail("DEMO_RUN_INVALID", "run.json is missing the confidential seller");
18
+ const path = resolve(eventPath);
19
+ if (!existsSync(path) || (statSync(path).mode & 0o077) !== 0)
20
+ fail("DEMO_EVENTS_MISSING", "owner-only buyer event stream must exist before seller exit");
21
+ const events = readDemoEvents(path);
22
+ if (!events.length || events.some((event) => event.runId !== runId))
23
+ fail("DEMO_EVENTS_INVALID", "event stream belongs to another run");
24
+ const sessionId = required("session-id");
25
+ if (!events.some((event) => event.stage === "artifact-delivery" && event.status === "complete" && event.publicDetails.kind === "competitor-analysis" && event.publicDetails.sessionId === sessionId))
26
+ fail("DEMO_DELIVERY_MISSING", "buyer has not recorded the confidential artifact delivery for this session");
27
+ const windowStart = Number(required("window-start")), windowEnd = Number(required("window-end"));
28
+ if (!Number.isSafeInteger(windowStart) || !Number.isSafeInteger(windowEnd) || windowEnd <= windowStart || windowEnd - windowStart > 7 * 24 * 60 * 60_000)
29
+ fail("EXIT_WINDOW_INVALID", "owner exit window must be valid and last no more than seven days");
30
+ const db = new SellerDatabase(secret("SUPABASE_URL"), secret("SUPABASE_SERVICE_ROLE_KEY"));
31
+ const queued = await queueHostedExit(db, secret("SELLER_STATE_KEY"), { sellerId, runId, sessionId, windowStart, windowEnd });
32
+ if (!events.some((event) => event.actor === "confidential-seller" && event.stage === "delayed-exit" && event.publicDetails.operationId === queued.operationId)) {
33
+ new DemoEventLog(path, runId).append({ actor: "confidential-seller", stage: "delayed-exit", status: "complete", publicDetails: { state: "queued", sessionId, operationId: queued.operationId, amount: queued.amount, executeAt: queued.executeAt, windowEnd: queued.windowEnd }, evidenceHashes: [], explorerLinks: [] });
34
+ }
35
+ return { runId, eventPath: path, sessionId, sellerId: queued.sellerId, operationId: queued.operationId, status: queued.status, amount: queued.amount, executeAt: queued.executeAt, windowEnd: queued.windowEnd };
36
+ }
37
+ main().then((data) => process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.demo.v1", ok: true, command: "hosted-exit", data })}\n`)).catch((error) => { process.stdout.write(`${JSON.stringify(errorOutput(error))}\n`); process.exitCode = 1; });
@@ -0,0 +1,24 @@
1
+ import { SellerDatabase } from "./seller-database.js";
2
+ export declare const hostedExitOperationId: (sessionId: string) => string;
3
+ export interface HostedExitView {
4
+ sellerId: string;
5
+ runId: string;
6
+ sessionId: string;
7
+ operationId: string;
8
+ status: "queued" | "submitted" | "finalized";
9
+ amount: string;
10
+ executeAt: number;
11
+ windowEnd: number;
12
+ }
13
+ export declare function queueHostedExit(db: SellerDatabase, stateKey: string, input: {
14
+ sellerId: string;
15
+ runId: string;
16
+ sessionId: string;
17
+ windowStart: number;
18
+ windowEnd: number;
19
+ }): Promise<HostedExitView>;
20
+ export declare function inspectHostedExit(db: SellerDatabase, stateKey: string, input: {
21
+ sellerId: string;
22
+ runId: string;
23
+ sessionId: string;
24
+ }): Promise<HostedExitView>;
@@ -0,0 +1,70 @@
1
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
2
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fail } from "./errors.js";
7
+ import { executeMandated, loadMandate } from "./mandate.js";
8
+ import { Phase3App } from "./phase3.js";
9
+ import { captureSellerHome, decryptSellerSnapshot, encryptSellerSnapshot, restoreSellerHome } from "./seller-state.js";
10
+ import { V2ExitQueue } from "./v2-exits.js";
11
+ const STANDARD_EXIT_MIST = 1000000n;
12
+ const validId = (value) => /^[a-zA-Z0-9._:-]{1,128}$/.test(value);
13
+ export const hostedExitOperationId = (sessionId) => `remote-exit-${sessionId}`;
14
+ async function withSeller(db, stateKey, sellerId, use) {
15
+ const stored = await db.getSnapshot(sellerId) ?? fail("SELLER_STATE_MISSING", "confidential seller snapshot is missing");
16
+ const snapshot = decryptSellerSnapshot(stored.payload, stateKey);
17
+ if (snapshot.sellerId !== sellerId)
18
+ fail("SELLER_IDENTITY_MISMATCH", "seller snapshot belongs to another role");
19
+ const root = mkdtempSync(join(tmpdir(), "supercuts-hosted-exit-")), home = join(root, "seller");
20
+ let app;
21
+ try {
22
+ restoreSellerHome(home, snapshot);
23
+ app = new Phase3App(home);
24
+ if (app.profile.role !== "seller" || app.profile.mock || app.profile.verifierVersion !== 2)
25
+ fail("LIVE_PROFILE_REQUIRED", "hosted exit needs the confidential seller's live v2 role");
26
+ const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing from its own snapshot");
27
+ const decoded = decodeSuiPrivateKey(encoded);
28
+ if (decoded.scheme !== "ED25519" || Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress().toLowerCase() !== sellerId.toLowerCase())
29
+ fail("SELLER_IDENTITY_MISMATCH", "seller snapshot signer differs from the configured seller");
30
+ return await use(app, home, stored.revision, snapshot.artifacts);
31
+ }
32
+ finally {
33
+ app?.close();
34
+ rmSync(root, { recursive: true, force: true });
35
+ }
36
+ }
37
+ function matchingPayment(app, sessionId, runId) {
38
+ const delivery = app.store.db.prepare("SELECT run_id FROM remote_deliveries WHERE session_id=?").get(sessionId);
39
+ if (!delivery || delivery.run_id !== runId)
40
+ fail("DELIVERY_NOT_FOUND", "seller has no delivered analysis for this run and session");
41
+ const note = app.store.db.prepare("SELECT commitment FROM notes WHERE operation_id=? AND role='payment'").get(sessionId) ?? fail("NOTE_NOT_FOUND", "seller has not recovered the matching payment note");
42
+ return note.commitment;
43
+ }
44
+ function view(app, sellerId, runId, sessionId) {
45
+ const commitment = matchingPayment(app, sessionId, runId);
46
+ const operationId = hostedExitOperationId(sessionId);
47
+ const queued = new V2ExitQueue(app.store).get(operationId) ?? fail("DEMO_EVIDENCE_MISMATCH", "seller has no queued standard exit");
48
+ if (queued.commitment !== commitment || queued.exactExit || queued.amount !== STANDARD_EXIT_MIST || !app.store.getSecret(`v2-exit-request:${operationId}`))
49
+ fail("DEMO_EVIDENCE_MISMATCH", "seller has no prepared standard exit for the recovered payment note");
50
+ return { sellerId, runId, sessionId, operationId, status: queued.status, amount: queued.amount.toString(), executeAt: queued.executeAt, windowEnd: queued.windowEnd };
51
+ }
52
+ export async function queueHostedExit(db, stateKey, input) {
53
+ if (!validId(input.runId) || !validId(input.sessionId) || !/^0x[0-9a-f]{64}$/i.test(input.sellerId))
54
+ fail("SELLER_REQUEST_INVALID", "seller, run, or session ID is invalid");
55
+ return withSeller(db, stateKey, input.sellerId, async (app, home, revision, artifacts) => {
56
+ const commitment = matchingPayment(app, input.sessionId, input.runId), operationId = hostedExitOperationId(input.sessionId);
57
+ if (!new V2ExitQueue(app.store).get(operationId)) {
58
+ if (!Number.isSafeInteger(input.windowStart) || input.windowStart < Date.now() + 60_000)
59
+ fail("EXIT_WINDOW_INVALID", "standard delayed exit must start at least one minute from now");
60
+ await executeMandated(app, loadMandate(home), "wallet.exit.queue", { "operation-id": operationId, commitment, amount: STANDARD_EXIT_MIST.toString(), "window-start": String(input.windowStart), "window-end": String(input.windowEnd) });
61
+ await db.putSnapshot(encryptSellerSnapshot(await captureSellerHome(home, input.sellerId, artifacts), stateKey), revision);
62
+ }
63
+ return view(app, input.sellerId, input.runId, input.sessionId);
64
+ });
65
+ }
66
+ export async function inspectHostedExit(db, stateKey, input) {
67
+ if (!validId(input.runId) || !validId(input.sessionId) || !/^0x[0-9a-f]{64}$/i.test(input.sellerId))
68
+ fail("SELLER_REQUEST_INVALID", "seller, run, or session ID is invalid");
69
+ return withSeller(db, stateKey, input.sellerId, async (app) => view(app, input.sellerId, input.runId, input.sessionId));
70
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
6
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
7
+ import { fail } from "./errors.js";
8
+ import { HostedSellerRepository } from "./hosted-seller-repository.js";
9
+ import { Phase3App } from "./phase3.js";
10
+ import { SellerDatabase } from "./seller-database.js";
11
+ import { decryptSellerSnapshot, restoreSellerHome } from "./seller-state.js";
12
+ const required = (name) => process.env[name] ?? fail("SELLER_CONFIG_MISSING", `${name} is required`);
13
+ async function main() {
14
+ if (process.argv[2] !== "--writes-paused")
15
+ fail("SELLER_IMPORT_MAINTENANCE_REQUIRED", "pause hosted seller writes and pass --writes-paused");
16
+ const legacy = new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY"));
17
+ const repo = new HostedSellerRepository(required("SUPABASE_DB_URL"), required("SELLER_STATE_KEY"));
18
+ try {
19
+ const catalog = await legacy.listCatalog();
20
+ const sellerIds = [...new Set(catalog.map((row) => String(row.seller_id)))].sort();
21
+ if (!sellerIds.length)
22
+ fail("SELLER_STATE_MISSING", "catalog contains no hosted sellers");
23
+ const claimsBefore = await legacy.listPaymentClaims();
24
+ await repo.verifyLegacyCollections(catalog, claimsBefore);
25
+ const results = [];
26
+ const revisions = new Map();
27
+ for (const sellerId of sellerIds) {
28
+ const stored = await legacy.getSnapshot(sellerId) ?? fail("SELLER_STATE_MISSING", `snapshot is missing for ${sellerId}`);
29
+ revisions.set(sellerId, stored.revision);
30
+ const snapshot = decryptSellerSnapshot(stored.payload, required("SELLER_STATE_KEY"));
31
+ const root = mkdtempSync(join(tmpdir(), "supercuts-seller-verify-")), home = join(root, "seller");
32
+ let app;
33
+ try {
34
+ restoreSellerHome(home, snapshot);
35
+ app = new Phase3App(home);
36
+ if (app.profile.mock || app.profile.role !== "seller")
37
+ fail("LIVE_PROFILE_REQUIRED", "import requires a live seller profile");
38
+ const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
39
+ const decoded = decodeSuiPrivateKey(encoded);
40
+ if (decoded.scheme !== "ED25519" || Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress().toLowerCase() !== sellerId.toLowerCase())
41
+ fail("SELLER_IDENTITY_MISMATCH", "snapshot signer differs from catalog seller");
42
+ }
43
+ finally {
44
+ app?.close();
45
+ rmSync(root, { recursive: true, force: true });
46
+ }
47
+ const imported = await repo.importSnapshot(snapshot, stored.revision);
48
+ const verified = await repo.verifyImport(sellerId);
49
+ if (verified.sourceHash !== imported.sourceHash || Object.values(verified.records).reduce((sum, count) => sum + count, 0) !== imported.records || verified.artifacts !== imported.artifacts)
50
+ fail("SELLER_IMPORT_MISMATCH", "seller readback differs from source snapshot");
51
+ results.push({ sellerId, capabilities: catalog.filter((row) => row.seller_id === sellerId).length, records: imported.records, artifacts: imported.artifacts, rowTypes: verified.records });
52
+ }
53
+ const claimsAfter = await legacy.listPaymentClaims();
54
+ if (JSON.stringify(claimsBefore) !== JSON.stringify(claimsAfter))
55
+ fail("SELLER_IMPORT_MISMATCH", "payment claims changed during migration");
56
+ if (JSON.stringify(catalog) !== JSON.stringify(await legacy.listCatalog()))
57
+ fail("SELLER_IMPORT_MISMATCH", "catalog changed during migration");
58
+ await repo.verifyLegacyCollections(catalog, claimsAfter);
59
+ for (const sellerId of sellerIds) {
60
+ const current = await legacy.getSnapshot(sellerId);
61
+ if (!current || current.revision !== revisions.get(sellerId))
62
+ fail("SELLER_IMPORT_MISMATCH", "seller snapshot changed during migration");
63
+ }
64
+ process.stdout.write(`${JSON.stringify({ ok: true, sellers: results, paymentClaims: claimsAfter.length, serviceCutoverReady: false })}\n`);
65
+ }
66
+ finally {
67
+ await repo.close();
68
+ }
69
+ }
70
+ main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller import failed"}\n`); process.exitCode = 1; });
@@ -0,0 +1,31 @@
1
+ import type { SellerSnapshot } from "./seller-state.js";
2
+ /** Direct Postgres connection used only by owner-side migration and seller processes. */
3
+ export declare class HostedSellerRepository {
4
+ #private;
5
+ private readonly wrappingKey;
6
+ constructor(connectionString: string, wrappingKey: string);
7
+ close(): Promise<void>;
8
+ verifyLegacyCollections(catalog: Array<Record<string, unknown>>, claims: Array<{
9
+ digest: string;
10
+ request_id: string;
11
+ seller_id: string;
12
+ }>): Promise<void>;
13
+ /** Durable intent; the caller performs the network action after this returns. */
14
+ claimOperation(sellerId: string, operationId: string, action: string, inputHash: string): Promise<"prepared" | "unresolved" | "confirmed">;
15
+ transitionOperation(sellerId: string, operationId: string, status: "unresolved" | "confirmed", evidenceHash?: string): Promise<void>;
16
+ /** Payment digest and request ID are both unique across all hosted sellers. */
17
+ claimPayment(digest: string, requestId: string, sellerId: string): Promise<void>;
18
+ /** Import is one transaction. Replays must match the original source exactly. */
19
+ importSnapshot(snapshot: SellerSnapshot, sourceRevision: number): Promise<{
20
+ records: number;
21
+ artifacts: number;
22
+ sourceHash: string;
23
+ }>;
24
+ private insertRecords;
25
+ /** Readback authenticates every encrypted row and returns only safe counts. */
26
+ verifyImport(sellerId: string): Promise<{
27
+ sourceHash: string;
28
+ records: Record<string, number>;
29
+ artifacts: number;
30
+ }>;
31
+ }