@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.
Files changed (155) 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 +32 -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 +511 -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/funding.d.ts +38 -0
  38. package/dist/funding.js +78 -0
  39. package/dist/hosted-exit-cli.d.ts +2 -0
  40. package/dist/hosted-exit-cli.js +37 -0
  41. package/dist/hosted-exit.d.ts +24 -0
  42. package/dist/hosted-exit.js +70 -0
  43. package/dist/hosted-seller-import.d.ts +2 -0
  44. package/dist/hosted-seller-import.js +70 -0
  45. package/dist/hosted-seller-repository.d.ts +31 -0
  46. package/dist/hosted-seller-repository.js +214 -0
  47. package/dist/index.d.ts +37 -0
  48. package/dist/index.js +37 -0
  49. package/dist/indexer.d.ts +63 -0
  50. package/dist/indexer.js +60 -0
  51. package/dist/live-deal-ports.d.ts +41 -0
  52. package/dist/live-deal-ports.js +212 -0
  53. package/dist/live-index.d.ts +28 -0
  54. package/dist/live-index.js +149 -0
  55. package/dist/live-runtime.d.ts +85 -0
  56. package/dist/live-runtime.js +644 -0
  57. package/dist/live-seller-transport.d.ts +30 -0
  58. package/dist/live-seller-transport.js +169 -0
  59. package/dist/live-seller-verifier.d.ts +11 -0
  60. package/dist/live-seller-verifier.js +65 -0
  61. package/dist/mandate.d.ts +34 -0
  62. package/dist/mandate.js +179 -0
  63. package/dist/mcp-bootstrap.d.ts +2 -0
  64. package/dist/mcp-bootstrap.js +61 -0
  65. package/dist/mcp.d.ts +2 -0
  66. package/dist/mcp.js +207 -0
  67. package/dist/memory-worker.d.ts +2 -0
  68. package/dist/memory-worker.js +38 -0
  69. package/dist/memory.d.ts +15 -0
  70. package/dist/memory.js +17 -0
  71. package/dist/memwal.d.ts +35 -0
  72. package/dist/memwal.js +83 -0
  73. package/dist/negotiation.d.ts +23 -0
  74. package/dist/negotiation.js +58 -0
  75. package/dist/notes.d.ts +11 -0
  76. package/dist/notes.js +26 -0
  77. package/dist/onboarding-doctor.d.ts +1 -0
  78. package/dist/onboarding-doctor.js +87 -0
  79. package/dist/phase3.d.ts +100 -0
  80. package/dist/phase3.js +1174 -0
  81. package/dist/procurement.d.ts +8 -0
  82. package/dist/procurement.js +19 -0
  83. package/dist/protocol-engine.d.ts +121 -0
  84. package/dist/protocol-engine.js +149 -0
  85. package/dist/prover.d.ts +28 -0
  86. package/dist/prover.js +70 -0
  87. package/dist/relayer.d.ts +32 -0
  88. package/dist/relayer.js +43 -0
  89. package/dist/remote-buyer.d.ts +85 -0
  90. package/dist/remote-buyer.js +285 -0
  91. package/dist/resource-server.d.ts +2 -0
  92. package/dist/resource-server.js +24 -0
  93. package/dist/resource.d.ts +22 -0
  94. package/dist/resource.js +55 -0
  95. package/dist/role-lock.d.ts +8 -0
  96. package/dist/role-lock.js +54 -0
  97. package/dist/seam.d.ts +2 -0
  98. package/dist/seam.js +117 -0
  99. package/dist/seller-catalog-generate.d.ts +2 -0
  100. package/dist/seller-catalog-generate.js +68 -0
  101. package/dist/seller-database.d.ts +41 -0
  102. package/dist/seller-database.js +80 -0
  103. package/dist/seller-directory-cli.d.ts +2 -0
  104. package/dist/seller-directory-cli.js +24 -0
  105. package/dist/seller-directory.d.ts +5 -0
  106. package/dist/seller-directory.js +50 -0
  107. package/dist/seller-live-cli.d.ts +2 -0
  108. package/dist/seller-live-cli.js +53 -0
  109. package/dist/seller-package.d.ts +9 -0
  110. package/dist/seller-package.js +50 -0
  111. package/dist/seller-provision.d.ts +2 -0
  112. package/dist/seller-provision.js +84 -0
  113. package/dist/seller-register-cli.d.ts +2 -0
  114. package/dist/seller-register-cli.js +40 -0
  115. package/dist/seller-runtime.d.ts +60 -0
  116. package/dist/seller-runtime.js +96 -0
  117. package/dist/seller-service.d.ts +34 -0
  118. package/dist/seller-service.js +422 -0
  119. package/dist/seller-state.d.ts +19 -0
  120. package/dist/seller-state.js +60 -0
  121. package/dist/seller-stdio.d.ts +6 -0
  122. package/dist/seller-stdio.js +29 -0
  123. package/dist/seller-v2-provision.d.ts +2 -0
  124. package/dist/seller-v2-provision.js +71 -0
  125. package/dist/seller-watch.d.ts +2 -0
  126. package/dist/seller-watch.js +45 -0
  127. package/dist/settlement.d.ts +95 -0
  128. package/dist/settlement.js +141 -0
  129. package/dist/store.d.ts +36 -0
  130. package/dist/store.js +193 -0
  131. package/dist/sui.d.ts +49 -0
  132. package/dist/sui.js +77 -0
  133. package/dist/tatum.d.ts +17 -0
  134. package/dist/tatum.js +83 -0
  135. package/dist/transcript.d.ts +31 -0
  136. package/dist/transcript.js +52 -0
  137. package/dist/v2-exit-relayer.d.ts +34 -0
  138. package/dist/v2-exit-relayer.js +102 -0
  139. package/dist/v2-exit-worker.d.ts +2 -0
  140. package/dist/v2-exit-worker.js +97 -0
  141. package/dist/v2-exits.d.ts +30 -0
  142. package/dist/v2-exits.js +78 -0
  143. package/dist/v2-prefund.d.ts +19 -0
  144. package/dist/v2-prefund.js +43 -0
  145. package/dist/v2-relayer-cli.d.ts +2 -0
  146. package/dist/v2-relayer-cli.js +39 -0
  147. package/dist/wallet.d.ts +32 -0
  148. package/dist/wallet.js +90 -0
  149. package/dist/workflow-memory.d.ts +22 -0
  150. package/dist/workflow-memory.js +49 -0
  151. package/dist/x402-resource-service.d.ts +36 -0
  152. package/dist/x402-resource-service.js +173 -0
  153. package/dist/x402.d.ts +46 -0
  154. package/dist/x402.js +136 -0
  155. package/package.json +60 -3
@@ -0,0 +1,96 @@
1
+ import { createHash } from "node:crypto";
2
+ import { verifySignedCapability, validateCapabilityPayload } from "./capability.js";
3
+ import { fail } from "./errors.js";
4
+ import { canonicalJson } from "./settlement.js";
5
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
6
+ const cacheKey = (requestId) => `seller:delivery:${requestId}`;
7
+ /** Framework-neutral seller execution boundary. The trusted payment verifier
8
+ * must use chain finality and exact agreement binding, never a buyer's digest
9
+ * alone. The transport must decrypt/authenticate task input before calling
10
+ * fulfill and encrypt the artifact before Walrus publication. */
11
+ export class SellerRuntime {
12
+ store;
13
+ ports;
14
+ #handlers = new Map();
15
+ constructor(store, ports) {
16
+ this.store = store;
17
+ this.ports = ports;
18
+ store.db.exec("CREATE TABLE IF NOT EXISTS seller_runtime_deliveries(request_id TEXT PRIMARY KEY,request_hash TEXT NOT NULL,status TEXT NOT NULL,blob_id TEXT,artifact_sha256 TEXT,created_at INTEGER NOT NULL)");
19
+ }
20
+ async registerCapability(registration, handler) {
21
+ await verifySignedCapability(registration);
22
+ const bytes = await this.ports.readBlob(registration.manifestBlobId);
23
+ let remote;
24
+ try {
25
+ remote = JSON.parse(Buffer.from(bytes).toString());
26
+ }
27
+ catch (error) {
28
+ return fail("CAPABILITY_BLOB_INVALID", "Walrus capability blob is not JSON", error);
29
+ }
30
+ if (canonicalJson(remote) !== canonicalJson(registration.manifest))
31
+ fail("CAPABILITY_BLOB_MISMATCH", "Walrus capability blob differs from the seller's signed manifest");
32
+ const id = registration.manifest.capabilityId;
33
+ const prior = this.#handlers.get(id);
34
+ const saved = this.store.getSecret(`seller:capability:${id}`);
35
+ const old = saved ? JSON.parse(saved.toString()) : prior?.registration;
36
+ if (old && old.manifest.manifestHash !== registration.manifest.manifestHash) {
37
+ if (old.manifest.expiresAt > Date.now() || old.manifest.sellerId !== registration.manifest.sellerId || old.manifest.capabilityId !== id)
38
+ fail("SELLER_CAPABILITY_CONFLICT", "an active capability cannot be replaced with different terms");
39
+ this.store.putSecret(`seller:capability:${id}`, Buffer.from(canonicalJson(registration)));
40
+ }
41
+ else
42
+ this.store.putSecretOnce(`seller:capability:${id}`, Buffer.from(canonicalJson(registration)));
43
+ this.#handlers.set(id, { registration, handler });
44
+ }
45
+ catalog() { return [...this.#handlers.values()].map((item) => item.registration); }
46
+ async fulfill(request) {
47
+ if (!request || typeof request.requestId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(request.requestId) || typeof request.dealId !== "string" || !request.dealId || typeof request.paymentDigest !== "string" || !request.paymentDigest || typeof request.agreementHash !== "string" || !/^[0-9a-f]{64}$/.test(request.agreementHash) || typeof request.agreedAmount !== "string" || !/^[1-9][0-9]*$/.test(request.agreedAmount) || !request.input || typeof request.input !== "object" || Array.isArray(request.input))
48
+ fail("SELLER_REQUEST_INVALID", "deal or request identity is invalid");
49
+ const registered = this.#handlers.get(request.capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "capability is not registered");
50
+ const manifest = registered.registration.manifest;
51
+ if (!manifest.supportedRails.includes(request.rail))
52
+ fail("SELLER_RAIL_UNSUPPORTED", "seller capability does not offer this payment rail");
53
+ if (BigInt(request.agreedAmount) < BigInt(manifest.priceBand.minimum) || BigInt(request.agreedAmount) > BigInt(manifest.priceBand.maximum))
54
+ fail("SELLER_PAYMENT_UNVERIFIED", "agreed amount is outside the signed capability price band");
55
+ validateCapabilityPayload(manifest, "input", request.input);
56
+ const requestHash = sha256(canonicalJson({ domain: "supercuts-seller-request-v1", request }));
57
+ const prior = this.store.db.prepare("SELECT request_hash,status FROM seller_runtime_deliveries WHERE request_id=?").get(request.requestId);
58
+ if (prior && prior.request_hash !== requestHash)
59
+ fail("SELLER_REQUEST_CONFLICT", "request ID identifies a different task or payment");
60
+ if (prior?.status === "complete") {
61
+ const saved = this.store.getSecret(cacheKey(request.requestId)) ?? fail("SELLER_DELIVERY_CORRUPT", "completed delivery metadata is missing");
62
+ return JSON.parse(saved.toString());
63
+ }
64
+ if (prior)
65
+ fail("SELLER_DELIVERY_UNRESOLVED", "seller request may already have run; operator reconciliation is required");
66
+ const payment = await this.ports.verifyPayment(request);
67
+ if (payment.finalized !== true || payment.tatumVerified !== true || payment.digest !== request.paymentDigest || payment.agreementHash !== request.agreementHash || payment.sellerId !== manifest.sellerId || payment.asset !== manifest.asset || payment.amount !== request.agreedAmount || payment.rail !== request.rail)
68
+ fail("SELLER_PAYMENT_UNVERIFIED", "final payment does not match the signed capability and agreement");
69
+ this.store.db.prepare("INSERT INTO seller_runtime_deliveries(request_id,request_hash,status,created_at) VALUES(?,?,'running',?)").run(request.requestId, requestHash, Date.now());
70
+ const result = await registered.handler(request);
71
+ if (!(result.artifact instanceof Uint8Array) || result.artifact.length > 512 * 1024 || !result.artifact.length || typeof result.contentType !== "string" || !result.contentType)
72
+ fail("DEAL_ARTIFACT_TOO_LARGE", "seller artifact must contain 1..524288 bytes and a content type");
73
+ if (!result.metadata || typeof result.metadata !== "object" || Array.isArray(result.metadata))
74
+ fail("CAPABILITY_PAYLOAD_INVALID", "seller metadata must be an object");
75
+ validateCapabilityPayload(manifest, "output", result.metadata);
76
+ const published = await this.ports.encryptAndPublish(request, result);
77
+ if (!published.blobId || !(published.ciphertext instanceof Uint8Array) || !published.ciphertext.length)
78
+ fail("SELLER_DELIVERY_INVALID", "encrypted delivery publication is incomplete");
79
+ const fetched = await this.ports.readBlob(published.blobId);
80
+ if (!Buffer.from(fetched).equals(Buffer.from(published.ciphertext)))
81
+ fail("WALRUS_BLOB_MISMATCH", "encrypted seller delivery failed Walrus readback");
82
+ const delivery = { dealId: request.dealId, requestId: request.requestId, blobId: published.blobId, ciphertextHash: sha256(published.ciphertext), artifactSha256: sha256(result.artifact), contentType: result.contentType, metadata: result.metadata };
83
+ this.store.db.exec("BEGIN IMMEDIATE");
84
+ try {
85
+ this.store.putSecretOnce(cacheKey(request.requestId), Buffer.from(canonicalJson(delivery)));
86
+ this.store.db.prepare("UPDATE seller_runtime_deliveries SET status='complete',blob_id=?,artifact_sha256=? WHERE request_id=? AND status='running'").run(delivery.blobId, delivery.artifactSha256, request.requestId);
87
+ this.store.db.exec("COMMIT");
88
+ }
89
+ catch (error) {
90
+ this.store.db.exec("ROLLBACK");
91
+ throw error;
92
+ }
93
+ return delivery;
94
+ }
95
+ serve() { return this.ports.serve(this); }
96
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { type IncomingMessage, type ServerResponse } from "node:http";
3
+ import { SellerDatabase } from "./seller-database.js";
4
+ export declare class SellerService {
5
+ #private;
6
+ private readonly db;
7
+ private readonly snapshotKey;
8
+ private readonly origin;
9
+ private readonly facilitatorUrl;
10
+ private readonly facilitatorKey;
11
+ private readonly allowedBuyers;
12
+ private readonly directoryAggregator?;
13
+ private constructor();
14
+ static create(config: {
15
+ db: SellerDatabase;
16
+ snapshotKey: string;
17
+ origin: string;
18
+ facilitatorUrl: string;
19
+ facilitatorKey: string;
20
+ allowedBuyers: ReadonlySet<string>;
21
+ claudeKey: string;
22
+ directoryAggregator?: string;
23
+ }): Promise<SellerService>;
24
+ private artifact;
25
+ private resource;
26
+ private persist;
27
+ private publishCiphertext;
28
+ private serialize;
29
+ private body;
30
+ private reply;
31
+ handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
32
+ createServer(): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
33
+ close(): void;
34
+ }
@@ -0,0 +1,422 @@
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", buyerAccess: this.allowedBuyers.size ? "restricted" : "open" });
186
+ if (request.method === "GET" && url.pathname === "/v1/access") {
187
+ const buyerAddress = String(url.searchParams.get("buyerAddress") ?? "").toLowerCase();
188
+ if (!/^0x[0-9a-f]{64}$/.test(buyerAddress))
189
+ fail("SELLER_REQUEST_INVALID", "buyerAddress is invalid");
190
+ return this.reply(response, 200, { buyerAddress, allowed: this.allowedBuyers.size === 0 || this.allowedBuyers.has(buyerAddress), mode: this.allowedBuyers.size ? "restricted" : "open" });
191
+ }
192
+ if (request.method === "GET" && url.pathname === "/v2/directory/health") {
193
+ if (!this.directoryAggregator)
194
+ return this.reply(response, 503, { ok: false, service: "supercuts-directory", reason: "aggregator not configured" });
195
+ const capabilities = await this.db.listDirectory();
196
+ return this.reply(response, 200, { ok: true, service: "supercuts-directory", listings: capabilities.length });
197
+ }
198
+ if (request.method === "GET" && url.pathname === "/v2/directory/catalog") {
199
+ const rawOffset = url.searchParams.get("offset") ?? "0";
200
+ if (!/^(0|[1-9][0-9]*)$/.test(rawOffset))
201
+ fail("SELLER_DIRECTORY_INVALID", "directory offset is invalid");
202
+ const offset = Number(rawOffset), capabilities = await this.db.listDirectory(Date.now(), offset);
203
+ return this.reply(response, 200, { capabilities, ...(capabilities.length === 100 ? { nextOffset: offset + 100 } : {}) });
204
+ }
205
+ if (request.method === "POST" && url.pathname === "/v2/directory/register") {
206
+ if (!this.directoryAggregator)
207
+ fail("SELLER_DIRECTORY_UNAVAILABLE", "directory Walrus aggregator is not configured");
208
+ const body = await this.body(request);
209
+ const registration = body.registration;
210
+ await registerDirectoryCapability(this.db, registration, this.directoryAggregator);
211
+ return this.reply(response, 200, { ok: true, capabilityId: registration.manifest.capabilityId, sellerId: registration.manifest.sellerId, expiresAt: registration.manifest.expiresAt });
212
+ }
213
+ if (request.method === "GET" && url.pathname === "/v1/catalog")
214
+ 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 })) });
215
+ if (request.method === "GET" && url.pathname === "/v2/catalog")
216
+ 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 })) });
217
+ if (request.method === "POST" && url.pathname === "/v2/prepare") {
218
+ const input = await this.body(request);
219
+ return this.serialize(async () => {
220
+ const capabilityId = validId(input.capabilityId, "capabilityId"), item = this.#v2.get(capabilityId) ?? fail("SELLER_CAPABILITY_UNKNOWN", "v2 capability is unknown");
221
+ return this.reply(response, 200, await item.transport.prepareRequest(input));
222
+ });
223
+ }
224
+ if (request.method === "POST" && url.pathname === "/v2/fulfill") {
225
+ const input = await this.body(request);
226
+ return this.serialize(async () => {
227
+ const sessionId = validId(input.sessionId, "sessionId"), sellerId = String(input.sellerId ?? "");
228
+ const transport = this.#v2BySeller.get(sellerId), slot = this.#slots.get(sellerId);
229
+ if (!transport || !slot?.app.store.db.prepare("SELECT 1 FROM seller_deal_prepares WHERE session_id=?").get(sessionId))
230
+ fail("NEGOTIATION_REQUIRED", "seller has no matching prepared v2 deal");
231
+ return this.reply(response, 200, await transport.fulfillRequest(input));
232
+ });
233
+ }
234
+ if (request.method === "GET") {
235
+ const item = [...this.#v2.values()].find(({ row }) => url.pathname.startsWith(`${new URL(row.manifest.endpoint).pathname.replace(/\/$/, "")}/`));
236
+ if (item)
237
+ return this.serialize(async () => item.transport.handle(request, response));
238
+ }
239
+ if (request.method === "GET" && url.pathname === "/v1/events") {
240
+ const runId = validId(url.searchParams.get("runId"), "runId");
241
+ return this.reply(response, 200, { events: await this.db.listEvents(runId) });
242
+ }
243
+ if (request.method === "POST" && url.pathname === "/v1/negotiate") {
244
+ const input = await this.body(request);
245
+ return await this.serialize(async () => {
246
+ 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);
247
+ 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 ?? "");
248
+ if (!/^0x[0-9a-f]{64}$/.test(buyerAddress) || buyerPublicKey.length > 1024 || ciphertext.length > 16384 || !ciphertext || !buyerSignature)
249
+ fail("SELLER_REQUEST_INVALID", "buyer identity or encrypted offer is invalid");
250
+ if (this.allowedBuyers.size > 0 && !this.allowedBuyers.has(buyerAddress))
251
+ fail("SELLER_BUYER_FORBIDDEN", "buyer is outside the seller service allowlist");
252
+ const signedBytes = Buffer.from(`${sessionId}:${capabilityId}:${buyerPublicKey}:${sha256(Buffer.from(ciphertext, "base64"))}`);
253
+ if (!(await isValidPersonalMessageSignature(signedBytes, buyerSignature, { address: buyerAddress })))
254
+ fail("SELLER_BUYER_SIGNATURE_INVALID", "buyer did not authorize this negotiation request");
255
+ 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);
256
+ if (prior) {
257
+ if (prior.capabilityId !== capabilityId || prior.buyerAddress !== buyerAddress || prior.buyerPublicKey !== buyerPublicKey || prior.ciphertext !== ciphertext || prior.runId !== runId)
258
+ fail("NEGOTIATION_SESSION_CONFLICT", "session already contains a different buyer offer");
259
+ return this.reply(response, 200, { sessionId, capabilityId, ciphertext: prior.replyCiphertext, signature: prior.replySignature, requestBlobId: prior.requestBlobId, replyBlobId: prior.replyBlobId, transcriptHash: prior.transcriptHash });
260
+ }
261
+ const sellerPrivateDer = slot.app.store.getSecret("identity:x25519-private") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
262
+ const shared = diffieHellman({ privateKey: createPrivateKey({ key: sellerPrivateDer, format: "der", type: "pkcs8" }), publicKey: createPublicKey({ key: Buffer.from(buyerPublicKey, "base64url"), format: "der", type: "spki" }) });
263
+ const key = Buffer.from(hkdfSync("sha256", shared, Buffer.from(sessionId), Buffer.from("supercuts-negotiation-v1"), 32));
264
+ let offer;
265
+ try {
266
+ offer = asObject(JSON.parse(decryptSessionMessage(key, sessionId, `${sessionId}:buyer:1`, ciphertext).toString()));
267
+ }
268
+ catch (error) {
269
+ return fail("NEGOTIATION_DECRYPT_FAILED", "seller could not read the buyer offer", error);
270
+ }
271
+ 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;
272
+ if (opening <= 0n || opening > maximum || maximum < minimum || maximum > BigInt(row.manifest.priceBand.maximum) || briefHash !== undefined && !/^[0-9a-f]{64}$/.test(briefHash))
273
+ fail("NEGOTIATION_BOUNDS_INVALID", "buyer offer exceeds approved price bounds");
274
+ const task = String(offer.task ?? "").slice(0, 512);
275
+ 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"] } });
276
+ if (decision.action.action !== "counteroffer")
277
+ fail("SELLER_DECISION_INVALID", "seller did not choose the approved response");
278
+ const reply = { capabilityId, amount: minimum.toString(), asset: row.manifest.asset, railOptions: row.manifest.supportedRails, accepted: true };
279
+ const replyCiphertext = encryptSessionMessage(key, sessionId, `${sessionId}:seller:2`, Buffer.from(JSON.stringify(reply)));
280
+ const requestBlobId = await this.publishCiphertext(slot, ciphertext), replyBlobId = await this.publishCiphertext(slot, replyCiphertext);
281
+ const transcriptHash = sha256(JSON.stringify({ request: sha256(Buffer.from(ciphertext, "base64")), reply: sha256(Buffer.from(replyCiphertext, "base64")) }));
282
+ const encodedSigner = slot.app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signing key is missing"), decoded = decodeSuiPrivateKey(encodedSigner);
283
+ if (decoded.scheme !== "ED25519")
284
+ fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
285
+ const signature = (await Ed25519Keypair.fromSecretKey(decoded.secretKey).signPersonalMessage(Buffer.from(`${sessionId}:${capabilityId}:${sha256(Buffer.from(replyCiphertext, "base64"))}:${transcriptHash}`))).signature;
286
+ 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);
287
+ try {
288
+ await this.persist(slot);
289
+ }
290
+ catch (error) {
291
+ this.#healthy = false;
292
+ throw error;
293
+ }
294
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "negotiation", status: "complete", details: { capabilityId, sessionId, requestBlobId, replyBlobId, transcriptHash, model: decision.model } });
295
+ return this.reply(response, 200, { sessionId, capabilityId, ciphertext: replyCiphertext, signature, requestBlobId, replyBlobId, transcriptHash });
296
+ });
297
+ }
298
+ if (request.method === "GET" && url.pathname.startsWith("/resource/")) {
299
+ 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);
300
+ return await this.serialize(async () => this.resource(slot, row).handle(request, response));
301
+ }
302
+ if (request.method === "POST" && url.pathname === "/v1/quote") {
303
+ const input = await this.body(request);
304
+ return await this.serialize(async () => {
305
+ 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);
306
+ if (!row.manifest.supportedRails.includes("x402"))
307
+ fail("SELLER_RAIL_UNSUPPORTED", "capability cannot quote x402");
308
+ const requestId = validId(input.requestId, "requestId"), operationId = validId(input.operationId, "operationId"), runId = validId(input.runId, "runId");
309
+ const negotiation = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,run_id AS runId FROM remote_negotiations WHERE session_id=?").get(requestId);
310
+ if (!negotiation || negotiation.capabilityId !== capabilityId || negotiation.runId !== runId)
311
+ fail("NEGOTIATION_REQUIRED", "exact quote requires the matching encrypted negotiation");
312
+ const briefHash = typeof input.briefHash === "string" ? input.briefHash : undefined;
313
+ if (briefHash !== undefined && !/^[0-9a-f]{64}$/.test(briefHash))
314
+ fail("SELLER_REQUEST_INVALID", "brief hash is invalid");
315
+ const quote = { requestId, operationId, capabilityId, amount: row.manifest.priceBand.minimum, artifactKey: briefHash ? `${capabilityId}:${briefHash}` : capabilityId, runId };
316
+ 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);
317
+ if (prior && JSON.stringify(prior) !== JSON.stringify({ operationId, capabilityId, amount: quote.amount, artifactKey: quote.artifactKey, runId }))
318
+ fail("SELLER_QUOTE_CONFLICT", "request ID identifies another quote");
319
+ 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"] } });
320
+ if (decision.action.action !== "present_quote")
321
+ fail("SELLER_DECISION_INVALID", "seller did not choose the approved quote");
322
+ 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);
323
+ const artifact = this.artifact(slot, quote.artifactKey), registration = this.resource(slot, row).register(requestId, operationId, artifact);
324
+ try {
325
+ await this.persist(slot);
326
+ }
327
+ catch (error) {
328
+ this.#healthy = false;
329
+ throw error;
330
+ }
331
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "quote", status: "complete", details: { capabilityId, amount: quote.amount, model: decision.model } });
332
+ return this.reply(response, 200, { capabilityId, sellerId: slot.sellerId, amount: quote.amount, asset: row.manifest.asset, requestId, operationId, resourceUrl: registration.url, artifactSha256: registration.artifactSha256 });
333
+ });
334
+ }
335
+ if (request.method === "POST" && url.pathname === "/v1/agreement") {
336
+ const input = await this.body(request);
337
+ return await this.serialize(async () => {
338
+ 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);
339
+ if (!row.manifest.supportedRails.includes("shielded"))
340
+ fail("SELLER_RAIL_UNSUPPORTED", "capability cannot use shielded settlement");
341
+ const sessionId = validId(input.sessionId, "sessionId"), runId = validId(input.runId, "runId"), briefHash = String(input.briefHash ?? ""), deliveryPublicKey = String(input.deliveryPublicKey ?? "");
342
+ if (!/^[0-9a-f]{64}$/.test(briefHash) || deliveryPublicKey.length > 1024)
343
+ fail("SELLER_REQUEST_INVALID", "brief hash or delivery key is invalid");
344
+ 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");
345
+ if (negotiation.capabilityId !== capabilityId || negotiation.runId !== runId || negotiation.briefHash !== briefHash)
346
+ fail("NEGOTIATION_REQUIRED", "agreement requires the matching encrypted negotiation");
347
+ const artifactKey = `${capabilityId}:${briefHash}`, artifact = this.artifact(slot, artifactKey), artifactSha256 = sha256(artifact);
348
+ const encoded = String(input.agreement ?? ""), draft = asObject(JSON.parse(Buffer.from(encoded, "base64url").toString()));
349
+ if (draft.counterparty !== slot.sellerId || draft.capabilityId !== capabilityId || draft.manifestHash !== row.manifest.manifestHash || draft.paymentAmount !== row.manifest.priceBand.minimum)
350
+ fail("AGREEMENT_BINDING_MISMATCH", "draft does not match the seller offer");
351
+ if (draft.transcriptRootLo !== BigInt(`0x${negotiation.transcriptHash.slice(0, 32)}`).toString() || draft.transcriptRootHi !== BigInt(`0x${negotiation.transcriptHash.slice(32)}`).toString())
352
+ fail("AGREEMENT_BINDING_MISMATCH", "draft does not bind the encrypted negotiation transcript");
353
+ const expected = sha256(JSON.stringify({ capabilityId, briefHash, artifactSha256, deliveryPublicKey }));
354
+ if (draft.serviceDigestLo !== BigInt(`0x${expected.slice(0, 32)}`).toString() || draft.serviceDigestHi !== BigInt(`0x${expected.slice(32)}`).toString())
355
+ fail("AGREEMENT_BINDING_MISMATCH", "draft does not bind the prepared analysis and buyer delivery key");
356
+ 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);
357
+ if (prior && JSON.stringify(prior) !== JSON.stringify({ capabilityId, artifactKey, deliveryPublicKey, runId }))
358
+ fail("AGREEMENT_SESSION_CONFLICT", "session identifies another delivery");
359
+ 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"] } });
360
+ if (decision.action.action !== "sign_approved_agreement")
361
+ fail("SELLER_DECISION_INVALID", "seller did not choose the approved agreement");
362
+ const signed = await executeMandated(slot.app, loadMandate(slot.home), "offer.counter", { "operation-id": `remote-counter-${sessionId}`, session: sessionId, agreement: encoded });
363
+ 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);
364
+ try {
365
+ await this.persist(slot);
366
+ }
367
+ catch (error) {
368
+ this.#healthy = false;
369
+ throw error;
370
+ }
371
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "agreement", status: "complete", details: { capabilityId, sessionId, model: decision.model, dealCommitment: signed.dealCommitment } });
372
+ return this.reply(response, 200, { sessionId, capabilityId, agreement: signed.agreement, dealCommitment: signed.dealCommitment, artifactSha256 });
373
+ });
374
+ }
375
+ if (request.method === "GET" && url.pathname.startsWith("/v1/delivery/")) {
376
+ const sessionId = validId(decodeURIComponent(url.pathname.slice("/v1/delivery/".length)), "sessionId");
377
+ return await this.serialize(async () => {
378
+ 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");
379
+ 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);
380
+ await executeMandated(slot.app, loadMandate(slot.home), "wallet.recover", { "operation-id": `remote-recover-${sessionId}` });
381
+ const note = slot.app.store.db.prepare("SELECT commitment FROM notes WHERE operation_id=? AND role='payment'").get(sessionId);
382
+ if (!note)
383
+ return this.reply(response, 202, { status: "payment-pending", sessionId });
384
+ const artifact = this.artifact(slot, row.artifactKey);
385
+ const recipient = createPublicKey({ key: Buffer.from(row.deliveryPublicKey, "base64url"), format: "der", type: "spki" });
386
+ const ephemeral = generateKeyPairSync("x25519");
387
+ const shared = diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipient });
388
+ const key = Buffer.from(hkdfSync("sha256", shared, Buffer.from(sessionId), Buffer.from("supercuts-delivery-v1"), 32));
389
+ const nonce = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, nonce);
390
+ cipher.setAAD(Buffer.from(`${sessionId}:${row.capabilityId}:${sha256(artifact)}`));
391
+ const ciphertext = Buffer.concat([cipher.update(artifact), cipher.final()]);
392
+ try {
393
+ await this.persist(slot);
394
+ }
395
+ catch (error) {
396
+ this.#healthy = false;
397
+ throw error;
398
+ }
399
+ 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 } });
400
+ 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") });
401
+ });
402
+ }
403
+ return this.reply(response, 404, { error: "NOT_FOUND" });
404
+ }
405
+ catch (error) {
406
+ const known = error instanceof SupercutsError ? error : new SupercutsError("SELLER_INTERNAL_ERROR", "seller request failed");
407
+ if (!response.headersSent)
408
+ this.reply(response, ["SELLER_STATE_UNAVAILABLE", "SELLER_DIRECTORY_UNAVAILABLE", "SELLER_DB_UNAVAILABLE"].includes(known.code) ? 503 : 400, { error: known.code, message: known.message });
409
+ }
410
+ }
411
+ createServer() { return createServer((request, response) => { void this.handle(request, response); }); }
412
+ close() { for (const slot of this.#slots.values())
413
+ slot.app.close(); rmSync(this.#root, { recursive: true, force: true }); }
414
+ }
415
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
416
+ const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
417
+ const port = Number(process.env.PORT ?? "3000");
418
+ const origin = required("SELLER_ORIGIN");
419
+ 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((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
420
+ .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); })
421
+ .catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller service failed"}\n`); process.exitCode = 1; });
422
+ }
@@ -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 {};