@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.
- package/LICENSE +201 -0
- package/README.md +249 -2
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +157 -0
- package/dist/backend.d.ts +27 -0
- package/dist/backend.js +62 -0
- package/dist/buyer-setup.d.ts +6 -0
- package/dist/buyer-setup.js +241 -0
- package/dist/capability.d.ts +32 -0
- package/dist/capability.js +87 -0
- package/dist/claude.d.ts +44 -0
- package/dist/claude.js +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +29 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +58 -0
- package/dist/dashboard.d.ts +5 -0
- package/dist/dashboard.js +21 -0
- package/dist/deal-adapter.d.ts +3 -0
- package/dist/deal-adapter.js +39 -0
- package/dist/deal-engine.d.ts +62 -0
- package/dist/deal-engine.js +211 -0
- package/dist/deal-wire.d.ts +16 -0
- package/dist/deal-wire.js +65 -0
- package/dist/deal.d.ts +79 -0
- package/dist/deal.js +228 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +449 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +13 -0
- package/dist/events.d.ts +27 -0
- package/dist/events.js +68 -0
- package/dist/facilitator.d.ts +21 -0
- package/dist/facilitator.js +173 -0
- package/dist/finality.d.ts +2 -0
- package/dist/finality.js +27 -0
- package/dist/hosted-exit-cli.d.ts +2 -0
- package/dist/hosted-exit-cli.js +37 -0
- package/dist/hosted-exit.d.ts +24 -0
- package/dist/hosted-exit.js +70 -0
- package/dist/hosted-seller-import.d.ts +2 -0
- package/dist/hosted-seller-import.js +70 -0
- package/dist/hosted-seller-repository.d.ts +31 -0
- package/dist/hosted-seller-repository.js +214 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/indexer.d.ts +63 -0
- package/dist/indexer.js +60 -0
- package/dist/live-deal-ports.d.ts +41 -0
- package/dist/live-deal-ports.js +212 -0
- package/dist/live-index.d.ts +28 -0
- package/dist/live-index.js +149 -0
- package/dist/live-runtime.d.ts +82 -0
- package/dist/live-runtime.js +570 -0
- package/dist/live-seller-transport.d.ts +30 -0
- package/dist/live-seller-transport.js +169 -0
- package/dist/live-seller-verifier.d.ts +11 -0
- package/dist/live-seller-verifier.js +65 -0
- package/dist/mandate.d.ts +34 -0
- package/dist/mandate.js +178 -0
- package/dist/mcp-bootstrap.d.ts +2 -0
- package/dist/mcp-bootstrap.js +39 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +187 -0
- package/dist/memory-worker.d.ts +2 -0
- package/dist/memory-worker.js +38 -0
- package/dist/memory.d.ts +15 -0
- package/dist/memory.js +17 -0
- package/dist/memwal.d.ts +35 -0
- package/dist/memwal.js +83 -0
- package/dist/negotiation.d.ts +23 -0
- package/dist/negotiation.js +58 -0
- package/dist/notes.d.ts +11 -0
- package/dist/notes.js +26 -0
- package/dist/onboarding-doctor.d.ts +1 -0
- package/dist/onboarding-doctor.js +76 -0
- package/dist/phase3.d.ts +100 -0
- package/dist/phase3.js +1144 -0
- package/dist/procurement.d.ts +8 -0
- package/dist/procurement.js +19 -0
- package/dist/protocol-engine.d.ts +121 -0
- package/dist/protocol-engine.js +149 -0
- package/dist/prover.d.ts +28 -0
- package/dist/prover.js +70 -0
- package/dist/relayer.d.ts +32 -0
- package/dist/relayer.js +43 -0
- package/dist/remote-buyer.d.ts +67 -0
- package/dist/remote-buyer.js +231 -0
- package/dist/resource-server.d.ts +2 -0
- package/dist/resource-server.js +24 -0
- package/dist/resource.d.ts +22 -0
- package/dist/resource.js +55 -0
- package/dist/seam.d.ts +2 -0
- package/dist/seam.js +117 -0
- package/dist/seller-catalog-generate.d.ts +2 -0
- package/dist/seller-catalog-generate.js +68 -0
- package/dist/seller-database.d.ts +41 -0
- package/dist/seller-database.js +80 -0
- package/dist/seller-directory-cli.d.ts +2 -0
- package/dist/seller-directory-cli.js +24 -0
- package/dist/seller-directory.d.ts +5 -0
- package/dist/seller-directory.js +50 -0
- package/dist/seller-live-cli.d.ts +2 -0
- package/dist/seller-live-cli.js +53 -0
- package/dist/seller-provision.d.ts +2 -0
- package/dist/seller-provision.js +84 -0
- package/dist/seller-register-cli.d.ts +2 -0
- package/dist/seller-register-cli.js +40 -0
- package/dist/seller-runtime.d.ts +60 -0
- package/dist/seller-runtime.js +96 -0
- package/dist/seller-service.d.ts +34 -0
- package/dist/seller-service.js +416 -0
- package/dist/seller-state.d.ts +19 -0
- package/dist/seller-state.js +60 -0
- package/dist/seller-stdio.d.ts +6 -0
- package/dist/seller-stdio.js +29 -0
- package/dist/seller-v2-provision.d.ts +2 -0
- package/dist/seller-v2-provision.js +71 -0
- package/dist/seller-watch.d.ts +2 -0
- package/dist/seller-watch.js +45 -0
- package/dist/settlement.d.ts +95 -0
- package/dist/settlement.js +141 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +193 -0
- package/dist/sui.d.ts +49 -0
- package/dist/sui.js +77 -0
- package/dist/tatum.d.ts +17 -0
- package/dist/tatum.js +83 -0
- package/dist/transcript.d.ts +31 -0
- package/dist/transcript.js +52 -0
- package/dist/v2-exit-relayer.d.ts +34 -0
- package/dist/v2-exit-relayer.js +102 -0
- package/dist/v2-exit-worker.d.ts +2 -0
- package/dist/v2-exit-worker.js +97 -0
- package/dist/v2-exits.d.ts +30 -0
- package/dist/v2-exits.js +78 -0
- package/dist/v2-prefund.d.ts +19 -0
- package/dist/v2-prefund.js +43 -0
- package/dist/v2-relayer-cli.d.ts +2 -0
- package/dist/v2-relayer-cli.js +39 -0
- package/dist/wallet.d.ts +32 -0
- package/dist/wallet.js +90 -0
- package/dist/workflow-memory.d.ts +22 -0
- package/dist/workflow-memory.js +49 -0
- package/dist/x402-resource-service.d.ts +34 -0
- package/dist/x402-resource-service.js +154 -0
- package/dist/x402.d.ts +46 -0
- package/dist/x402.js +136 -0
- package/package.json +60 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { fail } from "./errors.js";
|
|
2
|
+
/** Server-only PostgREST access. The service-role key must never reach a buyer tool. */
|
|
3
|
+
export class SellerDatabase {
|
|
4
|
+
origin;
|
|
5
|
+
serviceKey;
|
|
6
|
+
http;
|
|
7
|
+
constructor(origin, serviceKey, http = fetch) {
|
|
8
|
+
this.origin = origin;
|
|
9
|
+
this.serviceKey = serviceKey;
|
|
10
|
+
this.http = http;
|
|
11
|
+
const url = new URL(origin);
|
|
12
|
+
if (url.protocol !== "https:")
|
|
13
|
+
fail("SELLER_DB_INVALID", "Supabase URL must use HTTPS");
|
|
14
|
+
if (!serviceKey)
|
|
15
|
+
fail("SELLER_DB_INVALID", "Supabase service key is required");
|
|
16
|
+
}
|
|
17
|
+
async request(path, init = {}) {
|
|
18
|
+
const response = await this.http(new URL(`/rest/v1/${path}`, this.origin), {
|
|
19
|
+
...init,
|
|
20
|
+
headers: { apikey: this.serviceKey, Authorization: `Bearer ${this.serviceKey}`, ...(init.body ? { "content-type": "application/json" } : {}), ...(init.headers ?? {}) },
|
|
21
|
+
signal: init.signal ?? AbortSignal.timeout(30_000),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
await response.arrayBuffer();
|
|
25
|
+
fail("SELLER_DB_UNAVAILABLE", `Supabase returned ${response.status}`);
|
|
26
|
+
}
|
|
27
|
+
const body = await response.text();
|
|
28
|
+
return body ? JSON.parse(body) : undefined;
|
|
29
|
+
}
|
|
30
|
+
async getSnapshot(sellerId) {
|
|
31
|
+
const rows = await this.request(`seller_state?seller_id=eq.${encodeURIComponent(sellerId)}&select=payload,revision`);
|
|
32
|
+
return rows[0];
|
|
33
|
+
}
|
|
34
|
+
async putSnapshot(snapshot, expectedRevision) {
|
|
35
|
+
if (expectedRevision === undefined) {
|
|
36
|
+
await this.request("seller_state?on_conflict=seller_id", { method: "POST", headers: { Prefer: "resolution=merge-duplicates,return=minimal" }, body: JSON.stringify({ seller_id: snapshot.sellerId, payload: snapshot, revision: 0, updated_at: new Date().toISOString() }) });
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const rows = await this.request(`seller_state?seller_id=eq.${encodeURIComponent(snapshot.sellerId)}&revision=eq.${expectedRevision}&select=revision`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: JSON.stringify({ payload: snapshot, revision: expectedRevision + 1, updated_at: new Date().toISOString() }) });
|
|
40
|
+
if (!Array.isArray(rows) || rows.length !== 1 || rows[0]?.revision !== expectedRevision + 1)
|
|
41
|
+
fail("SELLER_STATE_CONFLICT", "another seller instance updated the durable snapshot");
|
|
42
|
+
return expectedRevision + 1;
|
|
43
|
+
}
|
|
44
|
+
async listCatalog() {
|
|
45
|
+
return await this.request("seller_catalog?select=seller_id,capability_id,manifest,blob_id,signature,transport_public_key,preview,updated_at&order=capability_id.asc");
|
|
46
|
+
}
|
|
47
|
+
async listDirectory(now = Date.now(), offset = 0) {
|
|
48
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 100_000)
|
|
49
|
+
fail("SELLER_DIRECTORY_INVALID", "directory offset is invalid");
|
|
50
|
+
const rows = await this.request(`seller_directory?expires_at=gt.${now}&select=registration&order=capability_id.asc&limit=100&offset=${offset}`);
|
|
51
|
+
return rows.map((row) => row.registration);
|
|
52
|
+
}
|
|
53
|
+
async putDirectory(registration) {
|
|
54
|
+
const manifest = registration.manifest;
|
|
55
|
+
const existing = await this.request(`seller_directory?capability_id=eq.${encodeURIComponent(manifest.capabilityId)}&select=seller_id,registration`);
|
|
56
|
+
if (existing[0] && existing[0].seller_id !== manifest.sellerId)
|
|
57
|
+
fail("SELLER_DIRECTORY_CONFLICT", "capability ID belongs to another seller");
|
|
58
|
+
await this.request("seller_directory?on_conflict=capability_id", { method: "POST", headers: { Prefer: "resolution=merge-duplicates,return=minimal" }, body: JSON.stringify({ capability_id: manifest.capabilityId, seller_id: manifest.sellerId, registration, endpoint_origin: new URL(manifest.endpoint).origin, expires_at: manifest.expiresAt, updated_at: new Date().toISOString() }) });
|
|
59
|
+
}
|
|
60
|
+
async putCatalog(row) {
|
|
61
|
+
await this.request("seller_catalog?on_conflict=capability_id", { method: "POST", headers: { Prefer: "resolution=merge-duplicates,return=minimal" }, body: JSON.stringify({ ...row, updated_at: new Date().toISOString() }) });
|
|
62
|
+
}
|
|
63
|
+
async addEvent(row) {
|
|
64
|
+
await this.request("seller_events", { method: "POST", headers: { Prefer: "return=minimal" }, body: JSON.stringify({ ...row, created_at: new Date().toISOString() }) });
|
|
65
|
+
}
|
|
66
|
+
async listEvents(runId) {
|
|
67
|
+
return await this.request(`seller_events?run_id=eq.${encodeURIComponent(runId)}&select=seller_id,stage,status,details,created_at&order=id.asc`);
|
|
68
|
+
}
|
|
69
|
+
async listPaymentClaims() {
|
|
70
|
+
return await this.request("seller_payment_claims?select=digest,request_id,seller_id&order=digest.asc");
|
|
71
|
+
}
|
|
72
|
+
async claimPayment(digest, requestId, sellerId) {
|
|
73
|
+
const inserted = await this.request("seller_payment_claims?on_conflict=digest&select=digest,request_id,seller_id", { method: "POST", headers: { Prefer: "resolution=ignore-duplicates,return=representation" }, body: JSON.stringify({ digest, request_id: requestId, seller_id: sellerId }) });
|
|
74
|
+
if (Array.isArray(inserted) && inserted.length === 1)
|
|
75
|
+
return;
|
|
76
|
+
const existing = await this.request(`seller_payment_claims?digest=eq.${encodeURIComponent(digest)}&select=request_id,seller_id`);
|
|
77
|
+
if (existing[0]?.request_id !== requestId || existing[0]?.seller_id !== sellerId)
|
|
78
|
+
fail("PAYMENT_CONSUMED", "payment digest is already claimed by another resource");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { verifySignedCapability } from "./capability.js";
|
|
5
|
+
import { fail } from "./errors.js";
|
|
6
|
+
async function main() {
|
|
7
|
+
const [command, file, directory] = process.argv.slice(2);
|
|
8
|
+
if (command !== "publish" || !file || !directory)
|
|
9
|
+
fail("ARGUMENT_REQUIRED", "usage: supercuts-seller-directory publish <owner-only-registration.json> <directory-origin>");
|
|
10
|
+
const path = resolve(file);
|
|
11
|
+
if ((statSync(path).mode & 0o077) !== 0)
|
|
12
|
+
fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
|
|
13
|
+
const registration = JSON.parse(readFileSync(path, "utf8"));
|
|
14
|
+
await verifySignedCapability(registration);
|
|
15
|
+
const url = new URL(directory);
|
|
16
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
|
|
17
|
+
fail("SELLER_DIRECTORY_INVALID", "directory origin must be HTTPS or loopback HTTP without path or credentials");
|
|
18
|
+
const response = await fetch(new URL("/v2/directory/register", url), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ registration }), redirect: "error", signal: AbortSignal.timeout(90_000) });
|
|
19
|
+
const result = await response.json();
|
|
20
|
+
if (!response.ok || result.ok !== true)
|
|
21
|
+
fail("SELLER_DIRECTORY_UNAVAILABLE", String(result.message ?? `directory returned HTTP ${response.status}`));
|
|
22
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
23
|
+
}
|
|
24
|
+
main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "directory publication failed"}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type SignedCapabilityRegistration } from "./capability.js";
|
|
2
|
+
import type { SellerDatabase } from "./seller-database.js";
|
|
3
|
+
export declare function validateDirectoryOrigin(endpoint: string, allowLoopback?: boolean): Promise<URL>;
|
|
4
|
+
/** The directory stores signed public metadata, never a seller role home. */
|
|
5
|
+
export declare function registerDirectoryCapability(db: Pick<SellerDatabase, "putDirectory">, registration: SignedCapabilityRegistration, aggregator: string, http?: typeof fetch, allowLoopback?: boolean): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { verifySignedCapability } from "./capability.js";
|
|
4
|
+
import { fail } from "./errors.js";
|
|
5
|
+
import { canonicalJson } from "./settlement.js";
|
|
6
|
+
function publicAddress(address) {
|
|
7
|
+
if (address.includes(":"))
|
|
8
|
+
return !/^(::|::1$|fc|fd|fe80:|2001:db8:)/i.test(address) && !address.startsWith("::ffff:");
|
|
9
|
+
const octets = address.split(".").map(Number);
|
|
10
|
+
return octets.length === 4 && octets.every((part) => Number.isInteger(part) && part >= 0 && part <= 255)
|
|
11
|
+
&& octets[0] !== 0 && octets[0] !== 10 && octets[0] !== 127 && octets[0] < 224
|
|
12
|
+
&& !(octets[0] === 169 && octets[1] === 254) && !(octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31)
|
|
13
|
+
&& !(octets[0] === 192 && octets[1] === 168) && !(octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127);
|
|
14
|
+
}
|
|
15
|
+
export async function validateDirectoryOrigin(endpoint, allowLoopback = false) {
|
|
16
|
+
const url = new URL(endpoint);
|
|
17
|
+
if (url.username || url.password || url.hash || url.search || url.port && url.port !== "443" && !allowLoopback)
|
|
18
|
+
fail("SELLER_ENDPOINT_UNSAFE", "directory endpoint has credentials, query, fragment, or nonstandard port");
|
|
19
|
+
if (allowLoopback && url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname))
|
|
20
|
+
return url;
|
|
21
|
+
if (url.protocol !== "https:" || url.hostname === "localhost" || url.hostname.endsWith(".localhost"))
|
|
22
|
+
fail("SELLER_ENDPOINT_UNSAFE", "directory endpoint must use a public HTTPS origin");
|
|
23
|
+
const addresses = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true });
|
|
24
|
+
if (!addresses.length || addresses.some(({ address }) => !publicAddress(address)))
|
|
25
|
+
fail("SELLER_ENDPOINT_UNSAFE", "directory endpoint resolves to a nonpublic address");
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
/** The directory stores signed public metadata, never a seller role home. */
|
|
29
|
+
export async function registerDirectoryCapability(db, registration, aggregator, http = fetch, allowLoopback = false) {
|
|
30
|
+
await verifySignedCapability(registration);
|
|
31
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(registration.manifestBlobId))
|
|
32
|
+
fail("WALRUS_BLOB_INVALID", "directory registration has an invalid Walrus blob ID");
|
|
33
|
+
const endpoint = await validateDirectoryOrigin(registration.manifest.endpoint, allowLoopback);
|
|
34
|
+
const read = async (url, limit) => {
|
|
35
|
+
const response = await http(url, { redirect: "error", signal: AbortSignal.timeout(15_000) });
|
|
36
|
+
if (!response.ok)
|
|
37
|
+
fail("SELLER_DIRECTORY_UNAVAILABLE", `registration verification returned HTTP ${response.status}`);
|
|
38
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
39
|
+
if (!bytes.length || bytes.length > limit)
|
|
40
|
+
fail("SELLER_DIRECTORY_INVALID", "registration verification response is too large");
|
|
41
|
+
return JSON.parse(bytes.toString());
|
|
42
|
+
};
|
|
43
|
+
const blob = await read(new URL(`/v1/blobs/${encodeURIComponent(registration.manifestBlobId)}`, aggregator), 128 * 1024);
|
|
44
|
+
if (canonicalJson(blob) !== canonicalJson(registration.manifest))
|
|
45
|
+
fail("CAPABILITY_BLOB_MISMATCH", "Walrus manifest differs from the signed registration");
|
|
46
|
+
const catalog = await read(new URL("/v2/catalog", endpoint.origin), 512 * 1024);
|
|
47
|
+
if (!Array.isArray(catalog.capabilities) || catalog.capabilities.length > 100 || !catalog.capabilities.some((item) => canonicalJson(item) === canonicalJson(registration)))
|
|
48
|
+
fail("SELLER_DIRECTORY_UNAVAILABLE", "seller endpoint does not serve the exact signed registration");
|
|
49
|
+
await db.putDirectory(registration);
|
|
50
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { fail } from "./errors.js";
|
|
6
|
+
import { Phase3App, errorOutput } from "./phase3.js";
|
|
7
|
+
import { LiveSellerTransport } from "./live-seller-transport.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
const home = process.argv[2], configPath = process.argv[3];
|
|
10
|
+
if (!home || !configPath)
|
|
11
|
+
return fail("ARGUMENT_REQUIRED", "usage: supercuts-seller-live <seller-home> <owner-only-config.json>");
|
|
12
|
+
if ((statSync(configPath).mode & 0o077) !== 0)
|
|
13
|
+
fail("SELLER_CONFIG_PERMISSIONS", "seller config must be owner-only");
|
|
14
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
15
|
+
if (!Array.isArray(config.capabilities) || !config.capabilities.length)
|
|
16
|
+
fail("SELLER_CONFIG_INVALID", "seller config must name registered capabilities and handlers");
|
|
17
|
+
const app = new Phase3App(home), transport = new LiveSellerTransport(app, config);
|
|
18
|
+
try {
|
|
19
|
+
for (const item of config.capabilities) {
|
|
20
|
+
let registration = item.registration;
|
|
21
|
+
if (item.registrationFile) {
|
|
22
|
+
const registrationPath = resolve(item.registrationFile);
|
|
23
|
+
if ((statSync(registrationPath).mode & 0o077) !== 0)
|
|
24
|
+
fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
|
|
25
|
+
registration = JSON.parse(readFileSync(registrationPath, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
if (!registration)
|
|
28
|
+
return fail("SELLER_CONFIG_INVALID", "capability requires registration or registrationFile");
|
|
29
|
+
const path = resolve(item.handler);
|
|
30
|
+
if ((statSync(path).mode & 0o077) !== 0)
|
|
31
|
+
fail("SELLER_HANDLER_PERMISSIONS", "seller handler module must be owner-only");
|
|
32
|
+
const loaded = await import(pathToFileURL(path).href);
|
|
33
|
+
const handler = loaded.default ?? loaded.handle;
|
|
34
|
+
if (typeof handler !== "function")
|
|
35
|
+
return fail("SELLER_HANDLER_INVALID", "seller handler must export default or handle(request)");
|
|
36
|
+
await transport.register(registration, handler);
|
|
37
|
+
}
|
|
38
|
+
const origin = new URL(config.origin), server = transport.createServer();
|
|
39
|
+
const host = config.listenHost ?? "127.0.0.1", port = config.listenPort ?? (origin.protocol === "http:" && ["127.0.0.1", "localhost", "::1"].includes(origin.hostname) ? Number(origin.port || 80) : 8788);
|
|
40
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
41
|
+
fail("SELLER_CONFIG_INVALID", "seller listenPort must be a valid TCP port");
|
|
42
|
+
await new Promise((done) => server.listen(port, host, done));
|
|
43
|
+
process.stdout.write(`${JSON.stringify({ ok: true, service: "supercuts-seller-live", origin: config.origin, capabilities: transport.runtime.catalog().map((item) => item.manifest.capabilityId) })}\n`);
|
|
44
|
+
const stop = () => server.close(() => { app.close(); process.exit(0); });
|
|
45
|
+
process.on("SIGINT", stop);
|
|
46
|
+
process.on("SIGTERM", stop);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
app.close();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
main().catch((error) => { process.stdout.write(`${JSON.stringify(errorOutput(error))}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { createHash } from "node:crypto";
|
|
7
|
+
import { validateCapabilityManifest } from "./capability.js";
|
|
8
|
+
import { fail } from "./errors.js";
|
|
9
|
+
import { executeMandated, loadMandate } from "./mandate.js";
|
|
10
|
+
import { Phase3App } from "./phase3.js";
|
|
11
|
+
import { SellerDatabase } from "./seller-database.js";
|
|
12
|
+
import { captureSellerHome, encryptSellerSnapshot } from "./seller-state.js";
|
|
13
|
+
async function main() {
|
|
14
|
+
const path = process.argv[2] ? resolve(process.argv[2]) : fail("ARGUMENT_REQUIRED", "usage: supercuts-seller-provision <owner-only-config.json>");
|
|
15
|
+
if ((statSync(path).mode & 0o077) !== 0)
|
|
16
|
+
fail("CREDENTIAL_PERMISSIONS", "seller provision config must be owner-only");
|
|
17
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
if (!Array.isArray(config.sellers) || config.sellers.length !== 3 || new Set(config.sellers.map((seller) => seller.sellerId)).size !== 3)
|
|
19
|
+
fail("SELLER_CONFIG_INVALID", "exactly three distinct sellers are required");
|
|
20
|
+
const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
|
|
21
|
+
const db = new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY"));
|
|
22
|
+
const key = required("SELLER_STATE_KEY");
|
|
23
|
+
for (const seller of config.sellers) {
|
|
24
|
+
const app = new Phase3App(resolve(seller.home));
|
|
25
|
+
try {
|
|
26
|
+
if (app.profile.mock || app.profile.role !== "seller")
|
|
27
|
+
fail("LIVE_PROFILE_REQUIRED", "provision only live seller role homes");
|
|
28
|
+
const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller Sui key is missing");
|
|
29
|
+
const decoded = decodeSuiPrivateKey(encoded);
|
|
30
|
+
if (decoded.scheme !== "ED25519")
|
|
31
|
+
fail("SELLER_IDENTITY_MISMATCH", "seller requires an ED25519 signer");
|
|
32
|
+
const signer = Ed25519Keypair.fromSecretKey(decoded.secretKey);
|
|
33
|
+
if (signer.toSuiAddress() !== seller.sellerId)
|
|
34
|
+
fail("SELLER_IDENTITY_MISMATCH", "seller ID must equal its Sui signer address");
|
|
35
|
+
const artifacts = {};
|
|
36
|
+
for (const capability of seller.capabilities) {
|
|
37
|
+
const manifest = capability.manifest;
|
|
38
|
+
validateCapabilityManifest(manifest);
|
|
39
|
+
if (manifest.sellerId !== seller.sellerId)
|
|
40
|
+
fail("SELLER_IDENTITY_MISMATCH", "manifest seller ID differs from its role home");
|
|
41
|
+
if (capability.artifact) {
|
|
42
|
+
const body = readFileSync(resolve(capability.artifact));
|
|
43
|
+
if (manifest.capability === "sui.market-brief") {
|
|
44
|
+
let report;
|
|
45
|
+
try {
|
|
46
|
+
report = JSON.parse(body.toString());
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return fail("BRIEF_INVALID", "market brief must be JSON", error);
|
|
50
|
+
}
|
|
51
|
+
if (typeof report.title !== "string" || typeof report.body !== "string" || !Array.isArray(report.citations) || report.citations.length < 2 || report.citations.some((value) => typeof value !== "string" || !/^https:\/\//.test(value)))
|
|
52
|
+
fail("BRIEF_INVALID", "market brief needs a title, body, and at least two HTTPS citations");
|
|
53
|
+
}
|
|
54
|
+
artifacts[manifest.capabilityId] = body.toString("base64url");
|
|
55
|
+
}
|
|
56
|
+
for (const [briefHash, artifactPath] of Object.entries(capability.variants ?? {})) {
|
|
57
|
+
if (!/^[0-9a-f]{64}$/.test(briefHash))
|
|
58
|
+
fail("SELLER_CONFIG_INVALID", "variant brief hash is invalid");
|
|
59
|
+
artifacts[`${manifest.capabilityId}:${briefHash}`] = readFileSync(resolve(artifactPath)).toString("base64url");
|
|
60
|
+
}
|
|
61
|
+
if (!capability.artifact && !Object.keys(capability.variants ?? {}).length)
|
|
62
|
+
fail("SELLER_ARTIFACT_MISSING", "capability needs a prepared artifact or brief variants");
|
|
63
|
+
const published = await executeMandated(app, loadMandate(seller.home), "capability.publish", { "operation-id": `publish-${manifest.manifestHash}`, manifest });
|
|
64
|
+
const blobId = String(published.blobId ?? "");
|
|
65
|
+
if (!blobId)
|
|
66
|
+
fail("WALRUS_PUBLISH_FAILED", "capability publish returned no blob ID");
|
|
67
|
+
const hashes = {};
|
|
68
|
+
for (const [artifactKey, value] of Object.entries(artifacts))
|
|
69
|
+
if (artifactKey === manifest.capabilityId || artifactKey.startsWith(`${manifest.capabilityId}:`))
|
|
70
|
+
hashes[artifactKey.slice(manifest.capabilityId.length).replace(/^:/, "") || "default"] = createHash("sha256").update(Buffer.from(value, "base64url")).digest("hex");
|
|
71
|
+
const preview = { ...capability.preview, artifactHashes: hashes };
|
|
72
|
+
const transportPublicKey = app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
|
|
73
|
+
const signature = (await signer.signPersonalMessage(Buffer.from(`${manifest.manifestHash}:${transportPublicKey}`))).signature;
|
|
74
|
+
await db.putCatalog({ seller_id: seller.sellerId, capability_id: manifest.capabilityId, manifest, blob_id: blobId, signature, transport_public_key: transportPublicKey, preview });
|
|
75
|
+
}
|
|
76
|
+
await db.putSnapshot(encryptSellerSnapshot(await captureSellerHome(seller.home, seller.sellerId, artifacts), key));
|
|
77
|
+
process.stdout.write(`${JSON.stringify({ sellerId: seller.sellerId, capabilities: seller.capabilities.map((item) => item.manifest.capabilityId), status: "provisioned" })}\n`);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
app.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "provision failed"}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, statSync, writeFileSync } 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 { createCapabilityManifest } from "./capability.js";
|
|
7
|
+
import { fail } from "./errors.js";
|
|
8
|
+
import { Phase3App, errorOutput } from "./phase3.js";
|
|
9
|
+
import { executeMandated, loadMandate } from "./mandate.js";
|
|
10
|
+
async function main() {
|
|
11
|
+
const home = process.argv[2], specPath = process.argv[3], outputPath = process.argv[4];
|
|
12
|
+
if (!home || !specPath || !outputPath)
|
|
13
|
+
return fail("ARGUMENT_REQUIRED", "usage: supercuts-seller-register <seller-home> <capability-spec.json> <registration.json>");
|
|
14
|
+
if ((statSync(specPath).mode & 0o077) !== 0)
|
|
15
|
+
fail("SELLER_CONFIG_PERMISSIONS", "capability spec must be owner-only");
|
|
16
|
+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
17
|
+
const app = new Phase3App(home);
|
|
18
|
+
try {
|
|
19
|
+
if (app.profile.role !== "seller" || app.profile.mock)
|
|
20
|
+
fail("SELLER_LIVE_REQUIRED", "registration requires a live seller role home");
|
|
21
|
+
const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
|
|
22
|
+
const decoded = decodeSuiPrivateKey(encoded);
|
|
23
|
+
if (decoded.scheme !== "ED25519")
|
|
24
|
+
fail("SIGNER_UNSUPPORTED", "seller requires an ED25519 signer");
|
|
25
|
+
const signer = Ed25519Keypair.fromSecretKey(decoded.secretKey), manifest = createCapabilityManifest({ ...spec, sellerId: signer.toSuiAddress() });
|
|
26
|
+
const published = await executeMandated(app, loadMandate(home), "capability.publish", { "operation-id": `publish-${manifest.manifestHash}`, manifest });
|
|
27
|
+
const blobId = String(published.blobId ?? "");
|
|
28
|
+
if (!blobId)
|
|
29
|
+
fail("WALRUS_PUBLISH_FAILED", "capability publication omitted a Walrus blob ID");
|
|
30
|
+
const transportPublicKey = app.store.getSecret("identity:x25519-public")?.toString("base64url") ?? fail("IDENTITY_MISSING", "seller transport key is missing");
|
|
31
|
+
const sellerSignature = (await signer.signPersonalMessage(Buffer.from(`${manifest.manifestHash}:${transportPublicKey}`))).signature;
|
|
32
|
+
const registration = { manifest, manifestBlobId: blobId, transportPublicKey, sellerSignature };
|
|
33
|
+
writeFileSync(resolve(outputPath), `${JSON.stringify(registration, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
34
|
+
process.stdout.write(`${JSON.stringify({ ok: true, capabilityId: manifest.capabilityId, manifestBlobId: blobId, registrationFile: resolve(outputPath) })}\n`);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
app.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
main().catch((error) => { process.stdout.write(`${JSON.stringify(errorOutput(error))}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type SignedCapabilityRegistration } from "./capability.js";
|
|
2
|
+
import type { RoleStore } from "./store.js";
|
|
3
|
+
export interface ServiceRequest {
|
|
4
|
+
dealId: string;
|
|
5
|
+
capabilityId: string;
|
|
6
|
+
input: Record<string, unknown>;
|
|
7
|
+
requestId: string;
|
|
8
|
+
paymentDigest: string;
|
|
9
|
+
agreementHash: string;
|
|
10
|
+
agreedAmount: string;
|
|
11
|
+
rail: "shielded" | "x402";
|
|
12
|
+
}
|
|
13
|
+
export interface ServiceResult {
|
|
14
|
+
artifact: Uint8Array;
|
|
15
|
+
contentType: string;
|
|
16
|
+
metadata: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export type ServiceHandler = (request: ServiceRequest) => Promise<ServiceResult>;
|
|
19
|
+
export interface VerifiedSellerPayment {
|
|
20
|
+
finalized: true;
|
|
21
|
+
tatumVerified: true;
|
|
22
|
+
digest: string;
|
|
23
|
+
agreementHash: string;
|
|
24
|
+
sellerId: string;
|
|
25
|
+
asset: string;
|
|
26
|
+
amount: string;
|
|
27
|
+
rail: "shielded" | "x402";
|
|
28
|
+
}
|
|
29
|
+
export interface SellerRuntimePorts {
|
|
30
|
+
readBlob(blobId: string): Promise<Uint8Array>;
|
|
31
|
+
verifyPayment(request: ServiceRequest): Promise<VerifiedSellerPayment>;
|
|
32
|
+
encryptAndPublish(request: ServiceRequest, result: ServiceResult): Promise<{
|
|
33
|
+
blobId: string;
|
|
34
|
+
ciphertext: Uint8Array;
|
|
35
|
+
}>;
|
|
36
|
+
serve(runtime: SellerRuntime): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export interface SellerDelivery {
|
|
39
|
+
dealId: string;
|
|
40
|
+
requestId: string;
|
|
41
|
+
blobId: string;
|
|
42
|
+
ciphertextHash: string;
|
|
43
|
+
artifactSha256: string;
|
|
44
|
+
contentType: string;
|
|
45
|
+
metadata: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/** Framework-neutral seller execution boundary. The trusted payment verifier
|
|
48
|
+
* must use chain finality and exact agreement binding, never a buyer's digest
|
|
49
|
+
* alone. The transport must decrypt/authenticate task input before calling
|
|
50
|
+
* fulfill and encrypt the artifact before Walrus publication. */
|
|
51
|
+
export declare class SellerRuntime {
|
|
52
|
+
#private;
|
|
53
|
+
private readonly store;
|
|
54
|
+
private readonly ports;
|
|
55
|
+
constructor(store: RoleStore, ports: SellerRuntimePorts);
|
|
56
|
+
registerCapability(registration: SignedCapabilityRegistration, handler: ServiceHandler): Promise<void>;
|
|
57
|
+
catalog(): Array<SignedCapabilityRegistration>;
|
|
58
|
+
fulfill(request: ServiceRequest): Promise<SellerDelivery>;
|
|
59
|
+
serve(): Promise<void>;
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|