@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,416 @@
1
+ #!/usr/bin/env node
2
+ import { createCipheriv, createHash, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes } from "node:crypto";
3
+ import { createServer } from "node:http";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { BoundedClaude } from "./claude.js";
8
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
9
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
10
+ import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
11
+ import { validateCapabilityManifest, verifySignedCapability } from "./capability.js";
12
+ import { fail, SupercutsError } from "./errors.js";
13
+ import { executeMandated, loadMandate } from "./mandate.js";
14
+ import { Phase3App } from "./phase3.js";
15
+ import { decryptSessionMessage, encryptSessionMessage } from "./negotiation.js";
16
+ import { SellerDatabase } from "./seller-database.js";
17
+ import { captureSellerHome, decryptSellerSnapshot, encryptSellerSnapshot, restoreSellerHome } from "./seller-state.js";
18
+ import { X402ResourceService } from "./x402-resource-service.js";
19
+ import { LiveSellerTransport } from "./live-seller-transport.js";
20
+ import { registerDirectoryCapability } from "./seller-directory.js";
21
+ const isV2 = (row) => row.preview.genericV2 === true;
22
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
23
+ const validId = (value, name) => typeof value === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(value) ? value : fail("SELLER_REQUEST_INVALID", `${name} is invalid`);
24
+ const asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : fail("SELLER_REQUEST_INVALID", "request must be an object");
25
+ export class SellerService {
26
+ db;
27
+ snapshotKey;
28
+ origin;
29
+ facilitatorUrl;
30
+ facilitatorKey;
31
+ allowedBuyers;
32
+ directoryAggregator;
33
+ #slots = new Map();
34
+ #catalog = new Map();
35
+ #v2 = new Map();
36
+ #v2BySeller = new Map();
37
+ #root = mkdtempSync(join(tmpdir(), "supercuts-sellers-"));
38
+ #claude;
39
+ #queue = Promise.resolve();
40
+ #healthy = true;
41
+ constructor(db, snapshotKey, origin, facilitatorUrl, facilitatorKey, allowedBuyers, claudeKey, directoryAggregator) {
42
+ this.db = db;
43
+ this.snapshotKey = snapshotKey;
44
+ this.origin = origin;
45
+ this.facilitatorUrl = facilitatorUrl;
46
+ this.facilitatorKey = facilitatorKey;
47
+ this.allowedBuyers = allowedBuyers;
48
+ this.directoryAggregator = directoryAggregator;
49
+ this.#claude = new BoundedClaude({ apiKey: claudeKey });
50
+ }
51
+ static async create(config) {
52
+ const service = new SellerService(config.db, config.snapshotKey, config.origin, config.facilitatorUrl, config.facilitatorKey, config.allowedBuyers, config.claudeKey, config.directoryAggregator);
53
+ const rows = (await config.db.listCatalog()).filter((row) => !(isV2(row) && row.manifest.expiresAt <= Date.now()));
54
+ for (const row of rows) {
55
+ validateCapabilityManifest(row.manifest);
56
+ if (row.manifest.capabilityId !== row.capability_id || row.manifest.sellerId !== row.seller_id || !row.blob_id || !row.signature || !row.transport_public_key)
57
+ fail("SELLER_CATALOG_INVALID", "catalog entry does not match its manifest");
58
+ await verifySignedCapability({ manifest: row.manifest, manifestBlobId: row.blob_id, transportPublicKey: row.transport_public_key, sellerSignature: row.signature });
59
+ if (!isV2(row))
60
+ service.#catalog.set(row.capability_id, row);
61
+ }
62
+ for (const sellerId of new Set(rows.map((row) => row.seller_id))) {
63
+ const stored = await config.db.getSnapshot(sellerId) ?? fail("SELLER_STATE_MISSING", `snapshot is missing for ${sellerId}`);
64
+ const snapshot = decryptSellerSnapshot(stored.payload, config.snapshotKey), home = join(service.#root, sha256(sellerId).slice(0, 16));
65
+ restoreSellerHome(home, snapshot);
66
+ const app = new Phase3App(home);
67
+ if (app.profile.role !== "seller" || app.profile.mock)
68
+ fail("LIVE_PROFILE_REQUIRED", "remote seller requires a live seller profile");
69
+ app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_quotes(request_id TEXT PRIMARY KEY, operation_id TEXT NOT NULL, capability_id TEXT NOT NULL, amount TEXT NOT NULL, artifact_key TEXT NOT NULL, run_id TEXT NOT NULL)");
70
+ app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_deliveries(session_id TEXT PRIMARY KEY, capability_id TEXT NOT NULL, artifact_key TEXT NOT NULL, delivery_public_key TEXT NOT NULL, run_id TEXT NOT NULL)");
71
+ app.store.db.exec("CREATE TABLE IF NOT EXISTS remote_negotiations(session_id TEXT PRIMARY KEY, capability_id TEXT NOT NULL, buyer_address TEXT NOT NULL, buyer_public_key TEXT NOT NULL, request_ciphertext TEXT NOT NULL, reply_ciphertext TEXT NOT NULL, reply_signature TEXT NOT NULL, request_blob_id TEXT NOT NULL, reply_blob_id TEXT NOT NULL, transcript_hash TEXT NOT NULL, brief_hash TEXT, run_id TEXT NOT NULL)");
72
+ const slot = { sellerId, home, app, artifacts: snapshot.artifacts, revision: stored.revision, resources: new Map() };
73
+ service.#slots.set(sellerId, slot);
74
+ for (const row of rows.filter((item) => item.seller_id === sellerId && !isV2(item) && item.manifest.supportedRails.includes("x402"))) {
75
+ const resource = service.resource(slot, row);
76
+ for (const quote of app.store.db.prepare("SELECT request_id AS requestId,operation_id AS operationId,capability_id AS capabilityId,amount,artifact_key AS artifactKey,run_id AS runId FROM remote_quotes WHERE capability_id=?").all(row.capability_id)) {
77
+ if (quote.amount !== row.manifest.priceBand.minimum)
78
+ fail("SELLER_QUOTE_INVALID", "stored quote price differs from the manifest");
79
+ resource.register(quote.requestId, quote.operationId, service.artifact(slot, quote.artifactKey));
80
+ }
81
+ }
82
+ const v2Rows = rows.filter((item) => item.seller_id === sellerId && isV2(item));
83
+ if (v2Rows.length) {
84
+ if (app.profile.verifierVersion !== 2)
85
+ fail("SELLER_POOL_VERSION_MISMATCH", "hosted v2 capabilities require a separate v2 seller role home");
86
+ const transport = new LiveSellerTransport(app, { origin: config.origin, facilitatorUrl: config.facilitatorUrl, facilitatorKey: config.facilitatorKey, persist: async () => { try {
87
+ await service.persist(slot);
88
+ }
89
+ catch (error) {
90
+ service.#healthy = false;
91
+ throw error;
92
+ } }, claimPayment: (digest, requestId) => config.db.claimPayment(digest, requestId, sellerId) });
93
+ for (const row of v2Rows) {
94
+ const metadata = row.preview.outputMetadata;
95
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
96
+ fail("SELLER_CONFIG_INVALID", "v2 fixed artifact requires outputMetadata in catalog preview");
97
+ await transport.register({ manifest: row.manifest, manifestBlobId: row.blob_id, transportPublicKey: row.transport_public_key, sellerSignature: row.signature }, async () => ({ artifact: service.artifact(slot, row.capability_id), contentType: typeof row.preview.contentType === "string" ? row.preview.contentType : "application/octet-stream", metadata: metadata }));
98
+ service.#v2.set(row.capability_id, { slot, transport, row });
99
+ }
100
+ service.#v2BySeller.set(sellerId, transport);
101
+ }
102
+ }
103
+ return service;
104
+ }
105
+ artifact(slot, key) {
106
+ const encoded = slot.artifacts[key] ?? fail("SELLER_ARTIFACT_MISSING", "prepared artifact is missing");
107
+ const body = Buffer.from(encoded, "base64url");
108
+ if (!body.length || body.length > 10 * 1024 * 1024)
109
+ fail("SELLER_ARTIFACT_INVALID", "prepared artifact size is invalid");
110
+ return body;
111
+ }
112
+ resource(slot, row) {
113
+ const found = slot.resources.get(row.capability_id);
114
+ if (found)
115
+ return found;
116
+ const resource = new X402ResourceService(slot.app.store, {
117
+ origin: this.origin,
118
+ pathPrefix: `/resource/${encodeURIComponent(row.capability_id)}`,
119
+ facilitatorUrl: this.facilitatorUrl,
120
+ facilitatorKey: this.facilitatorKey,
121
+ network: "sui:testnet",
122
+ asset: row.manifest.asset,
123
+ payTo: slot.sellerId,
124
+ amount: row.manifest.priceBand.minimum,
125
+ }, fetch, async () => { try {
126
+ await this.persist(slot);
127
+ }
128
+ catch (error) {
129
+ this.#healthy = false;
130
+ throw error;
131
+ } }, (digest, requestId) => this.db.claimPayment(digest, requestId, slot.sellerId));
132
+ slot.resources.set(row.capability_id, resource);
133
+ return resource;
134
+ }
135
+ async persist(slot) {
136
+ const snapshot = await captureSellerHome(slot.home, slot.sellerId, slot.artifacts);
137
+ slot.revision = await this.db.putSnapshot(encryptSellerSnapshot(snapshot, this.snapshotKey), slot.revision);
138
+ }
139
+ async publishCiphertext(slot, ciphertext) {
140
+ const publisher = slot.app.profile.live?.walrusPublisherUrl ?? fail("LIVE_WALRUS_CONFIG_MISSING", "seller needs a Walrus publisher"), aggregator = slot.app.profile.live?.walrusAggregatorUrl ?? fail("LIVE_WALRUS_CONFIG_MISSING", "seller needs a Walrus aggregator");
141
+ const body = Buffer.from(ciphertext, "base64"), put = await fetch(`${publisher.replace(/\/$/, "")}/v1/blobs?epochs=5`, { method: "PUT", body, signal: AbortSignal.timeout(60_000) });
142
+ if (!put.ok)
143
+ fail("WALRUS_PUBLISH_FAILED", `Walrus publisher returned ${put.status}`);
144
+ const result = await put.json();
145
+ const blobId = result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId ?? result.alreadyRegistered?.blobId ?? fail("WALRUS_PUBLISH_FAILED", "Walrus publisher omitted blob ID");
146
+ const read = await fetch(`${aggregator.replace(/\/$/, "")}/v1/blobs/${encodeURIComponent(blobId)}`, { signal: AbortSignal.timeout(60_000) });
147
+ if (!read.ok || !Buffer.from(await read.arrayBuffer()).equals(body))
148
+ fail("WALRUS_BLOB_MISMATCH", "negotiation ciphertext did not round-trip through Walrus");
149
+ return blobId;
150
+ }
151
+ serialize(operation) {
152
+ const result = this.#queue.then(async () => {
153
+ if (!this.#healthy)
154
+ fail("SELLER_STATE_UNAVAILABLE", "seller service requires restart after a failed persistence operation");
155
+ return operation();
156
+ });
157
+ this.#queue = result.catch(() => undefined);
158
+ return result;
159
+ }
160
+ async body(request) {
161
+ const chunks = [];
162
+ let length = 0;
163
+ for await (const chunk of request) {
164
+ length += chunk.length;
165
+ if (length > 128 * 1024)
166
+ fail("SELLER_REQUEST_INVALID", "request body is too large");
167
+ chunks.push(chunk);
168
+ }
169
+ try {
170
+ return asObject(JSON.parse(Buffer.concat(chunks).toString()));
171
+ }
172
+ catch (error) {
173
+ return fail("SELLER_REQUEST_INVALID", "request body is not JSON", error);
174
+ }
175
+ }
176
+ reply(response, status, data) {
177
+ const body = Buffer.from(JSON.stringify(data));
178
+ response.writeHead(status, { "content-type": "application/json", "content-length": String(body.length), "cache-control": "no-store" });
179
+ response.end(body);
180
+ }
181
+ async handle(request, response) {
182
+ try {
183
+ const url = new URL(request.url ?? "/", this.origin);
184
+ if (request.method === "GET" && url.pathname === "/health")
185
+ return this.reply(response, this.#healthy ? 200 : 503, { ok: this.#healthy, service: "supercuts-sellers" });
186
+ if (request.method === "GET" && url.pathname === "/v2/directory/health") {
187
+ if (!this.directoryAggregator)
188
+ return this.reply(response, 503, { ok: false, service: "supercuts-directory", reason: "aggregator not configured" });
189
+ const capabilities = await this.db.listDirectory();
190
+ return this.reply(response, 200, { ok: true, service: "supercuts-directory", listings: capabilities.length });
191
+ }
192
+ if (request.method === "GET" && url.pathname === "/v2/directory/catalog") {
193
+ const rawOffset = url.searchParams.get("offset") ?? "0";
194
+ if (!/^(0|[1-9][0-9]*)$/.test(rawOffset))
195
+ fail("SELLER_DIRECTORY_INVALID", "directory offset is invalid");
196
+ const offset = Number(rawOffset), capabilities = await this.db.listDirectory(Date.now(), offset);
197
+ return this.reply(response, 200, { capabilities, ...(capabilities.length === 100 ? { nextOffset: offset + 100 } : {}) });
198
+ }
199
+ if (request.method === "POST" && url.pathname === "/v2/directory/register") {
200
+ if (!this.directoryAggregator)
201
+ fail("SELLER_DIRECTORY_UNAVAILABLE", "directory Walrus aggregator is not configured");
202
+ const body = await this.body(request);
203
+ const registration = body.registration;
204
+ await registerDirectoryCapability(this.db, registration, this.directoryAggregator);
205
+ return this.reply(response, 200, { ok: true, capabilityId: registration.manifest.capabilityId, sellerId: registration.manifest.sellerId, expiresAt: registration.manifest.expiresAt });
206
+ }
207
+ if (request.method === "GET" && url.pathname === "/v1/catalog")
208
+ return this.reply(response, 200, { capabilities: [...this.#catalog.values()].map(({ manifest, blob_id, signature, transport_public_key, preview }) => ({ manifest, blobId: blob_id, signature, transportPublicKey: transport_public_key, preview })) });
209
+ if (request.method === "GET" && url.pathname === "/v2/catalog")
210
+ return this.reply(response, 200, { capabilities: [...this.#v2.values()].map(({ row }) => ({ manifest: row.manifest, manifestBlobId: row.blob_id, sellerSignature: row.signature, transportPublicKey: row.transport_public_key })) });
211
+ if (request.method === "POST" && url.pathname === "/v2/prepare") {
212
+ const input = await this.body(request);
213
+ return this.serialize(async () => {
214
+ const capabilityId = validId(input.capabilityId, "capabilityId"), item = this.#v2.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "v2 capability is unknown");
215
+ return this.reply(response, 200, await item.transport.prepareRequest(input));
216
+ });
217
+ }
218
+ if (request.method === "POST" && url.pathname === "/v2/fulfill") {
219
+ const input = await this.body(request);
220
+ return this.serialize(async () => {
221
+ const sessionId = validId(input.sessionId, "sessionId"), sellerId = String(input.sellerId ?? "");
222
+ const transport = this.#v2BySeller.get(sellerId), slot = this.#slots.get(sellerId);
223
+ if (!transport || !slot?.app.store.db.prepare("SELECT 1 FROM seller_deal_prepares WHERE session_id=?").get(sessionId))
224
+ fail("NEGOTIATION_REQUIRED", "seller has no matching prepared v2 deal");
225
+ return this.reply(response, 200, await transport.fulfillRequest(input));
226
+ });
227
+ }
228
+ if (request.method === "GET") {
229
+ const item = [...this.#v2.values()].find(({ row }) => url.pathname.startsWith(`${new URL(row.manifest.endpoint).pathname.replace(/\/$/, "")}/`));
230
+ if (item)
231
+ return this.serialize(async () => item.transport.handle(request, response));
232
+ }
233
+ if (request.method === "GET" && url.pathname === "/v1/events") {
234
+ const runId = validId(url.searchParams.get("runId"), "runId");
235
+ return this.reply(response, 200, { events: await this.db.listEvents(runId) });
236
+ }
237
+ if (request.method === "POST" && url.pathname === "/v1/negotiate") {
238
+ const input = await this.body(request);
239
+ return await this.serialize(async () => {
240
+ const capabilityId = validId(input.capabilityId, "capabilityId"), row = this.#catalog.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "capability is unknown"), slot = this.#slots.get(row.seller_id);
241
+ const sessionId = validId(input.sessionId, "sessionId"), runId = validId(input.runId, "runId"), buyerAddress = String(input.buyerAddress ?? ""), buyerPublicKey = String(input.buyerPublicKey ?? ""), ciphertext = String(input.ciphertext ?? ""), buyerSignature = String(input.buyerSignature ?? "");
242
+ if (!/^0x[0-9a-f]{64}$/.test(buyerAddress) || buyerPublicKey.length > 1024 || ciphertext.length > 16384 || !ciphertext || !buyerSignature)
243
+ fail("SELLER_REQUEST_INVALID", "buyer identity or encrypted offer is invalid");
244
+ if (!this.allowedBuyers.has(buyerAddress))
245
+ fail("SELLER_BUYER_FORBIDDEN", "buyer is outside the seller service allowlist");
246
+ const signedBytes = Buffer.from(`${sessionId}:${capabilityId}:${buyerPublicKey}:${sha256(Buffer.from(ciphertext, "base64"))}`);
247
+ if (!(await isValidPersonalMessageSignature(signedBytes, buyerSignature, { address: buyerAddress })))
248
+ fail("SELLER_BUYER_SIGNATURE_INVALID", "buyer did not authorize this negotiation request");
249
+ const prior = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,buyer_address AS buyerAddress,buyer_public_key AS buyerPublicKey,request_ciphertext AS ciphertext,reply_ciphertext AS replyCiphertext,reply_signature AS replySignature,request_blob_id AS requestBlobId,reply_blob_id AS replyBlobId,transcript_hash AS transcriptHash,brief_hash AS briefHash,run_id AS runId FROM remote_negotiations WHERE session_id=?").get(sessionId);
250
+ if (prior) {
251
+ if (prior.capabilityId !== capabilityId || prior.buyerAddress !== buyerAddress || prior.buyerPublicKey !== buyerPublicKey || prior.ciphertext !== ciphertext || prior.runId !== runId)
252
+ fail("NEGOTIATION_SESSION_CONFLICT", "session already contains a different buyer offer");
253
+ return this.reply(response, 200, { sessionId, capabilityId, ciphertext: prior.replyCiphertext, signature: prior.replySignature, requestBlobId: prior.requestBlobId, replyBlobId: prior.replyBlobId, transcriptHash: prior.transcriptHash });
254
+ }
255
+ const sellerPrivateDer = slot.app.store.getSecret("identity:x25519-private") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
256
+ const shared = diffieHellman({ privateKey: createPrivateKey({ key: sellerPrivateDer, format: "der", type: "pkcs8" }), publicKey: createPublicKey({ key: Buffer.from(buyerPublicKey, "base64url"), format: "der", type: "spki" }) });
257
+ const key = Buffer.from(hkdfSync("sha256", shared, Buffer.from(sessionId), Buffer.from("supercuts-negotiation-v1"), 32));
258
+ let offer;
259
+ try {
260
+ offer = asObject(JSON.parse(decryptSessionMessage(key, sessionId, `${sessionId}:buyer:1`, ciphertext).toString()));
261
+ }
262
+ catch (error) {
263
+ return fail("NEGOTIATION_DECRYPT_FAILED", "seller could not read the buyer offer", error);
264
+ }
265
+ const opening = BigInt(String(offer.opening ?? "0")), maximum = BigInt(String(offer.maximum ?? "0")), minimum = BigInt(row.manifest.priceBand.minimum), briefHash = typeof offer.briefHash === "string" ? offer.briefHash : undefined;
266
+ if (opening <= 0n || opening > maximum || maximum < minimum || maximum > BigInt(row.manifest.priceBand.maximum) || briefHash !== undefined && !/^[0-9a-f]{64}$/.test(briefHash))
267
+ fail("NEGOTIATION_BOUNDS_INVALID", "buyer offer exceeds approved price bounds");
268
+ const task = String(offer.task ?? "").slice(0, 512);
269
+ const decision = await this.#claude.decide({ system: "You are a bounded research seller. Present only the fixed price and service terms approved by Supercuts.", task, candidates: [{ action: "counteroffer", capabilityId, amount: minimum.toString() }], schema: { name: "respond_to_private_offer", description: "Present the approved exact price.", properties: { action: { type: "string", enum: ["counteroffer"] } }, required: ["action"] } });
270
+ if (decision.action.action !== "counteroffer")
271
+ fail("SELLER_DECISION_INVALID", "seller did not choose the approved response");
272
+ const reply = { capabilityId, amount: minimum.toString(), asset: row.manifest.asset, railOptions: row.manifest.supportedRails, accepted: true };
273
+ const replyCiphertext = encryptSessionMessage(key, sessionId, `${sessionId}:seller:2`, Buffer.from(JSON.stringify(reply)));
274
+ const requestBlobId = await this.publishCiphertext(slot, ciphertext), replyBlobId = await this.publishCiphertext(slot, replyCiphertext);
275
+ const transcriptHash = sha256(JSON.stringify({ request: sha256(Buffer.from(ciphertext, "base64")), reply: sha256(Buffer.from(replyCiphertext, "base64")) }));
276
+ const encodedSigner = slot.app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signing key is missing"), decoded = decodeSuiPrivateKey(encodedSigner);
277
+ if (decoded.scheme !== "ED25519")
278
+ fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
279
+ const signature = (await Ed25519Keypair.fromSecretKey(decoded.secretKey).signPersonalMessage(Buffer.from(`${sessionId}:${capabilityId}:${sha256(Buffer.from(replyCiphertext, "base64"))}:${transcriptHash}`))).signature;
280
+ slot.app.store.db.prepare("INSERT INTO remote_negotiations(session_id,capability_id,buyer_address,buyer_public_key,request_ciphertext,reply_ciphertext,reply_signature,request_blob_id,reply_blob_id,transcript_hash,brief_hash,run_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(sessionId, capabilityId, buyerAddress, buyerPublicKey, ciphertext, replyCiphertext, signature, requestBlobId, replyBlobId, transcriptHash, briefHash ?? null, runId);
281
+ try {
282
+ await this.persist(slot);
283
+ }
284
+ catch (error) {
285
+ this.#healthy = false;
286
+ throw error;
287
+ }
288
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "negotiation", status: "complete", details: { capabilityId, sessionId, requestBlobId, replyBlobId, transcriptHash, model: decision.model } });
289
+ return this.reply(response, 200, { sessionId, capabilityId, ciphertext: replyCiphertext, signature, requestBlobId, replyBlobId, transcriptHash });
290
+ });
291
+ }
292
+ if (request.method === "GET" && url.pathname.startsWith("/resource/")) {
293
+ const capabilityId = decodeURIComponent(url.pathname.split("/")[2] ?? ""), row = this.#catalog.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "resource capability is unknown"), slot = this.#slots.get(row.seller_id);
294
+ return await this.serialize(async () => this.resource(slot, row).handle(request, response));
295
+ }
296
+ if (request.method === "POST" && url.pathname === "/v1/quote") {
297
+ const input = await this.body(request);
298
+ return await this.serialize(async () => {
299
+ const capabilityId = validId(input.capabilityId, "capabilityId"), row = this.#catalog.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "capability is unknown"), slot = this.#slots.get(row.seller_id);
300
+ if (!row.manifest.supportedRails.includes("x402"))
301
+ fail("SELLER_RAIL_UNSUPPORTED", "capability cannot quote x402");
302
+ const requestId = validId(input.requestId, "requestId"), operationId = validId(input.operationId, "operationId"), runId = validId(input.runId, "runId");
303
+ const negotiation = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,run_id AS runId FROM remote_negotiations WHERE session_id=?").get(requestId);
304
+ if (!negotiation || negotiation.capabilityId !== capabilityId || negotiation.runId !== runId)
305
+ fail("NEGOTIATION_REQUIRED", "exact quote requires the matching encrypted negotiation");
306
+ const briefHash = typeof input.briefHash === "string" ? input.briefHash : undefined;
307
+ if (briefHash !== undefined && !/^[0-9a-f]{64}$/.test(briefHash))
308
+ fail("SELLER_REQUEST_INVALID", "brief hash is invalid");
309
+ const quote = { requestId, operationId, capabilityId, amount: row.manifest.priceBand.minimum, artifactKey: briefHash ? `${capabilityId}:${briefHash}` : capabilityId, runId };
310
+ const prior = slot.app.store.db.prepare("SELECT operation_id AS operationId,capability_id AS capabilityId,amount,artifact_key AS artifactKey,run_id AS runId FROM remote_quotes WHERE request_id=?").get(requestId);
311
+ if (prior && JSON.stringify(prior) !== JSON.stringify({ operationId, capabilityId, amount: quote.amount, artifactKey: quote.artifactKey, runId }))
312
+ fail("SELLER_QUOTE_CONFLICT", "request ID identifies another quote");
313
+ const decision = await this.#claude.decide({ system: "You are a bounded seller agent. You may present only the fixed approved quote.", task: String(input.task ?? "Sui market brief").slice(0, 512), candidates: [{ action: "present_quote", capabilityId, amount: quote.amount }], schema: { name: "respond_to_buyer", description: "Present the approved exact price.", properties: { action: { type: "string", enum: ["present_quote"] } }, required: ["action"] } });
314
+ if (decision.action.action !== "present_quote")
315
+ fail("SELLER_DECISION_INVALID", "seller did not choose the approved quote");
316
+ slot.app.store.db.prepare("INSERT OR IGNORE INTO remote_quotes(request_id,operation_id,capability_id,amount,artifact_key,run_id) VALUES(?,?,?,?,?,?)").run(requestId, operationId, capabilityId, quote.amount, quote.artifactKey, runId);
317
+ const artifact = this.artifact(slot, quote.artifactKey), registration = this.resource(slot, row).register(requestId, operationId, artifact);
318
+ try {
319
+ await this.persist(slot);
320
+ }
321
+ catch (error) {
322
+ this.#healthy = false;
323
+ throw error;
324
+ }
325
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "quote", status: "complete", details: { capabilityId, amount: quote.amount, model: decision.model } });
326
+ return this.reply(response, 200, { capabilityId, sellerId: slot.sellerId, amount: quote.amount, asset: row.manifest.asset, requestId, operationId, resourceUrl: registration.url, artifactSha256: registration.artifactSha256 });
327
+ });
328
+ }
329
+ if (request.method === "POST" && url.pathname === "/v1/agreement") {
330
+ const input = await this.body(request);
331
+ return await this.serialize(async () => {
332
+ const capabilityId = validId(input.capabilityId, "capabilityId"), row = this.#catalog.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "capability is unknown"), slot = this.#slots.get(row.seller_id);
333
+ if (!row.manifest.supportedRails.includes("shielded"))
334
+ fail("SELLER_RAIL_UNSUPPORTED", "capability cannot use shielded settlement");
335
+ const sessionId = validId(input.sessionId, "sessionId"), runId = validId(input.runId, "runId"), briefHash = String(input.briefHash ?? ""), deliveryPublicKey = String(input.deliveryPublicKey ?? "");
336
+ if (!/^[0-9a-f]{64}$/.test(briefHash) || deliveryPublicKey.length > 1024)
337
+ fail("SELLER_REQUEST_INVALID", "brief hash or delivery key is invalid");
338
+ const negotiation = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,run_id AS runId,brief_hash AS briefHash,transcript_hash AS transcriptHash FROM remote_negotiations WHERE session_id=?").get(sessionId) ?? fail("NEGOTIATION_REQUIRED", "agreement requires the matching encrypted negotiation");
339
+ if (negotiation.capabilityId !== capabilityId || negotiation.runId !== runId || negotiation.briefHash !== briefHash)
340
+ fail("NEGOTIATION_REQUIRED", "agreement requires the matching encrypted negotiation");
341
+ const artifactKey = `${capabilityId}:${briefHash}`, artifact = this.artifact(slot, artifactKey), artifactSha256 = sha256(artifact);
342
+ const encoded = String(input.agreement ?? ""), draft = asObject(JSON.parse(Buffer.from(encoded, "base64url").toString()));
343
+ if (draft.counterparty !== slot.sellerId || draft.capabilityId !== capabilityId || draft.manifestHash !== row.manifest.manifestHash || draft.paymentAmount !== row.manifest.priceBand.minimum)
344
+ fail("AGREEMENT_BINDING_MISMATCH", "draft does not match the seller offer");
345
+ if (draft.transcriptRootLo !== BigInt(`0x${negotiation.transcriptHash.slice(0, 32)}`).toString() || draft.transcriptRootHi !== BigInt(`0x${negotiation.transcriptHash.slice(32)}`).toString())
346
+ fail("AGREEMENT_BINDING_MISMATCH", "draft does not bind the encrypted negotiation transcript");
347
+ const expected = sha256(JSON.stringify({ capabilityId, briefHash, artifactSha256, deliveryPublicKey }));
348
+ if (draft.serviceDigestLo !== BigInt(`0x${expected.slice(0, 32)}`).toString() || draft.serviceDigestHi !== BigInt(`0x${expected.slice(32)}`).toString())
349
+ fail("AGREEMENT_BINDING_MISMATCH", "draft does not bind the prepared analysis and buyer delivery key");
350
+ const prior = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,artifact_key AS artifactKey,delivery_public_key AS deliveryPublicKey,run_id AS runId FROM remote_deliveries WHERE session_id=?").get(sessionId);
351
+ if (prior && JSON.stringify(prior) !== JSON.stringify({ capabilityId, artifactKey, deliveryPublicKey, runId }))
352
+ fail("AGREEMENT_SESSION_CONFLICT", "session identifies another delivery");
353
+ const decision = await this.#claude.decide({ system: "You are a bounded seller agent. Sign only the backend-verified agreement.", task: `Respond to accepted competitor analysis request for ${capabilityId}.`, candidates: [{ action: "sign_approved_agreement", capabilityId }], schema: { name: "respond_to_private_offer", description: "Approve the backend-verified agreement.", properties: { action: { type: "string", enum: ["sign_approved_agreement"] } }, required: ["action"] } });
354
+ if (decision.action.action !== "sign_approved_agreement")
355
+ fail("SELLER_DECISION_INVALID", "seller did not choose the approved agreement");
356
+ const signed = await executeMandated(slot.app, loadMandate(slot.home), "offer.counter", { "operation-id": `remote-counter-${sessionId}`, session: sessionId, agreement: encoded });
357
+ slot.app.store.db.prepare("INSERT OR IGNORE INTO remote_deliveries(session_id,capability_id,artifact_key,delivery_public_key,run_id) VALUES(?,?,?,?,?)").run(sessionId, capabilityId, artifactKey, deliveryPublicKey, runId);
358
+ try {
359
+ await this.persist(slot);
360
+ }
361
+ catch (error) {
362
+ this.#healthy = false;
363
+ throw error;
364
+ }
365
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "agreement", status: "complete", details: { capabilityId, sessionId, model: decision.model, dealCommitment: signed.dealCommitment } });
366
+ return this.reply(response, 200, { sessionId, capabilityId, agreement: signed.agreement, dealCommitment: signed.dealCommitment, artifactSha256 });
367
+ });
368
+ }
369
+ if (request.method === "GET" && url.pathname.startsWith("/v1/delivery/")) {
370
+ const sessionId = validId(decodeURIComponent(url.pathname.slice("/v1/delivery/".length)), "sessionId");
371
+ return await this.serialize(async () => {
372
+ const slot = [...this.#slots.values()].find((item) => item.app.store.db.prepare("SELECT 1 FROM remote_deliveries WHERE session_id=?").get(sessionId)) ?? fail("DELIVERY_NOT_FOUND", "delivery session is unknown");
373
+ const row = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,artifact_key AS artifactKey,delivery_public_key AS deliveryPublicKey,run_id AS runId FROM remote_deliveries WHERE session_id=?").get(sessionId);
374
+ await executeMandated(slot.app, loadMandate(slot.home), "wallet.recover", { "operation-id": `remote-recover-${sessionId}` });
375
+ const note = slot.app.store.db.prepare("SELECT commitment FROM notes WHERE operation_id=? AND role='payment'").get(sessionId);
376
+ if (!note)
377
+ return this.reply(response, 202, { status: "payment-pending", sessionId });
378
+ const artifact = this.artifact(slot, row.artifactKey);
379
+ const recipient = createPublicKey({ key: Buffer.from(row.deliveryPublicKey, "base64url"), format: "der", type: "spki" });
380
+ const ephemeral = generateKeyPairSync("x25519");
381
+ const shared = diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipient });
382
+ const key = Buffer.from(hkdfSync("sha256", shared, Buffer.from(sessionId), Buffer.from("supercuts-delivery-v1"), 32));
383
+ const nonce = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, nonce);
384
+ cipher.setAAD(Buffer.from(`${sessionId}:${row.capabilityId}:${sha256(artifact)}`));
385
+ const ciphertext = Buffer.concat([cipher.update(artifact), cipher.final()]);
386
+ try {
387
+ await this.persist(slot);
388
+ }
389
+ catch (error) {
390
+ this.#healthy = false;
391
+ throw error;
392
+ }
393
+ await this.db.addEvent({ run_id: row.runId, seller_id: slot.sellerId, stage: "delivery", status: "complete", details: { capabilityId: row.capabilityId, sessionId, artifactSha256: sha256(artifact), paymentNoteCommitment: note.commitment } });
394
+ return this.reply(response, 200, { status: "delivered", sessionId, capabilityId: row.capabilityId, artifactSha256: sha256(artifact), ephemeralPublicKey: ephemeral.publicKey.export({ type: "spki", format: "der" }).toString("base64url"), nonce: nonce.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), ciphertext: ciphertext.toString("base64url") });
395
+ });
396
+ }
397
+ return this.reply(response, 404, { error: "NOT_FOUND" });
398
+ }
399
+ catch (error) {
400
+ const known = error instanceof SupercutsError ? error : new SupercutsError("SELLER_INTERNAL_ERROR", "seller request failed");
401
+ if (!response.headersSent)
402
+ this.reply(response, ["SELLER_STATE_UNAVAILABLE", "SELLER_DIRECTORY_UNAVAILABLE", "SELLER_DB_UNAVAILABLE"].includes(known.code) ? 503 : 400, { error: known.code, message: known.message });
403
+ }
404
+ }
405
+ createServer() { return createServer((request, response) => { void this.handle(request, response); }); }
406
+ close() { for (const slot of this.#slots.values())
407
+ slot.app.close(); rmSync(this.#root, { recursive: true, force: true }); }
408
+ }
409
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
410
+ const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
411
+ const port = Number(process.env.PORT ?? "3000");
412
+ const origin = required("SELLER_ORIGIN");
413
+ SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set(required("SELLER_ALLOWED_BUYERS").split(",")), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
414
+ .then((service) => { const server = service.createServer(); server.listen(port, "0.0.0.0"); const close = () => server.close(() => { service.close(); process.exit(0); }); process.on("SIGINT", close); process.on("SIGTERM", close); })
415
+ .catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller service failed"}\n`); process.exitCode = 1; });
416
+ }
@@ -0,0 +1,19 @@
1
+ declare const FILES: readonly ["profile.json", "mandate.json", "state.key", "state.sqlite"];
2
+ export interface SellerSnapshot {
3
+ version: 1;
4
+ sellerId: string;
5
+ files: Record<(typeof FILES)[number], string>;
6
+ artifacts: Record<string, string>;
7
+ }
8
+ export interface EncryptedSellerSnapshot {
9
+ version: 1;
10
+ sellerId: string;
11
+ nonce: string;
12
+ tag: string;
13
+ ciphertext: string;
14
+ }
15
+ export declare function encryptSellerSnapshot(snapshot: SellerSnapshot, encodedKey: string): EncryptedSellerSnapshot;
16
+ export declare function decryptSellerSnapshot(value: EncryptedSellerSnapshot, encodedKey: string): SellerSnapshot;
17
+ export declare function captureSellerHome(home: string, sellerId: string, artifacts?: Record<string, string>): Promise<SellerSnapshot>;
18
+ export declare function restoreSellerHome(home: string, snapshot: SellerSnapshot): void;
19
+ export {};
@@ -0,0 +1,60 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { backup, DatabaseSync } from "node:sqlite";
5
+ import { fail } from "./errors.js";
6
+ const FILES = ["profile.json", "mandate.json", "state.key", "state.sqlite"];
7
+ function keyBytes(encoded) {
8
+ const key = Buffer.from(encoded, "base64url");
9
+ if (key.length !== 32)
10
+ fail("SELLER_STATE_KEY_INVALID", "seller snapshot key must contain 32 bytes");
11
+ return key;
12
+ }
13
+ export function encryptSellerSnapshot(snapshot, encodedKey) {
14
+ const nonce = randomBytes(12), cipher = createCipheriv("aes-256-gcm", keyBytes(encodedKey), nonce);
15
+ cipher.setAAD(Buffer.from(`supercuts-seller-state-v1:${snapshot.sellerId}`));
16
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(snapshot)), cipher.final()]);
17
+ return { version: 1, sellerId: snapshot.sellerId, nonce: nonce.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), ciphertext: ciphertext.toString("base64url") };
18
+ }
19
+ export function decryptSellerSnapshot(value, encodedKey) {
20
+ try {
21
+ if (value.version !== 1 || !value.sellerId)
22
+ fail("SELLER_STATE_INVALID", "seller snapshot version or identity is invalid");
23
+ const decipher = createDecipheriv("aes-256-gcm", keyBytes(encodedKey), Buffer.from(value.nonce, "base64url"));
24
+ decipher.setAAD(Buffer.from(`supercuts-seller-state-v1:${value.sellerId}`));
25
+ decipher.setAuthTag(Buffer.from(value.tag, "base64url"));
26
+ const snapshot = JSON.parse(Buffer.concat([decipher.update(Buffer.from(value.ciphertext, "base64url")), decipher.final()]).toString());
27
+ if (snapshot.version !== 1 || snapshot.sellerId !== value.sellerId || !snapshot.artifacts || typeof snapshot.artifacts !== "object" || FILES.some((name) => typeof snapshot.files?.[name] !== "string"))
28
+ fail("SELLER_STATE_INVALID", "seller snapshot contents are invalid");
29
+ return snapshot;
30
+ }
31
+ catch (error) {
32
+ return fail("SELLER_STATE_DECRYPT_FAILED", "seller snapshot could not be authenticated", error);
33
+ }
34
+ }
35
+ export async function captureSellerHome(home, sellerId, artifacts = {}) {
36
+ const db = new DatabaseSync(join(home, "state.sqlite"), { readOnly: true });
37
+ const temporary = join(home, `snapshot-${randomBytes(8).toString("hex")}.sqlite`);
38
+ try {
39
+ await backup(db, temporary);
40
+ const files = {};
41
+ for (const name of FILES)
42
+ files[name] = readFileSync(name === "state.sqlite" ? temporary : join(home, name)).toString("base64url");
43
+ return { version: 1, sellerId, files, artifacts };
44
+ }
45
+ finally {
46
+ db.close();
47
+ if (existsSync(temporary)) {
48
+ const { unlinkSync } = await import("node:fs");
49
+ unlinkSync(temporary);
50
+ }
51
+ }
52
+ }
53
+ export function restoreSellerHome(home, snapshot) {
54
+ if (!snapshot.sellerId || snapshot.version !== 1)
55
+ fail("SELLER_STATE_INVALID", "seller snapshot identity is invalid");
56
+ mkdirSync(home, { recursive: true, mode: 0o700 });
57
+ chmodSync(home, 0o700);
58
+ for (const name of FILES)
59
+ writeFileSync(join(home, name), Buffer.from(snapshot.files[name], "base64url"), { mode: 0o600, flag: "wx" });
60
+ }
@@ -0,0 +1,6 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import { SellerRuntime } from "./seller-runtime.js";
3
+ /** Local JSONL adapter. The host owns the seller signer, payment verifier,
4
+ * handler, and Walrus transport; callers receive only public registration
5
+ * metadata and encrypted delivery references. */
6
+ export declare function serveSellerStdio(runtime: SellerRuntime, input?: Readable, output?: Writable): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { createInterface } from "node:readline";
2
+ import { SupercutsError } from "./errors.js";
3
+ /** Local JSONL adapter. The host owns the seller signer, payment verifier,
4
+ * handler, and Walrus transport; callers receive only public registration
5
+ * metadata and encrypted delivery references. */
6
+ export async function serveSellerStdio(runtime, input = process.stdin, output = process.stdout) {
7
+ const lines = createInterface({ input, crlfDelay: Infinity });
8
+ let queue = Promise.resolve();
9
+ const send = (value) => { output.write(`${JSON.stringify(value)}\n`); };
10
+ for await (const line of lines) {
11
+ queue = queue.then(async () => {
12
+ let id = null;
13
+ try {
14
+ if (Buffer.byteLength(line) > 1024 * 1024)
15
+ throw new SupercutsError("SELLER_REQUEST_INVALID", "seller stdio request exceeds 1 MiB");
16
+ const parsed = JSON.parse(line);
17
+ if (!parsed || typeof parsed.id !== "string" || !parsed.id || !["catalog", "fulfill"].includes(parsed.method))
18
+ throw new SupercutsError("SELLER_REQUEST_INVALID", "seller stdio method or request ID is invalid");
19
+ id = parsed.id;
20
+ const result = parsed.method === "catalog" ? { capabilities: runtime.catalog() } : await runtime.fulfill(parsed.params);
21
+ send({ id, ok: true, result });
22
+ }
23
+ catch (error) {
24
+ send({ id, ok: false, error: { code: error instanceof SupercutsError ? error.code : "SELLER_REQUEST_FAILED", message: error instanceof SupercutsError ? error.message : "seller request failed" } });
25
+ }
26
+ });
27
+ }
28
+ await queue;
29
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
5
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
6
+ import { verifySignedCapability, validateCapabilityPayload } from "./capability.js";
7
+ import { fail } from "./errors.js";
8
+ import { SellerDatabase } from "./seller-database.js";
9
+ import { captureSellerHome, decryptSellerSnapshot, encryptSellerSnapshot } from "./seller-state.js";
10
+ import { Phase3App } from "./phase3.js";
11
+ async function main() {
12
+ const configPath = resolve(process.argv[2] ?? fail("ARGUMENT_REQUIRED", "usage: supercuts-seller-v2-provision <owner-only-config.json>"));
13
+ if ((statSync(configPath).mode & 0o077) !== 0)
14
+ fail("SELLER_CONFIG_PERMISSIONS", "provision config must be owner-only");
15
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
16
+ if (!Array.isArray(config.sellers) || !config.sellers.length)
17
+ fail("SELLER_CONFIG_INVALID", "at least one seller is required");
18
+ const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
19
+ const db = new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY"));
20
+ const stateKey = required("SELLER_STATE_KEY"), origin = new URL(required("SELLER_ORIGIN")).origin;
21
+ for (const seller of config.sellers) {
22
+ const stored = await db.getSnapshot(seller.sellerId);
23
+ let snapshot;
24
+ if (stored)
25
+ snapshot = decryptSellerSnapshot(stored.payload, stateKey);
26
+ else {
27
+ if (!seller.home)
28
+ fail("SELLER_STATE_MISSING", "new hosted v2 seller requires its separate local role home");
29
+ const home = resolve(seller.home);
30
+ if ((statSync(home).mode & 0o077) !== 0)
31
+ fail("SELLER_CONFIG_PERMISSIONS", "seller role home must be owner-only");
32
+ const app = new Phase3App(home);
33
+ try {
34
+ if (app.profile.role !== "seller" || app.profile.mock || app.profile.verifierVersion !== 2)
35
+ fail("SELLER_POOL_VERSION_MISMATCH", "new hosted v2 seller requires a live v2 seller role home");
36
+ const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
37
+ const decoded = decodeSuiPrivateKey(encoded);
38
+ if (decoded.scheme !== "ED25519" || Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress() !== seller.sellerId)
39
+ fail("SELLER_IDENTITY_MISMATCH", "v2 seller home signer differs from configured seller ID");
40
+ snapshot = await captureSellerHome(home, seller.sellerId);
41
+ }
42
+ finally {
43
+ app.close();
44
+ }
45
+ }
46
+ const profile = JSON.parse(Buffer.from(snapshot.files["profile.json"], "base64url").toString());
47
+ if (profile.role !== "seller" || profile.mock || profile.verifierVersion !== 2)
48
+ fail("SELLER_POOL_VERSION_MISMATCH", "v2 capability cannot use a v1 seller snapshot; create a separate v2 role home");
49
+ const registrations = [];
50
+ for (const item of seller.capabilities) {
51
+ const path = resolve(item.registrationFile);
52
+ if ((statSync(path).mode & 0o077) !== 0)
53
+ fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
54
+ const registration = JSON.parse(readFileSync(path, "utf8"));
55
+ await verifySignedCapability(registration);
56
+ if (registration.manifest.sellerId !== seller.sellerId || new URL(registration.manifest.endpoint).origin !== origin || !new URL(registration.manifest.endpoint).pathname.startsWith("/v2/resource/"))
57
+ fail("SELLER_CONFIG_INVALID", "v2 registration seller or endpoint differs from the hosted service");
58
+ validateCapabilityPayload(registration.manifest, "output", item.outputMetadata);
59
+ const artifact = readFileSync(resolve(item.artifactFile));
60
+ if (!artifact.length || artifact.length > 10 * 1024 * 1024)
61
+ fail("SELLER_ARTIFACT_INVALID", "v2 artifact must be 1 byte to 10 MiB");
62
+ snapshot.artifacts[registration.manifest.capabilityId] = artifact.toString("base64url");
63
+ registrations.push({ registration, item });
64
+ }
65
+ await db.putSnapshot(encryptSellerSnapshot(snapshot, stateKey), stored?.revision);
66
+ for (const { registration, item } of registrations)
67
+ await db.putCatalog({ seller_id: seller.sellerId, capability_id: registration.manifest.capabilityId, manifest: registration.manifest, blob_id: registration.manifestBlobId, signature: registration.sellerSignature, transport_public_key: registration.transportPublicKey, preview: { genericV2: true, outputMetadata: item.outputMetadata, contentType: item.contentType ?? "application/octet-stream" } });
68
+ process.stdout.write(`${JSON.stringify({ ok: true, sellerId: seller.sellerId, capabilities: registrations.map(({ registration }) => registration.manifest.capabilityId), restartRequired: true })}\n`);
69
+ }
70
+ }
71
+ main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "v2 provisioning failed"}\n`); process.exitCode = 1; });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};