@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
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DeploymentConfig {
|
|
2
|
+
network: "testnet";
|
|
3
|
+
chainId: string;
|
|
4
|
+
packageId: string;
|
|
5
|
+
poolId: string;
|
|
6
|
+
coinType: string;
|
|
7
|
+
coinTypeHash: string;
|
|
8
|
+
deploymentDomain: string;
|
|
9
|
+
verifierVersion: number;
|
|
10
|
+
deploymentCheckpoint: bigint;
|
|
11
|
+
initialRoot: string;
|
|
12
|
+
tatumGrpcEndpoint: string;
|
|
13
|
+
tatumApiKey: string;
|
|
14
|
+
writeGrpcEndpoint: string;
|
|
15
|
+
walrusPublisherUrl: string;
|
|
16
|
+
walrusAggregatorUrl: string;
|
|
17
|
+
x402FacilitatorUrl: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseDeploymentConfig(value: Record<string, unknown>): DeploymentConfig;
|
|
20
|
+
export declare function loadStateKey(path: string): Buffer;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { fail } from "./errors.js";
|
|
3
|
+
const HEX_32 = /^(?:0x)?[0-9a-f]{64}$/i;
|
|
4
|
+
const SUI_ID = /^0x[0-9a-f]{64}$/i;
|
|
5
|
+
export function parseDeploymentConfig(value) {
|
|
6
|
+
if (value.network !== "testnet")
|
|
7
|
+
fail("CONFIG_INVALID", "network must be the pinned testnet deployment");
|
|
8
|
+
const string = (key) => {
|
|
9
|
+
const result = value[key];
|
|
10
|
+
if (typeof result !== "string" || !result.trim())
|
|
11
|
+
fail("CONFIG_INVALID", `${key} is required`);
|
|
12
|
+
return result;
|
|
13
|
+
};
|
|
14
|
+
const checkpoint = string("deploymentCheckpoint");
|
|
15
|
+
const verifierVersion = value.verifierVersion;
|
|
16
|
+
const config = {
|
|
17
|
+
network: "testnet",
|
|
18
|
+
chainId: string("chainId"), packageId: string("packageId"), poolId: string("poolId"),
|
|
19
|
+
coinType: string("coinType"), coinTypeHash: string("coinTypeHash"),
|
|
20
|
+
deploymentDomain: string("deploymentDomain"), verifierVersion: Number(verifierVersion),
|
|
21
|
+
deploymentCheckpoint: BigInt(checkpoint), initialRoot: string("initialRoot"),
|
|
22
|
+
tatumGrpcEndpoint: string("tatumGrpcEndpoint"), tatumApiKey: string("tatumApiKey"),
|
|
23
|
+
writeGrpcEndpoint: string("writeGrpcEndpoint"), walrusPublisherUrl: string("walrusPublisherUrl"),
|
|
24
|
+
walrusAggregatorUrl: string("walrusAggregatorUrl"), x402FacilitatorUrl: string("x402FacilitatorUrl"),
|
|
25
|
+
};
|
|
26
|
+
if (![config.packageId, config.poolId].every((id) => SUI_ID.test(id)))
|
|
27
|
+
fail("CONFIG_INVALID", "packageId and poolId must be 32-byte Sui IDs");
|
|
28
|
+
if (![config.coinTypeHash, config.deploymentDomain, config.initialRoot].every((hex) => HEX_32.test(hex)))
|
|
29
|
+
fail("CONFIG_INVALID", "deployment hashes must be 32-byte hex values");
|
|
30
|
+
if (!Number.isInteger(config.verifierVersion) || config.verifierVersion < 1)
|
|
31
|
+
fail("CONFIG_INVALID", "verifierVersion must be a positive integer");
|
|
32
|
+
if (config.deploymentCheckpoint < 0n)
|
|
33
|
+
fail("CONFIG_INVALID", "deploymentCheckpoint cannot be negative");
|
|
34
|
+
for (const [name, endpoint] of Object.entries({ tatumGrpcEndpoint: config.tatumGrpcEndpoint, writeGrpcEndpoint: config.writeGrpcEndpoint, walrusPublisherUrl: config.walrusPublisherUrl, walrusAggregatorUrl: config.walrusAggregatorUrl, x402FacilitatorUrl: config.x402FacilitatorUrl })) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(endpoint);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return fail("CONFIG_INVALID", `${name} must be an absolute URL`, error);
|
|
41
|
+
}
|
|
42
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "::1"].includes(url.hostname)))
|
|
43
|
+
fail("CONFIG_INVALID", `${name} must use HTTPS except on loopback`);
|
|
44
|
+
if (url.username || url.password)
|
|
45
|
+
fail("CONFIG_INVALID", `${name} cannot contain embedded credentials`);
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
49
|
+
export function loadStateKey(path) {
|
|
50
|
+
const mode = statSync(path).mode & 0o777;
|
|
51
|
+
if ((mode & 0o077) !== 0)
|
|
52
|
+
fail("STATE_KEY_PERMISSIONS", `state key ${path} must not be group/world accessible`);
|
|
53
|
+
const raw = readFileSync(path);
|
|
54
|
+
const key = /^[0-9a-fA-F]{64}\s*$/.test(raw.toString("utf8")) ? Buffer.from(raw.toString("utf8").trim(), "hex") : raw;
|
|
55
|
+
if (key.length !== 32)
|
|
56
|
+
fail("STATE_KEY_INVALID", "state key must contain exactly 32 bytes or 64 hex characters");
|
|
57
|
+
return key;
|
|
58
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readDemoEvents, recordedReplay } from "./events.js";
|
|
3
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Supercuts funded-run evidence</title><style>body{font:15px system-ui;margin:0;background:#0b1020;color:#eef2ff}header{position:sticky;top:0;padding:18px 24px;background:#111936;border-bottom:1px solid #334155}main{max-width:980px;margin:auto;padding:24px}.event{display:grid;grid-template-columns:9rem 10rem 1fr;gap:14px;padding:14px;border-bottom:1px solid #27334f}.mode{color:#fbbf24;font-weight:700}.complete,.allowed{color:#86efac}.failed,.rejected{color:#fca5a5}code{color:#bfdbfe;white-space:pre-wrap}</style></head><body><header><b>Supercuts</b> · <span class="mode" id="mode">loading evidence…</span></header><main id="events"></main><script>const out=document.querySelector('#events');const source=new EventSource('/events');source.onmessage=e=>{const v=JSON.parse(e.data);document.querySelector('#mode').textContent=v.mode==='live'?'LIVE FUNDED RUN':'RECORDED FUNDED RUN';const n=document.createElement('div');n.className='event';n.innerHTML='<span>#'+v.sequence+' '+v.actor+'</span><span class="'+v.status+'">'+v.stage+' · '+v.status+'</span><code></code>';n.querySelector('code').textContent=JSON.stringify(v.publicDetails,null,2);out.append(n)};</script></body></html>`;
|
|
4
|
+
export function serveEventDashboard(path, port, replay = false) {
|
|
5
|
+
const server = createServer((request, response) => {
|
|
6
|
+
if (request.url === "/") {
|
|
7
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
8
|
+
return response.end(html);
|
|
9
|
+
}
|
|
10
|
+
if (request.url === "/events") {
|
|
11
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive", "access-control-allow-origin": "http://127.0.0.1" });
|
|
12
|
+
const source = readDemoEvents(path), events = replay ? recordedReplay(source) : source;
|
|
13
|
+
for (const event of events)
|
|
14
|
+
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
15
|
+
return response.end();
|
|
16
|
+
}
|
|
17
|
+
response.writeHead(404);
|
|
18
|
+
response.end("Not Found");
|
|
19
|
+
});
|
|
20
|
+
return new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", () => resolve({ server, url: `http://127.0.0.1:${port}/` })); });
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { DealEngine } from "./deal-engine.js";
|
|
5
|
+
import { DealJournal } from "./deal.js";
|
|
6
|
+
import { fail } from "./errors.js";
|
|
7
|
+
import { loadMandate } from "./mandate.js";
|
|
8
|
+
import { LiveDealPorts } from "./live-deal-ports.js";
|
|
9
|
+
export async function createConfiguredDealEngine(app) {
|
|
10
|
+
if (app.profile.role !== "buyer" || app.profile.mock)
|
|
11
|
+
fail("DEAL_LIVE_REQUIRED", "composite deals require a live buyer role home");
|
|
12
|
+
const mandate = loadMandate(app.home);
|
|
13
|
+
if (mandate.version !== "supercuts.mandate.v2")
|
|
14
|
+
fail("DEAL_MANDATE_VERSION", "composite deals require an owner-issued v2 mandate");
|
|
15
|
+
if (!mandate.allowedCommands.includes("deal.execute"))
|
|
16
|
+
fail("MANDATE_COMMAND_FORBIDDEN", "owner mandate does not allow composite deal execution");
|
|
17
|
+
const path = join(app.home, "deal-ports.mjs");
|
|
18
|
+
if (!existsSync(path)) {
|
|
19
|
+
if (!app.store.getSecret("live:memory-config"))
|
|
20
|
+
fail("LIVE_MEMORY_CONFIG_MISSING", "live composite deals require an owner-only Walrus Memory configuration");
|
|
21
|
+
return new DealEngine(new DealJournal(app.store), new LiveDealPorts(app, mandate), mandate);
|
|
22
|
+
}
|
|
23
|
+
if ((statSync(path).mode & 0o077) !== 0)
|
|
24
|
+
fail("DEAL_PORTS_PERMISSIONS", "deal ports adapter must be owner-only");
|
|
25
|
+
let loaded;
|
|
26
|
+
try {
|
|
27
|
+
loaded = await import(pathToFileURL(path).href);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return fail("DEAL_PORTS_LOAD_FAILED", "owner deal ports adapter could not load", error);
|
|
31
|
+
}
|
|
32
|
+
if (typeof loaded.createDealPorts !== "function")
|
|
33
|
+
return fail("DEAL_PORTS_INVALID", "owner adapter must export createDealPorts");
|
|
34
|
+
const ports = await loaded.createDealPorts(app, mandate);
|
|
35
|
+
for (const method of ["readBlob", "discover", "plan", "negotiate", "pay", "deliver", "rememberOutcome", "inventory"])
|
|
36
|
+
if (typeof ports?.[method] !== "function")
|
|
37
|
+
fail("DEAL_PORTS_INVALID", `owner adapter is missing ${method}`);
|
|
38
|
+
return new DealEngine(new DealJournal(app.store), ports, mandate);
|
|
39
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SignedCapabilityRegistration } from "./capability.js";
|
|
2
|
+
import { DealJournal, type DealView, type ExecuteDealInput } from "./deal.js";
|
|
3
|
+
import type { AgentMandate } from "./mandate.js";
|
|
4
|
+
import { type SettlementPlanV1 } from "./settlement.js";
|
|
5
|
+
export interface DealNegotiation {
|
|
6
|
+
transcriptBlobs: Array<{
|
|
7
|
+
blobId: string;
|
|
8
|
+
ciphertextHash: string;
|
|
9
|
+
}>;
|
|
10
|
+
agreementBlobId: string;
|
|
11
|
+
agreementCiphertextHash: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DealPayment {
|
|
14
|
+
transactionDigest: string;
|
|
15
|
+
finalized: true;
|
|
16
|
+
tatumVerified: true;
|
|
17
|
+
proveMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface DealArtifact {
|
|
20
|
+
blobId: string;
|
|
21
|
+
ciphertextHash: string;
|
|
22
|
+
artifactSha256: string;
|
|
23
|
+
contentType: string;
|
|
24
|
+
metadata: Record<string, unknown>;
|
|
25
|
+
artifactBase64: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DealOutcome {
|
|
28
|
+
payloadHash: string;
|
|
29
|
+
blobId: string;
|
|
30
|
+
indexed: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface DealOutcomeQueued {
|
|
33
|
+
queued: true;
|
|
34
|
+
payloadHash: string;
|
|
35
|
+
}
|
|
36
|
+
/** A port owns transport, signing, and finality checks. It must be safe to call
|
|
37
|
+
* again with the same child IDs after a process crash. */
|
|
38
|
+
export interface DealPorts {
|
|
39
|
+
readBlob(blobId: string): Promise<Uint8Array>;
|
|
40
|
+
discover(input: ExecuteDealInput): Promise<SignedCapabilityRegistration>;
|
|
41
|
+
plan(input: ExecuteDealInput, registration: SignedCapabilityRegistration, paymentOperationId: string): Promise<SettlementPlanV1>;
|
|
42
|
+
negotiate(input: ExecuteDealInput, registration: SignedCapabilityRegistration, plan: SettlementPlanV1, agreementOperationId: string): Promise<DealNegotiation>;
|
|
43
|
+
pay(input: ExecuteDealInput, plan: SettlementPlanV1, negotiation: DealNegotiation, paymentOperationId: string): Promise<DealPayment | {
|
|
44
|
+
pending: true;
|
|
45
|
+
}>;
|
|
46
|
+
deliver(input: ExecuteDealInput, plan: SettlementPlanV1, negotiation: DealNegotiation, payment: DealPayment, deliveryOperationId: string): Promise<DealArtifact | {
|
|
47
|
+
pending: true;
|
|
48
|
+
}>;
|
|
49
|
+
rememberOutcome(input: ExecuteDealInput, plan: SettlementPlanV1, payment: DealPayment, artifact: DealArtifact, outcomeOperationId: string): Promise<DealOutcome | DealOutcomeQueued | {
|
|
50
|
+
unresolved: true;
|
|
51
|
+
}>;
|
|
52
|
+
inventory(blobId: string): Promise<boolean>;
|
|
53
|
+
}
|
|
54
|
+
export declare class DealEngine {
|
|
55
|
+
private readonly journal;
|
|
56
|
+
private readonly ports;
|
|
57
|
+
private readonly mandate;
|
|
58
|
+
constructor(journal: DealJournal, ports: DealPorts, mandate: AgentMandate);
|
|
59
|
+
execute(input: ExecuteDealInput): Promise<DealView>;
|
|
60
|
+
resume(operationId: string): Promise<DealView>;
|
|
61
|
+
private finishOutcome;
|
|
62
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateCapabilityPayload, verifySignedCapability } from "./capability.js";
|
|
3
|
+
import { dealChildId } from "./deal.js";
|
|
4
|
+
import { fail, SupercutsError } from "./errors.js";
|
|
5
|
+
import { canonicalJson, validateSettlementPlan } from "./settlement.js";
|
|
6
|
+
const hash = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
7
|
+
export class DealEngine {
|
|
8
|
+
journal;
|
|
9
|
+
ports;
|
|
10
|
+
mandate;
|
|
11
|
+
constructor(journal, ports, mandate) {
|
|
12
|
+
this.journal = journal;
|
|
13
|
+
this.ports = ports;
|
|
14
|
+
this.mandate = mandate;
|
|
15
|
+
if (mandate.version !== "supercuts.mandate.v2" || mandate.role !== "buyer")
|
|
16
|
+
fail("DEAL_MANDATE_VERSION", "deal execution requires an owner-issued buyer mandate v2");
|
|
17
|
+
}
|
|
18
|
+
async execute(input) {
|
|
19
|
+
this.journal.create(input);
|
|
20
|
+
return this.resume(input.operationId);
|
|
21
|
+
}
|
|
22
|
+
async resume(operationId) {
|
|
23
|
+
const input = this.journal.input(operationId);
|
|
24
|
+
let view = this.journal.get(operationId);
|
|
25
|
+
if (view.state === "complete" || view.state === "failed" || view.state === "unresolved")
|
|
26
|
+
return view;
|
|
27
|
+
if (view.state === "outcome_pending")
|
|
28
|
+
return this.finishOutcome(operationId, input);
|
|
29
|
+
if (Date.now() > this.mandate.expiresAt)
|
|
30
|
+
fail("MANDATE_EXPIRED", "owner mandate has expired");
|
|
31
|
+
if (!this.mandate.allowedAssets.includes(input.budget.asset) || BigInt(input.budget.maximum) > BigInt(this.mandate.perDealCeiling))
|
|
32
|
+
fail("DEAL_BUDGET_DENIED", "deal budget exceeds the owner mandate");
|
|
33
|
+
if (input.requestedRail === "x402" && (this.mandate.allowPublicX402 !== true || !this.mandate.allowedRails?.includes("x402") || this.mandate.requiredPrivacy === "shielded" || input.privacyHint === "shielded"))
|
|
34
|
+
fail("DEAL_PUBLIC_RAIL_FORBIDDEN", "owner mandate did not authorize public x402");
|
|
35
|
+
if (view.state === "awaiting_approval" || view.state === "review_required")
|
|
36
|
+
return view;
|
|
37
|
+
let registration = this.journal.step(operationId, "discovery");
|
|
38
|
+
if (!registration) {
|
|
39
|
+
const started = Date.now();
|
|
40
|
+
registration = await this.ports.discover(input);
|
|
41
|
+
await verifySignedCapability(registration);
|
|
42
|
+
const manifestBytes = await this.ports.readBlob(registration.manifestBlobId);
|
|
43
|
+
let publishedManifest;
|
|
44
|
+
try {
|
|
45
|
+
publishedManifest = JSON.parse(Buffer.from(manifestBytes).toString());
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return fail("CAPABILITY_BLOB_INVALID", "Walrus capability blob is not JSON", error);
|
|
49
|
+
}
|
|
50
|
+
if (canonicalJson(publishedManifest) !== canonicalJson(registration.manifest))
|
|
51
|
+
fail("CAPABILITY_BLOB_MISMATCH", "Walrus manifest differs from the seller's signed capability");
|
|
52
|
+
this.journal.saveStep(operationId, "discovery", input, registration);
|
|
53
|
+
this.journal.recordTiming(operationId, "discovery", Date.now() - started);
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
await verifySignedCapability(registration);
|
|
57
|
+
if (input.capabilityId && registration.manifest.capabilityId !== input.capabilityId || input.capabilityClass && registration.manifest.capability !== input.capabilityClass || registration.manifest.asset !== input.budget.asset)
|
|
58
|
+
fail("DEAL_CAPABILITY_MISMATCH", "seller capability differs from the task selector or asset");
|
|
59
|
+
validateCapabilityPayload(registration.manifest, "input", input.input);
|
|
60
|
+
if (!this.mandate.allowedCounterparties.includes(registration.manifest.sellerId) || this.mandate.allowedCapabilityIds && !this.mandate.allowedCapabilityIds.includes(registration.manifest.capabilityId) || this.mandate.allowedCapabilityClasses && !this.mandate.allowedCapabilityClasses.includes(registration.manifest.capability))
|
|
61
|
+
fail("DEAL_SELLER_FORBIDDEN", "signed seller capability is outside the owner mandate");
|
|
62
|
+
if (view.state === "created")
|
|
63
|
+
view = this.journal.advance(operationId, "created", "discovered");
|
|
64
|
+
const paymentOperationId = dealChildId(operationId, "payment");
|
|
65
|
+
let plan = this.journal.step(operationId, "planning");
|
|
66
|
+
if (!plan) {
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
plan = await this.ports.plan(input, registration, paymentOperationId);
|
|
69
|
+
validateSettlementPlan(plan);
|
|
70
|
+
if (plan.operationId !== paymentOperationId || plan.manifestHash !== registration.manifest.manifestHash || plan.asset !== input.budget.asset || BigInt(plan.maximumAmount) > BigInt(input.budget.maximum))
|
|
71
|
+
fail("DEAL_PLAN_MISMATCH", "settlement plan exceeds the deal's capability or budget");
|
|
72
|
+
this.journal.saveStep(operationId, "planning", { input, manifestHash: registration.manifest.manifestHash }, plan);
|
|
73
|
+
this.journal.recordTiming(operationId, "planning", Date.now() - started);
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
validateSettlementPlan(plan);
|
|
77
|
+
if (plan.operationId !== paymentOperationId || plan.manifestHash !== registration.manifest.manifestHash || plan.asset !== input.budget.asset || BigInt(plan.maximumAmount) > BigInt(input.budget.maximum))
|
|
78
|
+
fail("DEAL_PLAN_MISMATCH", "saved plan exceeds the deal's capability or budget");
|
|
79
|
+
if (input.requestedRail && plan.rail !== input.requestedRail || input.privacyHint === "shielded" && plan.rail !== "shielded")
|
|
80
|
+
fail("DEAL_PRIVACY_UNAVAILABLE", "planned rail differs from the requested privacy");
|
|
81
|
+
if (!input.requestedRail && registration.manifest.supportedRails.includes("shielded") && plan.rail !== "shielded")
|
|
82
|
+
fail("DEAL_PRIVACY_UNAVAILABLE", "shielded is the default rail when the seller supports it");
|
|
83
|
+
if (plan.rail === "x402" && (this.mandate.allowPublicX402 !== true || !this.mandate.allowedRails?.includes("x402") || this.mandate.requiredPrivacy === "shielded"))
|
|
84
|
+
fail("DEAL_PUBLIC_RAIL_FORBIDDEN", "owner mandate did not authorize public x402");
|
|
85
|
+
if (view.state === "discovered")
|
|
86
|
+
view = this.journal.advance(operationId, "discovered", "planned", { rail: plan.rail, planHash: plan.planHash });
|
|
87
|
+
if (plan.status === "review_required")
|
|
88
|
+
return this.journal.advance(operationId, "planned", "review_required");
|
|
89
|
+
if (Date.now() >= plan.expiresAt && ["planned", "negotiating", "agreed", "evidence_ready", "funding_required"].includes(view.state))
|
|
90
|
+
return fail("DEAL_PLAN_EXPIRED", "saved settlement plan expired before payment");
|
|
91
|
+
let negotiation = this.journal.step(operationId, "negotiation");
|
|
92
|
+
if (view.state === "planned")
|
|
93
|
+
view = this.journal.advance(operationId, "planned", "negotiating");
|
|
94
|
+
if (!negotiation) {
|
|
95
|
+
const started = Date.now();
|
|
96
|
+
negotiation = await this.ports.negotiate(input, registration, plan, dealChildId(operationId, "agreement"));
|
|
97
|
+
if (!negotiation.agreementBlobId || !/^[0-9a-f]{64}$/.test(negotiation.agreementCiphertextHash) || !Array.isArray(negotiation.transcriptBlobs) || !negotiation.transcriptBlobs.length)
|
|
98
|
+
fail("DEAL_AGREEMENT_INVALID", "seller agreement or Walrus transcript evidence is incomplete");
|
|
99
|
+
this.journal.saveStep(operationId, "negotiation", { planHash: plan.planHash }, negotiation);
|
|
100
|
+
this.journal.recordTiming(operationId, "negotiation", Date.now() - started);
|
|
101
|
+
}
|
|
102
|
+
const agreementReadStarted = Date.now();
|
|
103
|
+
for (const item of [...negotiation.transcriptBlobs, { blobId: negotiation.agreementBlobId, ciphertextHash: negotiation.agreementCiphertextHash }]) {
|
|
104
|
+
if (!item.blobId || !/^[0-9a-f]{64}$/.test(item.ciphertextHash) || createHash("sha256").update(await this.ports.readBlob(item.blobId)).digest("hex") !== item.ciphertextHash)
|
|
105
|
+
fail("WALRUS_BLOB_MISMATCH", "encrypted negotiation or agreement differs from Walrus readback");
|
|
106
|
+
}
|
|
107
|
+
this.journal.recordTiming(operationId, "agreement_readback", Date.now() - agreementReadStarted);
|
|
108
|
+
if (view.state === "negotiating")
|
|
109
|
+
view = this.journal.advance(operationId, "negotiating", "agreed");
|
|
110
|
+
this.journal.recordBlob(operationId, "agreement", negotiation.agreementCiphertextHash, negotiation.agreementBlobId);
|
|
111
|
+
if (view.state === "agreed")
|
|
112
|
+
view = this.journal.advance(operationId, "agreed", "evidence_ready");
|
|
113
|
+
let payment = this.journal.step(operationId, "payment");
|
|
114
|
+
if (view.state === "evidence_ready" || view.state === "funding_required")
|
|
115
|
+
view = this.journal.advance(operationId, view.state, "payment_pending");
|
|
116
|
+
if (!payment) {
|
|
117
|
+
let candidate;
|
|
118
|
+
const started = Date.now();
|
|
119
|
+
try {
|
|
120
|
+
candidate = await this.ports.pay(input, plan, negotiation, paymentOperationId);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof SupercutsError && error.code === "INSUFFICIENT_SHIELDED_BALANCE")
|
|
124
|
+
return this.journal.advance(operationId, "payment_pending", "funding_required");
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
if ("pending" in candidate)
|
|
128
|
+
return this.journal.get(operationId);
|
|
129
|
+
if (candidate.finalized !== true || candidate.tatumVerified !== true || !candidate.transactionDigest)
|
|
130
|
+
fail("DEAL_PAYMENT_UNVERIFIED", "payment lacks final Sui and Tatum evidence");
|
|
131
|
+
payment = candidate;
|
|
132
|
+
this.journal.saveStep(operationId, "payment", { planHash: plan.planHash, agreementBlobId: negotiation.agreementBlobId }, payment);
|
|
133
|
+
this.journal.recordTiming(operationId, "payment", Date.now() - started);
|
|
134
|
+
if (typeof payment.proveMs === "number")
|
|
135
|
+
this.journal.recordTiming(operationId, "proof", payment.proveMs);
|
|
136
|
+
}
|
|
137
|
+
if (view.state === "payment_pending")
|
|
138
|
+
view = this.journal.advance(operationId, "payment_pending", "paid");
|
|
139
|
+
let artifact = this.journal.step(operationId, "delivery");
|
|
140
|
+
if (view.state === "paid")
|
|
141
|
+
view = this.journal.advance(operationId, "paid", "delivery_pending");
|
|
142
|
+
if (!artifact) {
|
|
143
|
+
const started = Date.now();
|
|
144
|
+
const candidate = await this.ports.deliver(input, plan, negotiation, payment, dealChildId(operationId, "delivery"));
|
|
145
|
+
if ("pending" in candidate)
|
|
146
|
+
return this.journal.get(operationId);
|
|
147
|
+
if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.ciphertextHash) || !/^[0-9a-f]{64}$/.test(candidate.artifactSha256))
|
|
148
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "seller delivery evidence is incomplete");
|
|
149
|
+
if (typeof candidate.artifactBase64 !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(candidate.artifactBase64))
|
|
150
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact encoding is invalid");
|
|
151
|
+
const artifactBytes = Buffer.from(candidate.artifactBase64, "base64");
|
|
152
|
+
if (!artifactBytes.length || artifactBytes.length > 512 * 1024 || createHash("sha256").update(artifactBytes).digest("hex") !== candidate.artifactSha256)
|
|
153
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "seller artifact hash or size is invalid");
|
|
154
|
+
validateCapabilityPayload(registration.manifest, "output", candidate.metadata);
|
|
155
|
+
artifact = candidate;
|
|
156
|
+
this.journal.saveStep(operationId, "delivery", { paymentDigest: payment.transactionDigest }, artifact);
|
|
157
|
+
this.journal.recordTiming(operationId, "delivery", Date.now() - started);
|
|
158
|
+
}
|
|
159
|
+
if (createHash("sha256").update(Buffer.from(artifact.artifactBase64, "base64")).digest("hex") !== artifact.artifactSha256)
|
|
160
|
+
fail("DEAL_DELIVERY_UNVERIFIED", "saved artifact hash differs from delivery evidence");
|
|
161
|
+
const deliveryReadStarted = Date.now();
|
|
162
|
+
if (createHash("sha256").update(await this.ports.readBlob(artifact.blobId)).digest("hex") !== artifact.ciphertextHash)
|
|
163
|
+
fail("WALRUS_BLOB_MISMATCH", "encrypted delivery differs from Walrus readback");
|
|
164
|
+
this.journal.recordTiming(operationId, "delivery_readback", Date.now() - deliveryReadStarted);
|
|
165
|
+
this.journal.recordBlob(operationId, "delivery", artifact.ciphertextHash, artifact.blobId);
|
|
166
|
+
if (view.state === "delivery_pending")
|
|
167
|
+
view = this.journal.advance(operationId, "delivery_pending", "delivered");
|
|
168
|
+
if (view.state === "delivered")
|
|
169
|
+
view = this.journal.advance(operationId, "delivered", "outcome_pending");
|
|
170
|
+
return this.finishOutcome(operationId, input);
|
|
171
|
+
}
|
|
172
|
+
async finishOutcome(operationId, input) {
|
|
173
|
+
const view = this.journal.get(operationId);
|
|
174
|
+
if (view.evidence.outcomeMemory === "indexed")
|
|
175
|
+
return view.state === "outcome_pending" ? this.journal.advance(operationId, "outcome_pending", "complete") : view;
|
|
176
|
+
if (view.evidence.outcomeMemory === "unresolved")
|
|
177
|
+
return view;
|
|
178
|
+
const existing = this.journal.outcomeStatus(operationId);
|
|
179
|
+
if (existing?.blobId) {
|
|
180
|
+
const indexed = await this.ports.inventory(existing.blobId);
|
|
181
|
+
this.journal.recordOutcome(operationId, existing.payloadHash, existing.blobId, indexed);
|
|
182
|
+
return indexed ? this.journal.advance(operationId, "outcome_pending", "complete") : this.journal.get(operationId);
|
|
183
|
+
}
|
|
184
|
+
if (existing)
|
|
185
|
+
return view;
|
|
186
|
+
const plan = this.journal.step(operationId, "planning") ?? fail("DEAL_PLAN_MISSING", "saved deal plan is missing");
|
|
187
|
+
const payment = this.journal.step(operationId, "payment") ?? fail("DEAL_PAYMENT_UNVERIFIED", "saved deal payment is missing");
|
|
188
|
+
const artifact = this.journal.step(operationId, "delivery") ?? fail("DEAL_DELIVERY_UNVERIFIED", "saved deal delivery is missing");
|
|
189
|
+
let outcome = this.journal.step(operationId, "outcome");
|
|
190
|
+
if (!outcome) {
|
|
191
|
+
const started = Date.now();
|
|
192
|
+
const candidate = await this.ports.rememberOutcome(input, plan, payment, artifact, dealChildId(operationId, "outcome"));
|
|
193
|
+
this.journal.recordTiming(operationId, "outcome_queue", Date.now() - started);
|
|
194
|
+
if ("unresolved" in candidate) {
|
|
195
|
+
this.journal.setOutcomeUnresolved(operationId);
|
|
196
|
+
return this.journal.get(operationId);
|
|
197
|
+
}
|
|
198
|
+
if ("queued" in candidate) {
|
|
199
|
+
this.journal.queueOutcome(operationId, candidate.payloadHash);
|
|
200
|
+
return this.journal.get(operationId);
|
|
201
|
+
}
|
|
202
|
+
if (!candidate.blobId || !/^[0-9a-f]{64}$/.test(candidate.payloadHash))
|
|
203
|
+
fail("DEAL_MEMORY_INVALID", "outcome memory is incomplete");
|
|
204
|
+
outcome = candidate;
|
|
205
|
+
this.journal.saveStep(operationId, "outcome", { paymentDigest: payment.transactionDigest, artifactSha256: artifact.artifactSha256 }, outcome);
|
|
206
|
+
}
|
|
207
|
+
const indexed = await this.ports.inventory(outcome.blobId);
|
|
208
|
+
this.journal.recordOutcome(operationId, outcome.payloadHash, outcome.blobId, indexed);
|
|
209
|
+
return indexed ? this.journal.advance(operationId, "outcome_pending", "complete") : this.journal.get(operationId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Phase3App } from "./phase3.js";
|
|
2
|
+
export declare const wireHash: (value: Uint8Array | string) => string;
|
|
3
|
+
export declare const wireJsonHash: (value: unknown) => string;
|
|
4
|
+
export declare function wireKey(app: Phase3App, peerPublicKey: string, sessionId: string): Buffer;
|
|
5
|
+
export declare function wireEncrypt(key: Uint8Array, sessionId: string, kind: string, value: unknown): string;
|
|
6
|
+
export declare function wireDecrypt<T>(key: Uint8Array, sessionId: string, kind: string, ciphertext: string): T;
|
|
7
|
+
export declare class WalrusDealBlobs {
|
|
8
|
+
#private;
|
|
9
|
+
private readonly http;
|
|
10
|
+
constructor(app: Phase3App, http?: typeof fetch);
|
|
11
|
+
read(blobId: string): Promise<Uint8Array>;
|
|
12
|
+
publish(bytes: Uint8Array): Promise<{
|
|
13
|
+
blobId: string;
|
|
14
|
+
ciphertextHash: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, diffieHellman, hkdfSync } from "node:crypto";
|
|
2
|
+
import { fail } from "./errors.js";
|
|
3
|
+
import { decryptSessionMessage, encryptSessionMessage } from "./negotiation.js";
|
|
4
|
+
import { canonicalJson } from "./settlement.js";
|
|
5
|
+
export const wireHash = (value) => createHash("sha256").update(value).digest("hex");
|
|
6
|
+
export const wireJsonHash = (value) => wireHash(canonicalJson(value));
|
|
7
|
+
export function wireKey(app, peerPublicKey, sessionId) {
|
|
8
|
+
const privateDer = app.store.getSecret("identity:x25519-private") ?? fail("IDENTITY_MISSING", "role transport key is missing");
|
|
9
|
+
let publicKey;
|
|
10
|
+
try {
|
|
11
|
+
publicKey = createPublicKey({ key: Buffer.from(peerPublicKey, "base64url"), format: "der", type: "spki" });
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return fail("DEAL_TRANSPORT_KEY_INVALID", "peer transport key is invalid", error);
|
|
15
|
+
}
|
|
16
|
+
if (publicKey.asymmetricKeyType !== "x25519")
|
|
17
|
+
fail("DEAL_TRANSPORT_KEY_INVALID", "peer transport key must be X25519");
|
|
18
|
+
const shared = diffieHellman({ privateKey: createPrivateKey({ key: privateDer, format: "der", type: "pkcs8" }), publicKey });
|
|
19
|
+
return Buffer.from(hkdfSync("sha256", shared, Buffer.from(sessionId), Buffer.from("supercuts-deal-wire-v1"), 32));
|
|
20
|
+
}
|
|
21
|
+
export function wireEncrypt(key, sessionId, kind, value) {
|
|
22
|
+
return encryptSessionMessage(key, sessionId, `${sessionId}:${kind}`, Buffer.from(canonicalJson(value)));
|
|
23
|
+
}
|
|
24
|
+
export function wireDecrypt(key, sessionId, kind, ciphertext) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(decryptSessionMessage(key, sessionId, `${sessionId}:${kind}`, ciphertext).toString());
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return fail("DEAL_MESSAGE_INVALID", "encrypted deal message could not be decoded", error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class WalrusDealBlobs {
|
|
33
|
+
http;
|
|
34
|
+
#publisher;
|
|
35
|
+
#aggregator;
|
|
36
|
+
constructor(app, http = fetch) {
|
|
37
|
+
this.http = http;
|
|
38
|
+
this.#publisher = app.profile.live?.walrusPublisherUrl ?? fail("LIVE_WALRUS_CONFIG_MISSING", "Walrus publisher is required");
|
|
39
|
+
this.#aggregator = app.profile.live?.walrusAggregatorUrl ?? fail("LIVE_WALRUS_CONFIG_MISSING", "Walrus aggregator is required");
|
|
40
|
+
}
|
|
41
|
+
async read(blobId) {
|
|
42
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(blobId))
|
|
43
|
+
fail("WALRUS_BLOB_INVALID", "Walrus blob ID is invalid");
|
|
44
|
+
const response = await this.http(`${this.#aggregator.replace(/\/$/, "")}/v1/blobs/${encodeURIComponent(blobId)}`, { signal: AbortSignal.timeout(60_000), redirect: "error" });
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
fail("WALRUS_READ_FAILED", `Walrus aggregator returned ${response.status}`);
|
|
47
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
48
|
+
if (bytes.length > 1024 * 1024)
|
|
49
|
+
fail("WALRUS_BLOB_TOO_LARGE", "deal blob exceeds 1 MiB");
|
|
50
|
+
return bytes;
|
|
51
|
+
}
|
|
52
|
+
async publish(bytes) {
|
|
53
|
+
if (!bytes.length || bytes.length > 1024 * 1024)
|
|
54
|
+
fail("WALRUS_BLOB_INVALID", "deal blob must contain 1..1048576 bytes");
|
|
55
|
+
const response = await this.http(`${this.#publisher.replace(/\/$/, "")}/v1/blobs?epochs=5`, { method: "PUT", body: Buffer.from(bytes), signal: AbortSignal.timeout(60_000), redirect: "error" });
|
|
56
|
+
if (!response.ok)
|
|
57
|
+
fail("WALRUS_PUBLISH_FAILED", `Walrus publisher returned ${response.status}`);
|
|
58
|
+
const result = await response.json();
|
|
59
|
+
const blobId = result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId ?? result.alreadyRegistered?.blobId ?? fail("WALRUS_PUBLISH_FAILED", "Walrus publisher omitted blob ID");
|
|
60
|
+
const fetched = await this.read(blobId);
|
|
61
|
+
if (!Buffer.from(fetched).equals(Buffer.from(bytes)))
|
|
62
|
+
fail("WALRUS_BLOB_MISMATCH", "deal blob failed Walrus readback");
|
|
63
|
+
return { blobId, ciphertextHash: wireHash(bytes) };
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/deal.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { RoleStore } from "./store.js";
|
|
2
|
+
export declare const DEAL_VERSION: "supercuts.deal.v2";
|
|
3
|
+
export type DealState = "created" | "discovered" | "planned" | "awaiting_approval" | "review_required" | "funding_required" | "negotiating" | "agreed" | "evidence_ready" | "payment_pending" | "paid" | "delivery_pending" | "delivered" | "outcome_pending" | "complete" | "failed" | "unresolved";
|
|
4
|
+
export interface ExecuteDealInput {
|
|
5
|
+
operationId: string;
|
|
6
|
+
capabilityId?: string;
|
|
7
|
+
capabilityClass?: string;
|
|
8
|
+
input: Record<string, unknown>;
|
|
9
|
+
budget: {
|
|
10
|
+
asset: string;
|
|
11
|
+
maximum: string;
|
|
12
|
+
};
|
|
13
|
+
privacyHint?: "any" | "shielded";
|
|
14
|
+
requestedRail?: "shielded" | "x402";
|
|
15
|
+
}
|
|
16
|
+
export interface DealView {
|
|
17
|
+
version: typeof DEAL_VERSION;
|
|
18
|
+
dealId: string;
|
|
19
|
+
operationId: string;
|
|
20
|
+
state: DealState;
|
|
21
|
+
rail?: "shielded" | "x402";
|
|
22
|
+
visibility?: "shielded-direct-payment" | "public-x402";
|
|
23
|
+
planHash?: string;
|
|
24
|
+
paymentDigest?: string;
|
|
25
|
+
deliveryHash?: string;
|
|
26
|
+
evidence: {
|
|
27
|
+
agreement: "pending" | "verified";
|
|
28
|
+
payment: "pending" | "finalized";
|
|
29
|
+
delivery: "pending" | "verified";
|
|
30
|
+
outcomeMemory: "pending" | "indexed" | "unresolved";
|
|
31
|
+
};
|
|
32
|
+
nextAction?: "resume" | "check_memory" | "reconcile_memory" | "fund_buyer" | "owner_approval" | "owner_review";
|
|
33
|
+
timingsMs?: Record<string, number>;
|
|
34
|
+
}
|
|
35
|
+
export interface DealEvidenceView {
|
|
36
|
+
version: "supercuts.deal-evidence.v1";
|
|
37
|
+
dealId: string;
|
|
38
|
+
transcriptBlobIds: string[];
|
|
39
|
+
agreementBlobId?: string;
|
|
40
|
+
deliveryBlobId?: string;
|
|
41
|
+
outcomeMemoryBlobId?: string;
|
|
42
|
+
ciphertextReadback: "pending" | "verified" | "unresolved";
|
|
43
|
+
inventory: "pending" | "indexed" | "missing" | "unresolved";
|
|
44
|
+
paymentFinalized: boolean;
|
|
45
|
+
paymentDigest?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface DealDelivery {
|
|
48
|
+
dealId: string;
|
|
49
|
+
contentType: string;
|
|
50
|
+
artifactSha256: string;
|
|
51
|
+
artifact: Uint8Array;
|
|
52
|
+
}
|
|
53
|
+
export declare function validateDealInput(value: unknown): asserts value is ExecuteDealInput;
|
|
54
|
+
export declare function dealChildId(operationId: string, step: "plan" | "agreement" | "payment" | "delivery" | "outcome"): string;
|
|
55
|
+
export declare class DealJournal {
|
|
56
|
+
private readonly store;
|
|
57
|
+
constructor(store: RoleStore);
|
|
58
|
+
create(input: ExecuteDealInput): DealView;
|
|
59
|
+
get(operationId: string): DealView;
|
|
60
|
+
input(operationId: string): ExecuteDealInput;
|
|
61
|
+
saveStep(operationId: string, step: string, input: unknown, result: unknown): void;
|
|
62
|
+
step<T>(operationId: string, step: string): T | undefined;
|
|
63
|
+
recordBlob(operationId: string, kind: "agreement" | "delivery", ciphertextHash: string, blobId: string): void;
|
|
64
|
+
recordOutcome(operationId: string, payloadHash: string, blobId: string, indexed: boolean): void;
|
|
65
|
+
outcomeStatus(operationId: string): {
|
|
66
|
+
payloadHash: string;
|
|
67
|
+
blobId: string | null;
|
|
68
|
+
status: string;
|
|
69
|
+
} | undefined;
|
|
70
|
+
queueOutcome(operationId: string, payloadHash: string): void;
|
|
71
|
+
setOutcomeUnresolved(operationId: string): void;
|
|
72
|
+
recordTiming(operationId: string, stage: string, durationMs: number): void;
|
|
73
|
+
evidence(dealId: string): DealEvidenceView;
|
|
74
|
+
delivery(dealId: string): DealDelivery;
|
|
75
|
+
advance(operationId: string, expected: DealState, next: DealState, result?: {
|
|
76
|
+
rail?: "shielded" | "x402";
|
|
77
|
+
planHash?: string;
|
|
78
|
+
}): DealView;
|
|
79
|
+
}
|