@supercuts-walrus/backend 0.1.0 → 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.
- package/dist/buyer-setup.js +2 -2
- package/dist/cli.js +6 -3
- package/dist/demo.js +70 -8
- package/dist/funding.d.ts +38 -0
- package/dist/funding.js +78 -0
- package/dist/live-runtime.d.ts +3 -0
- package/dist/live-runtime.js +80 -6
- package/dist/mandate.js +9 -8
- package/dist/mcp-bootstrap.js +24 -2
- package/dist/mcp.js +39 -19
- package/dist/onboarding-doctor.js +13 -2
- package/dist/phase3.js +35 -5
- package/dist/remote-buyer.d.ts +18 -0
- package/dist/remote-buyer.js +59 -5
- package/dist/role-lock.d.ts +8 -0
- package/dist/role-lock.js +54 -0
- package/dist/seller-package.d.ts +9 -0
- package/dist/seller-package.js +50 -0
- package/dist/seller-service.js +9 -3
- package/dist/x402-resource-service.d.ts +2 -0
- package/dist/x402-resource-service.js +36 -17
- package/package.json +1 -1
package/dist/buyer-setup.js
CHANGED
|
@@ -69,7 +69,7 @@ function writePolicy(home, config) {
|
|
|
69
69
|
const base = { ...mandate };
|
|
70
70
|
delete base.allowedCapabilityIds;
|
|
71
71
|
delete base.allowedCapabilityClasses;
|
|
72
|
-
const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
|
|
72
|
+
const next = { ...base, version: "supercuts.mandate.v2", allowedCommands: [...new Set([...mandate.allowedCommands, "deal.execute", "deal.get", "deal.resume", "deal.evidence", "deal.delivery", "deal.catalog", "deal.policy", "settlement.plan", "memory.inventory", "remote.research"])], allowedCounterparties: config.allowedCounterparties ?? [], perDealCeiling: config.perDealCeiling, totalRunBudget: config.totalBudget, expiresAt: config.expiresAt, requiredPrivacy: "any", requireSettlementPlan: true, allowedRails: config.allowPublicX402 === true ? ["shielded", "x402"] : ["shielded"], allowPublicX402: config.allowPublicX402 === true, ...(config.allowedCapabilityIds ? { allowedCapabilityIds: config.allowedCapabilityIds } : {}), ...(config.allowedCapabilityClasses ? { allowedCapabilityClasses: config.allowedCapabilityClasses } : {}) };
|
|
73
73
|
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
74
74
|
chmodSync(path, 0o600);
|
|
75
75
|
writeFileSync(join(home, "deal-sellers.json"), `${JSON.stringify(config.directoryUrl ? { directoryUrl: origins([config.directoryUrl])[0], origins: config.sellerOrigins?.length ? origins(config.sellerOrigins) : [] } : origins(config.sellerOrigins ?? []), null, 2)}\n`, { mode: 0o600 });
|
|
@@ -206,7 +206,7 @@ export async function approveBuyerSeller(home, capabilityId) {
|
|
|
206
206
|
profile.policy.allowedCounterparties = updated.allowedCounterparties;
|
|
207
207
|
writeFileSync(path, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
|
|
208
208
|
writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
|
|
209
|
-
return { capabilityId, sellerId: registration.manifest.sellerId, approved: true };
|
|
209
|
+
return { capabilityId, sellerId: registration.manifest.sellerId, approved: true, manifestHash: registration.manifest.manifestHash, description: registration.manifest.description, priceBand: registration.manifest.priceBand, asset: registration.manifest.asset, supportedRails: registration.manifest.supportedRails, expiresAt: registration.manifest.expiresAt };
|
|
210
210
|
}
|
|
211
211
|
export async function addBuyerPrivacyRule(home, capabilityId) {
|
|
212
212
|
const app = new Phase3App(home);
|
package/dist/cli.js
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
import { Phase3App, errorOutput, homePath, initialize, parseArgs } from "./phase3.js";
|
|
3
3
|
import { addBuyerPrivacyRule, approveBuyerSeller, createBuyerSession, promptBuyerSetup, renewBuyer, setupBuyer } from "./buyer-setup.js";
|
|
4
4
|
import { onboardingDoctor } from "./onboarding-doctor.js";
|
|
5
|
+
import { executeFunding, prepareFunding } from "./funding.js";
|
|
6
|
+
import { packageHostedSeller } from "./seller-package.js";
|
|
5
7
|
const words = process.argv.slice(2);
|
|
6
8
|
let command = words.shift() ?? "";
|
|
7
|
-
if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding"].includes(command))
|
|
9
|
+
if (["wallet", "offer", "pay", "resource", "memory", "audit", "capability", "settlement", "setup", "policy", "onboarding", "onboard", "funding", "seller"].includes(command))
|
|
8
10
|
command += `.${words.shift() ?? ""}`;
|
|
9
11
|
try {
|
|
10
12
|
if (!command || command === "help" || command === "--help" || command === "setup.") {
|
|
11
|
-
process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts
|
|
13
|
+
process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.help.v1", ok: true, commands: ["onboard buyer", "funding prepare", "funding execute", "seller package", "setup buyer", "setup renew", "setup session", "init", "doctor", "status", "wallet", "offer", "pay", "memory", "capability", "audit"], setup: "Run supercuts onboard buyer --home <private-directory> for guided live Testnet setup. Use seller package to validate and prepare an operator-assisted hosted seller bundle.", live: "Use funding prepare to calculate public gas/payment and shielded deposit requirements. Funding execute requires explicit confirmation and funds only the generated buyer address." })}\n`);
|
|
12
14
|
process.exit(0);
|
|
13
15
|
}
|
|
14
16
|
const args = parseArgs(words);
|
|
15
17
|
const home = homePath(args);
|
|
16
|
-
const
|
|
18
|
+
const fundingInput = { publicPayment: String(args["public-payment"] ?? "0"), shieldedPayment: String(args["shielded-payment"] ?? "0"), ...(typeof args["gas-reserve"] === "string" ? { gasReserve: args["gas-reserve"] } : {}), ...(typeof args["change-reserve"] === "string" ? { changeReserve: args["change-reserve"] } : {}) };
|
|
19
|
+
const result = command === "init" ? initialize(home, args) : command === "setup.buyer" || command === "onboard.buyer" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: typeof args.config === "string" ? setupBuyer(home, args.config) : await promptBuyerSetup(home) } : command === "setup.renew" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: renewBuyer(home, String(args.config ?? "")) } : command === "setup.session" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: createBuyerSession(home) } : command === "funding.prepare" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await prepareFunding(home, fundingInput) } : command === "funding.execute" ? { schemaVersion: "supercuts.funding.v1", ok: true, command, data: await executeFunding(home, { ...fundingInput, operationId: String(args["operation-id"] ?? ""), requestFaucet: args["request-faucet"] === true, confirmed: args.confirm === true }) } : command === "seller.package" ? { schemaVersion: "supercuts.seller-package.v1", ok: true, command, data: await packageHostedSeller({ home, spec: String(args.spec ?? ""), artifact: String(args.artifact ?? ""), metadata: String(args.metadata ?? ""), registration: String(args.registration ?? ""), bundle: String(args.bundle ?? ""), ...(typeof args["content-type"] === "string" ? { contentType: args["content-type"] } : {}) }) } : command === "policy.add" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await addBuyerPrivacyRule(home, String(args["capability-id"] ?? "")) } : command === "policy.approve-seller" ? { schemaVersion: "supercuts.setup.v1", ok: true, command, data: await approveBuyerSeller(home, String(args["capability-id"] ?? "")) } : command === "onboarding.doctor" ? { schemaVersion: "supercuts.onboarding.v1", ok: true, command, data: await onboardingDoctor(home, typeof args.config === "string" ? args.config : undefined) } : await (async () => { const app = new Phase3App(home); try {
|
|
17
20
|
return await app.execute(command, args);
|
|
18
21
|
}
|
|
19
22
|
finally {
|
package/dist/demo.js
CHANGED
|
@@ -23,6 +23,7 @@ const value = (name, fallback) => typeof args[name] === "string" ? args[name] :
|
|
|
23
23
|
const stateDir = () => resolve(value("state-dir", ".supercuts-demo"));
|
|
24
24
|
const homes = (root) => ({ buyer: join(root, "buyer"), publicSeller: join(root, "public-seller"), secondPublicSeller: join(root, "second-public-seller"), confidentialSeller: join(root, "confidential-seller"), auditor: join(root, "auditor") });
|
|
25
25
|
const send = (data) => process.stdout.write(`${JSON.stringify({ schemaVersion: "supercuts.demo.v1", ok: true, command, data })}\n`);
|
|
26
|
+
const help = () => ({ commands: ["setup", "doctor", "seed-policy", "reuse-memories", "live", "replay", "check"], setup: "Run supercuts-demo setup --state-dir <owner-only-directory> with the pinned live configuration. Fund only the buyer address in funding[0].address.", live: "The live demo uses Sui Testnet and Mainnet-backed Walrus services; mock receipts are never settlement evidence." });
|
|
26
27
|
function keyAddress(path) { const decoded = decodeSuiPrivateKey(readFileSync(path, "utf8").trim()); if (decoded.scheme !== "ED25519")
|
|
27
28
|
fail("SIGNER_UNSUPPORTED", "demo requires ED25519 Sui keys"); return Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress(); }
|
|
28
29
|
function createKey(path) { if (!existsSync(path))
|
|
@@ -48,20 +49,51 @@ async function setup() {
|
|
|
48
49
|
fail("DEMO_ROLE_PROFILE_MISMATCH", "demo setup needs fresh isolated live v2 role homes; choose a new --state-dir instead of reusing v1 or mock profiles");
|
|
49
50
|
}
|
|
50
51
|
const workflowPath = typeof args.workflow === "string" ? resolve(args.workflow) : !mock && typeof args["seller-url"] !== "string" ? resolve(value("workflow")) : undefined;
|
|
52
|
+
let activeCapabilities = [];
|
|
51
53
|
if (workflowPath) {
|
|
52
|
-
const workflow = JSON.parse(readFileSync(workflowPath, "utf8")),
|
|
53
|
-
if (!
|
|
54
|
+
const workflow = JSON.parse(readFileSync(workflowPath, "utf8")), configured = workflow.capabilities ?? [];
|
|
55
|
+
if (!configured.length)
|
|
54
56
|
fail("DEMO_CAPABILITIES_REQUIRED", "live workflow must provide capability manifests for the buyer mandate");
|
|
55
|
-
for (const manifest of
|
|
57
|
+
for (const manifest of configured)
|
|
56
58
|
validateCapabilityManifest(manifest);
|
|
59
|
+
let capabilities = configured;
|
|
60
|
+
if (!mock && typeof args["seller-url"] === "string") {
|
|
61
|
+
const response = await fetch(new URL("/v1/catalog", new URL(args["seller-url"])), { signal: AbortSignal.timeout(90_000) });
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
fail("SELLER_SERVICE_UNAVAILABLE", `remote seller catalog returned ${response.status}`);
|
|
64
|
+
const served = await response.json();
|
|
65
|
+
const allowedIds = new Set(configured.map((item) => item.capabilityId)), entries = (served.capabilities ?? []).filter((item) => item.manifest && allowedIds.has(item.manifest.capabilityId));
|
|
66
|
+
if (!entries.length || entries.some((item) => !item.manifest || typeof item.blobId !== "string"))
|
|
67
|
+
fail("SELLER_CATALOG_INVALID", "remote seller catalog has no configured capabilities with Walrus identities");
|
|
68
|
+
const app = new Phase3App(roles.buyer);
|
|
69
|
+
try {
|
|
70
|
+
const discovered = (await app.execute("capability.discover", { "blob-ids": Buffer.from(JSON.stringify(entries.map((item) => item.blobId))).toString("base64url") })).data.capabilities;
|
|
71
|
+
const verified = new Map(discovered.map((item) => [`${item.capabilityId}:${item.blobId}`, item.manifestHash]));
|
|
72
|
+
if (entries.some((item) => verified.get(`${item.manifest.capabilityId}:${item.blobId}`) !== item.manifest.manifestHash))
|
|
73
|
+
fail("SELLER_CATALOG_INVALID", "remote seller catalog differs from its Walrus manifests");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
app.close();
|
|
77
|
+
}
|
|
78
|
+
capabilities = entries.map((item) => item.manifest);
|
|
79
|
+
}
|
|
80
|
+
activeCapabilities = capabilities;
|
|
57
81
|
const mandatePath = join(roles.buyer, "mandate.json"), mandate = JSON.parse(readFileSync(mandatePath, "utf8"));
|
|
58
82
|
mandate.requireSettlementPlan = true;
|
|
59
83
|
mandate.allowedCommands = [...new Set([...(Array.isArray(mandate.allowedCommands) ? mandate.allowedCommands : []), "settlement.plan"])];
|
|
84
|
+
mandate.allowedCounterparties = [...new Set(capabilities.map((item) => item.sellerId))];
|
|
60
85
|
mandate.allowedCapabilityIds = [...new Set(capabilities.map((item) => item.capabilityId))];
|
|
61
86
|
mandate.allowedCapabilityClasses = [...new Set(capabilities.map((item) => item.capability))];
|
|
62
87
|
mandate.allowedRails = [...new Set(capabilities.flatMap((item) => item.supportedRails))];
|
|
63
88
|
writeFileSync(mandatePath, `${JSON.stringify(mandate, null, 2)}\n`, { mode: 0o600 });
|
|
64
89
|
chmodSync(mandatePath, 0o600);
|
|
90
|
+
const buyerProfilePath = join(roles.buyer, "profile.json"), buyerProfile = JSON.parse(readFileSync(buyerProfilePath, "utf8"));
|
|
91
|
+
if (!buyerProfile.policy)
|
|
92
|
+
fail("DEMO_ROLE_PROFILE_MISMATCH", "buyer profile has no payment policy");
|
|
93
|
+
const buyerPolicy = buyerProfile.policy;
|
|
94
|
+
buyerPolicy.allowedCounterparties = [...new Set(capabilities.map((item) => item.sellerId))];
|
|
95
|
+
writeFileSync(buyerProfilePath, `${JSON.stringify(buyerProfile, null, 2)}\n`, { mode: 0o600 });
|
|
96
|
+
chmodSync(buyerProfilePath, 0o600);
|
|
65
97
|
}
|
|
66
98
|
const mcpScript = join(dirname(fileURLToPath(import.meta.url)), "mcp.js");
|
|
67
99
|
const mcp = { mcpServers: Object.fromEntries(Object.entries(roles).map(([name, home]) => [`supercuts-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`, { command: process.execPath, args: [mcpScript], env: { SUPERCUTS_HOME: home } }])) };
|
|
@@ -75,8 +107,10 @@ async function setup() {
|
|
|
75
107
|
writeFileSync(join(root, "buyer-codex.toml"), codex, { mode: 0o600 });
|
|
76
108
|
}
|
|
77
109
|
const remote = typeof args["seller-url"] === "string";
|
|
78
|
-
writeFileSync(join(root, "run.json"), `${JSON.stringify({ schema: "supercuts.demo.run.v1", createdAt: new Date().toISOString(), runId, ...(remote ? { eventPath } : {}), mock, homes: roles, addresses }, null, 2)}\n`, { mode: 0o600 });
|
|
79
|
-
|
|
110
|
+
writeFileSync(join(root, "run.json"), `${JSON.stringify({ schema: "supercuts.demo.run.v1", createdAt: new Date().toISOString(), runId, ...(remote ? { eventPath, sellerUrl: new URL(String(args["seller-url"])).toString() } : {}), mock, homes: roles, addresses }, null, 2)}\n`, { mode: 0o600 });
|
|
111
|
+
const publicSellers = activeCapabilities.filter((item) => item.capability === "sui.market-brief"), analysisSeller = activeCapabilities.find((item) => item.capability === "sui.competitor-analysis");
|
|
112
|
+
const settlementSellers = activeCapabilities.length ? { publicA: publicSellers[0]?.sellerId, publicB: publicSellers[1]?.sellerId, analysis: analysisSeller?.sellerId } : { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller };
|
|
113
|
+
return { stateDir: root, mock, funding: mock ? [] : [{ address: addresses.buyer, needs: ["Testnet SUI gas", "configured x402 asset", "shielded deposit value"] }], sellerPayTo: settlementSellers.publicA, sellerAddresses: settlementSellers, localRoleAddresses: { publicA: addresses.publicSeller, publicB: addresses.secondPublicSeller, analysis: addresses.confidentialSeller, auditor: addresses.auditor }, files: { mcp: join(root, "mcp.json"), run: join(root, "run.json"), events: remote ? eventPath : join(root, "events.jsonl"), ...(remote ? { codex: join(root, "buyer-codex.toml") } : {}) } };
|
|
80
114
|
}
|
|
81
115
|
async function doctor() {
|
|
82
116
|
if (typeof args["buyer-home"] === "string") {
|
|
@@ -103,6 +137,11 @@ async function doctor() {
|
|
|
103
137
|
const report = (await app.execute("doctor", {})).data;
|
|
104
138
|
if (report.readyForDemo === false)
|
|
105
139
|
fail("DEMO_PREREQUISITES_MISSING", `${name}: ${report.fixes.join(" ")}`);
|
|
140
|
+
if (name === "buyer") {
|
|
141
|
+
const mandate = loadMandate(home), profileSellers = [...app.profile.policy.allowedCounterparties].sort(), mandateSellers = [...mandate.allowedCounterparties].sort();
|
|
142
|
+
if (JSON.stringify(profileSellers) !== JSON.stringify(mandateSellers))
|
|
143
|
+
fail("POLICY_COUNTERPARTY_MISMATCH", "buyer mandate and payment profile allow different sellers; rerun setup refresh before funding");
|
|
144
|
+
}
|
|
106
145
|
reports[name] = report;
|
|
107
146
|
}
|
|
108
147
|
finally {
|
|
@@ -114,16 +153,24 @@ async function doctor() {
|
|
|
114
153
|
const base = new URL(args["seller-url"]), health = await fetch(new URL("/health", base), { signal: AbortSignal.timeout(90_000) }), catalog = await fetch(new URL("/v1/catalog", base), { signal: AbortSignal.timeout(30_000) });
|
|
115
154
|
if (!health.ok || !catalog.ok)
|
|
116
155
|
fail("SELLER_SERVICE_UNAVAILABLE", "remote seller health or catalog check failed");
|
|
156
|
+
const healthBody = await health.json(), buyerSigner = String(reports.buyer?.signer ?? ""), accessUrl = new URL("/v1/access", base);
|
|
157
|
+
accessUrl.searchParams.set("buyerAddress", buyerSigner);
|
|
158
|
+
const access = await fetch(accessUrl, { signal: AbortSignal.timeout(30_000) });
|
|
159
|
+
if (!access.ok)
|
|
160
|
+
fail("SELLER_SERVICE_UNAVAILABLE", "remote seller access preflight failed");
|
|
161
|
+
const accessBody = await access.json();
|
|
162
|
+
if (accessBody.allowed !== true)
|
|
163
|
+
fail("SELLER_BUYER_FORBIDDEN", "remote seller does not currently accept this buyer; use open access or complete owner enrollment before funding");
|
|
117
164
|
const body = await catalog.json(), capabilities = body.capabilities ?? [];
|
|
118
165
|
if (capabilities.filter((item) => item.manifest?.capability === "sui.market-brief").length < 2 || !capabilities.some((item) => item.manifest?.capability === "sui.competitor-analysis"))
|
|
119
166
|
fail("SELLER_CATALOG_INVALID", "remote catalog needs two briefs and one competitor analysis");
|
|
120
|
-
remote = { healthy: true, origin: base.origin, capabilities: capabilities.length };
|
|
167
|
+
remote = { healthy: true, origin: base.origin, capabilities: capabilities.length, buyerAccess: accessBody.mode ?? healthBody.buyerAccess ?? "unknown", buyerAllowed: true };
|
|
121
168
|
}
|
|
122
169
|
return { healthy: true, roles: reports, isolation: new Set(Object.values(roles)).size === 5, ...(remote ? { remote } : {}) };
|
|
123
170
|
}
|
|
124
171
|
async function seedPolicy() {
|
|
125
172
|
const source = JSON.parse(readFileSync(resolve(value("workflow")), "utf8"));
|
|
126
|
-
|
|
173
|
+
let manifest = source.capabilities?.find((item) => item.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "workflow has no competitor analysis capability");
|
|
127
174
|
validateCapabilityManifest(manifest);
|
|
128
175
|
if (manifest.capability !== "sui.competitor-analysis")
|
|
129
176
|
fail("DEMO_POLICY_SCOPE_INVALID", "privacy policy must target the competitor analysis capability");
|
|
@@ -131,6 +178,21 @@ async function seedPolicy() {
|
|
|
131
178
|
try {
|
|
132
179
|
if (app.profile.mock)
|
|
133
180
|
fail("LIVE_PROFILE_REQUIRED", "saved demo privacy policy requires a live buyer profile");
|
|
181
|
+
const runPath = join(stateDir(), "run.json"), run = existsSync(runPath) ? JSON.parse(readFileSync(runPath, "utf8")) : {};
|
|
182
|
+
if (run.sellerUrl) {
|
|
183
|
+
const response = await fetch(new URL("/v1/catalog", run.sellerUrl), { signal: AbortSignal.timeout(90_000) });
|
|
184
|
+
if (!response.ok)
|
|
185
|
+
fail("SELLER_SERVICE_UNAVAILABLE", `remote seller catalog returned ${response.status}`);
|
|
186
|
+
const body = await response.json();
|
|
187
|
+
const entry = body.capabilities?.find((item) => item.manifest?.capabilityId === manifest.capabilityId && item.manifest.capability === "sui.competitor-analysis") ?? fail("DEMO_POLICY_SCOPE_INVALID", "remote catalog has no matching competitor analysis capability");
|
|
188
|
+
if (!entry.manifest || typeof entry.blobId !== "string")
|
|
189
|
+
fail("SELLER_CATALOG_INVALID", "remote analysis capability has no Walrus identity");
|
|
190
|
+
const liveManifest = entry.manifest, liveBlobId = entry.blobId;
|
|
191
|
+
const discovered = (await app.execute("capability.discover", { "blob-ids": Buffer.from(JSON.stringify([liveBlobId])).toString("base64url") })).data.capabilities;
|
|
192
|
+
if (discovered.length !== 1 || discovered[0].blobId !== liveBlobId || discovered[0].manifestHash !== liveManifest.manifestHash)
|
|
193
|
+
fail("SELLER_CATALOG_INVALID", "remote analysis capability differs from its Walrus manifest");
|
|
194
|
+
manifest = liveManifest;
|
|
195
|
+
}
|
|
134
196
|
const memory = { version: "supercuts.settlement-memory.v1", kind: "privacy_rule", scope: { capabilityId: manifest.capabilityId, counterparty: manifest.sellerId, network: "sui:testnet", asset: manifest.asset }, createdAt: Date.now(), signal: { requiresShielded: true } };
|
|
135
197
|
const parameters = { "operation-id": value("operation-id", `privacy-${manifest.capabilityId}`), memory: Buffer.from(JSON.stringify(memory)).toString("base64url") };
|
|
136
198
|
const token = app.approve(app.preview("memory.policy", parameters).challengeId);
|
|
@@ -440,7 +502,7 @@ async function replay() {
|
|
|
440
502
|
return { label: "recorded funded run", freshTransaction: false, events };
|
|
441
503
|
}
|
|
442
504
|
try {
|
|
443
|
-
const data = command === "setup" ? await setup() : command === "doctor" ? await doctor() : command === "seed-policy" ? await seedPolicy() : command === "reuse-memories" ? reuseMemories() : command === "check" ? await check() : command === "live" ? await live() : command === "replay" ? await replay() : fail("COMMAND_UNKNOWN", "use setup, doctor, seed-policy, reuse-memories, live, replay, or check");
|
|
505
|
+
const data = ["help", "--help", "-h"].includes(command) ? help() : command === "setup" ? await setup() : command === "doctor" ? await doctor() : command === "seed-policy" ? await seedPolicy() : command === "reuse-memories" ? reuseMemories() : command === "check" ? await check() : command === "live" ? await live() : command === "replay" ? await replay() : fail("COMMAND_UNKNOWN", "use setup, doctor, seed-policy, reuse-memories, live, replay, or check");
|
|
444
506
|
send(data);
|
|
445
507
|
}
|
|
446
508
|
catch (error) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface FundingPlan {
|
|
2
|
+
address: string;
|
|
3
|
+
publicBalanceMist: string;
|
|
4
|
+
shieldedBalanceMist: string;
|
|
5
|
+
publicPaymentMist: string;
|
|
6
|
+
shieldedPaymentMist: string;
|
|
7
|
+
gasReserveMist: string;
|
|
8
|
+
changeReserveMist: string;
|
|
9
|
+
poolDepositMist: string;
|
|
10
|
+
requiredPublicMist: string;
|
|
11
|
+
publicShortfallMist: string;
|
|
12
|
+
faucetRequired: boolean;
|
|
13
|
+
depositRequired: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function calculateFundingPlan(input: {
|
|
16
|
+
address: string;
|
|
17
|
+
publicBalance: bigint;
|
|
18
|
+
shieldedBalance: bigint;
|
|
19
|
+
publicPayment: bigint;
|
|
20
|
+
shieldedPayment: bigint;
|
|
21
|
+
gasReserve: bigint;
|
|
22
|
+
changeReserve: bigint;
|
|
23
|
+
}): FundingPlan;
|
|
24
|
+
export declare function prepareFunding(home: string, values: {
|
|
25
|
+
publicPayment: string;
|
|
26
|
+
shieldedPayment: string;
|
|
27
|
+
gasReserve?: string;
|
|
28
|
+
changeReserve?: string;
|
|
29
|
+
}): Promise<FundingPlan>;
|
|
30
|
+
export declare function executeFunding(home: string, values: {
|
|
31
|
+
publicPayment: string;
|
|
32
|
+
shieldedPayment: string;
|
|
33
|
+
gasReserve?: string;
|
|
34
|
+
changeReserve?: string;
|
|
35
|
+
operationId: string;
|
|
36
|
+
requestFaucet: boolean;
|
|
37
|
+
confirmed: boolean;
|
|
38
|
+
}): Promise<Record<string, unknown>>;
|
package/dist/funding.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { fail } from "./errors.js";
|
|
4
|
+
import { Phase3App } from "./phase3.js";
|
|
5
|
+
import { writeClient } from "./sui.js";
|
|
6
|
+
const amount = (value, name) => /^[0-9]+$/.test(value) ? BigInt(value) : fail("FUNDING_INPUT_INVALID", `${name} must be unsigned MIST`);
|
|
7
|
+
const POOL_DEPOSIT_DENOMINATIONS = [10000000n, 100000000n];
|
|
8
|
+
const standardPoolDeposit = (shortfall) => {
|
|
9
|
+
if (shortfall === 0n)
|
|
10
|
+
return 0n;
|
|
11
|
+
const denomination = POOL_DEPOSIT_DENOMINATIONS.find((candidate) => candidate >= shortfall);
|
|
12
|
+
return denomination ?? fail("FUNDING_PLAN_UNSUPPORTED", `shielded funding needs ${shortfall} MIST, above the largest supported single deposit of ${POOL_DEPOSIT_DENOMINATIONS.at(-1)} MIST`);
|
|
13
|
+
};
|
|
14
|
+
export function calculateFundingPlan(input) {
|
|
15
|
+
const shieldedTarget = input.shieldedPayment > 0n ? input.shieldedPayment + input.changeReserve : 0n;
|
|
16
|
+
const poolShortfall = shieldedTarget > input.shieldedBalance ? shieldedTarget - input.shieldedBalance : 0n;
|
|
17
|
+
const poolDeposit = standardPoolDeposit(poolShortfall);
|
|
18
|
+
const requiredPublic = input.publicPayment + poolDeposit + input.gasReserve;
|
|
19
|
+
const publicShortfall = requiredPublic > input.publicBalance ? requiredPublic - input.publicBalance : 0n;
|
|
20
|
+
return { address: input.address, publicBalanceMist: input.publicBalance.toString(), shieldedBalanceMist: input.shieldedBalance.toString(), publicPaymentMist: input.publicPayment.toString(), shieldedPaymentMist: input.shieldedPayment.toString(), gasReserveMist: input.gasReserve.toString(), changeReserveMist: input.changeReserve.toString(), poolDepositMist: poolDeposit.toString(), requiredPublicMist: requiredPublic.toString(), publicShortfallMist: publicShortfall.toString(), faucetRequired: publicShortfall > 0n, depositRequired: poolDeposit > 0n };
|
|
21
|
+
}
|
|
22
|
+
export async function prepareFunding(home, values) {
|
|
23
|
+
const publicPayment = amount(values.publicPayment, "public payment"), shieldedPayment = amount(values.shieldedPayment, "shielded payment");
|
|
24
|
+
const gasReserve = amount(values.gasReserve ?? "100000000", "gas reserve"), changeReserve = amount(values.changeReserve ?? "1000000", "change reserve");
|
|
25
|
+
const app = new Phase3App(home);
|
|
26
|
+
try {
|
|
27
|
+
if (app.profile.role !== "buyer" || app.profile.mock)
|
|
28
|
+
fail("LIVE_PROFILE_REQUIRED", "funding requires a live buyer role home");
|
|
29
|
+
const doctor = await app.execute("doctor", {}), address = String(doctor.data.signer ?? "");
|
|
30
|
+
const endpoint = app.profile.live?.writeGrpcEndpoint ?? fail("LIVE_PROFILE_INVALID", "buyer write RPC is missing");
|
|
31
|
+
const [{ balance }, shielded] = await Promise.all([writeClient(endpoint).getBalance({ owner: address, coinType: "0x2::sui::SUI" }), app.execute("wallet.balance", {})]);
|
|
32
|
+
return calculateFundingPlan({ address, publicBalance: BigInt(balance.balance), shieldedBalance: BigInt(String(shielded.data.available ?? "0")), publicPayment, shieldedPayment, gasReserve, changeReserve });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
app.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function executeFunding(home, values) {
|
|
39
|
+
if (!values.confirmed)
|
|
40
|
+
fail("FUNDING_CONFIRMATION_REQUIRED", "funding execution requires --confirm after reviewing the plan");
|
|
41
|
+
let plan = await prepareFunding(home, values);
|
|
42
|
+
if (plan.faucetRequired) {
|
|
43
|
+
if (!values.requestFaucet)
|
|
44
|
+
fail("BUYER_FUNDING_REQUIRED", `buyer needs ${plan.publicShortfallMist} additional MIST; rerun with --request-faucet --confirm or fund ${plan.address} manually`);
|
|
45
|
+
execFileSync("sui", ["client", "faucet", "--url", "https://faucet.testnet.sui.io", "--address", plan.address], { stdio: "inherit" });
|
|
46
|
+
for (let attempt = 0; attempt < 15; attempt++) {
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
48
|
+
plan = await prepareFunding(home, values);
|
|
49
|
+
if (!plan.faucetRequired)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (plan.faucetRequired)
|
|
53
|
+
fail("FAUCET_ALLOCATION_INSUFFICIENT", `the public faucet allocation did not reach the required ${plan.requiredPublicMist} MIST; wait for finality and inspect funding status before requesting another allocation`);
|
|
54
|
+
}
|
|
55
|
+
if (!plan.depositRequired)
|
|
56
|
+
return { funded: true, faucetRequested: values.requestFaucet, deposit: "not-required", plan };
|
|
57
|
+
if (!values.operationId)
|
|
58
|
+
fail("ARGUMENT_REQUIRED", "funding execute requires --operation-id for safe retry");
|
|
59
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
60
|
+
try {
|
|
61
|
+
if (process.stdin.isTTY) {
|
|
62
|
+
const answer = (await rl.question(`Deposit ${plan.poolDepositMist} MIST into the experimental Testnet pool? type yes: `)).trim().toLowerCase();
|
|
63
|
+
if (answer !== "yes")
|
|
64
|
+
fail("FUNDING_CANCELLED", "pool deposit was not confirmed");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
rl.close();
|
|
69
|
+
}
|
|
70
|
+
const app = new Phase3App(home);
|
|
71
|
+
try {
|
|
72
|
+
const deposit = await app.execute("wallet.deposit", { amount: plan.poolDepositMist, "operation-id": values.operationId, authorize: true });
|
|
73
|
+
return { funded: true, faucetRequested: values.requestFaucet, deposit: deposit.data, plan: await prepareFunding(home, values) };
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
app.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/live-runtime.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { Groth16Prover, type ProverConfig } from "./prover.js";
|
|
|
5
5
|
import { type QueuedExit } from "./v2-exits.js";
|
|
6
6
|
import { TatumLedger } from "./tatum.js";
|
|
7
7
|
import type { RoleStore } from "./store.js";
|
|
8
|
+
export declare function x402ResourceFailure(status: number, response: Uint8Array): string;
|
|
9
|
+
export declare function timestampExpirationUnsupported(value: unknown): boolean;
|
|
8
10
|
export interface LiveRuntimeProfile {
|
|
9
11
|
role: "buyer" | "seller" | "auditor";
|
|
10
12
|
packageId: string;
|
|
@@ -52,6 +54,7 @@ export declare class LiveRuntime {
|
|
|
52
54
|
private existingSigned;
|
|
53
55
|
private submit;
|
|
54
56
|
private confirmTatum;
|
|
57
|
+
paymentStatus(operationId: string, reconcile?: boolean): Promise<Record<string, unknown>>;
|
|
55
58
|
private prepareOpening;
|
|
56
59
|
deposit(amount: bigint, operationId: string): Promise<Record<string, unknown>>;
|
|
57
60
|
recover(): Promise<Record<string, unknown>>;
|
package/dist/live-runtime.js
CHANGED
|
@@ -10,6 +10,38 @@ import { buildDeposit, buildPrivateTransfer, buildWithdraw, writeClient } from "
|
|
|
10
10
|
import { V2ExitQueue } from "./v2-exits.js";
|
|
11
11
|
import { TatumLedger } from "./tatum.js";
|
|
12
12
|
import { paymentBindingBytes, requirementsHash } from "./x402.js";
|
|
13
|
+
export function x402ResourceFailure(status, response) {
|
|
14
|
+
const fallback = `paid resource returned ${status}`;
|
|
15
|
+
let value;
|
|
16
|
+
try {
|
|
17
|
+
value = JSON.parse(Buffer.from(response).toString("utf8"));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
return fallback;
|
|
24
|
+
const record = value, code = typeof record.error === "string" ? record.error.replace(/\s+/g, " ").slice(0, 128) : "", message = typeof record.message === "string" ? record.message.replace(/\s+/g, " ").slice(0, 512) : "";
|
|
25
|
+
return code || message ? `${fallback}: ${[code, message].filter(Boolean).join(": ")}` : fallback;
|
|
26
|
+
}
|
|
27
|
+
export function timestampExpirationUnsupported(value) {
|
|
28
|
+
const messages = [];
|
|
29
|
+
const visit = (item, depth = 0) => {
|
|
30
|
+
if (depth > 3 || item == null)
|
|
31
|
+
return;
|
|
32
|
+
if (typeof item === "string")
|
|
33
|
+
messages.push(item);
|
|
34
|
+
else if (item instanceof Error) {
|
|
35
|
+
messages.push(item.message);
|
|
36
|
+
visit(item.cause, depth + 1);
|
|
37
|
+
}
|
|
38
|
+
else if (typeof item === "object")
|
|
39
|
+
for (const nested of Object.values(item))
|
|
40
|
+
visit(nested, depth + 1);
|
|
41
|
+
};
|
|
42
|
+
visit(value);
|
|
43
|
+
return messages.some((message) => /timestamp-based transaction expiration is not yet supported|timestamp.*expiration.*not supported/i.test(message));
|
|
44
|
+
}
|
|
13
45
|
const hex = (value) => value.toString(16).padStart(64, "0");
|
|
14
46
|
const bytes32 = (value) => Buffer.from(hex(value), "hex");
|
|
15
47
|
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -87,7 +119,7 @@ export class LiveRuntime {
|
|
|
87
119
|
}
|
|
88
120
|
if (this.profile.role === "buyer" && !this.store.getSecret("live:memory-config"))
|
|
89
121
|
fixes.push("Configure an owner-only Walrus Memory file using the local 1536-dimensional embedding adapter.");
|
|
90
|
-
return { healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
|
|
122
|
+
return { role: this.profile.role, network: "testnet", packageId: this.profile.packageId, poolId: this.profile.poolId, asset: this.profile.coinType, verifierVersion: this.profile.verifierVersion, keyPermissions: "owner-only", rpc: this.profile.live.writeGrpcEndpoint, healthy: true, readyForDemo: fixes.length === 0, fixes, runtime: "testnet-live", signer: this.signer.toSuiAddress(), babyJubPublicKey: identity.publicKey.map(String), x25519PublicKey: this.store.getSecret("identity:x25519-public")?.toString("base64"), rapidSnark: artifacts.rapidSnark, artifacts: artifacts.artifacts, poolObjectVerified: true, tatumServiceVerified: true };
|
|
91
123
|
}
|
|
92
124
|
async balance() {
|
|
93
125
|
const state = await this.index.sync();
|
|
@@ -165,6 +197,29 @@ export class LiveRuntime {
|
|
|
165
197
|
async confirmTatum(digest, checkpoint) {
|
|
166
198
|
assertTatumFinality(await this.tatum.getTransaction(digest), digest, checkpoint);
|
|
167
199
|
}
|
|
200
|
+
async paymentStatus(operationId, reconcile = false) {
|
|
201
|
+
const row = this.store.db.prepare("SELECT digest,status,updated_at AS updatedAt FROM live_signed_transactions WHERE operation_id=?").get(operationId);
|
|
202
|
+
if (!row)
|
|
203
|
+
fail("OPERATION_NOT_FOUND", "payment operation was not found in the live transaction journal");
|
|
204
|
+
const payment = row;
|
|
205
|
+
const context = this.store.db.prepare("SELECT kind FROM live_operation_context WHERE operation_id=?").get(operationId);
|
|
206
|
+
const delivered = Boolean(this.store.db.prepare("SELECT 1 FROM live_x402_deliveries WHERE operation_id=?").get(operationId));
|
|
207
|
+
if (payment.status === "finalized")
|
|
208
|
+
return { operationId, status: "finalized", transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "not-applicable-or-pending", finalized: true, safeToResumeSameOperationId: true };
|
|
209
|
+
if (!reconcile)
|
|
210
|
+
return { operationId, status: payment.status, transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: false, chainStatus: "unchecked", safeToResumeSameOperationId: true };
|
|
211
|
+
let transaction;
|
|
212
|
+
try {
|
|
213
|
+
transaction = await this.client.waitForTransaction({ digest: payment.digest, timeout: 5_000, include: { effects: true } });
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return { operationId, status: payment.status, transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: false, chainStatus: "not-found-or-pending", safeToResumeSameOperationId: true };
|
|
217
|
+
}
|
|
218
|
+
const checkpoint = assertSuiFinality(transaction, payment.digest);
|
|
219
|
+
await this.confirmTatum(payment.digest, checkpoint);
|
|
220
|
+
this.store.db.prepare("UPDATE live_signed_transactions SET status='finalized',updated_at=? WHERE operation_id=?").run(Date.now(), operationId);
|
|
221
|
+
return { operationId, status: "finalized", transactionDigest: payment.digest, kind: context?.kind ?? "unknown", deliveryStatus: delivered ? "delivered" : "pending", finalized: true, checkpoint, tatumVerified: true, safeToResumeSameOperationId: true };
|
|
222
|
+
}
|
|
168
223
|
prepareOpening(opening, role, operationId, leaves) {
|
|
169
224
|
const commitment = hex(opening.commitment), key = `note:${commitment}`;
|
|
170
225
|
this.store.db.exec("BEGIN IMMEDIATE");
|
|
@@ -534,21 +589,40 @@ export class LiveRuntime {
|
|
|
534
589
|
fail("POLICY_ASSET_FORBIDDEN", "challenged asset is not allowed");
|
|
535
590
|
if (!this.profile.policy.allowedCounterparties.includes(payTo))
|
|
536
591
|
fail("POLICY_COUNTERPARTY_FORBIDDEN", "challenged payee is not allowed");
|
|
537
|
-
const extra = requirements.extra, requestId = String(extra?.requestId ?? operationId), challengeOperationId = String(extra?.operationId ?? operationId), nonce = String(extra?.nonce ?? "");
|
|
538
|
-
|
|
539
|
-
|
|
592
|
+
const extra = requirements.extra, requestId = String(extra?.requestId ?? operationId), challengeOperationId = String(extra?.operationId ?? operationId), nonce = String(extra?.nonce ?? ""), expiresAt = Number(extra?.expiresAt);
|
|
593
|
+
const unsignedRequirements = { ...requirements, extra: { requestId, operationId: challengeOperationId, nonce, expiresAt } };
|
|
594
|
+
if (challengeOperationId !== operationId || !nonce || !Number.isSafeInteger(expiresAt) || expiresAt <= Date.now() || String(extra?.requirementsHash ?? "") !== requirementsHash(unsignedRequirements))
|
|
595
|
+
fail("X402_CHALLENGE_INVALID", "challenge does not bind this operation, request, and expiry");
|
|
540
596
|
bind?.({ amount: amount.toString(), counterparty: payTo, asset, endpoint: resourceUrl, requestId, requirementsHash: String(extra?.requirementsHash) });
|
|
541
597
|
const tx = new (await import("@mysten/sui/transactions")).Transaction();
|
|
542
598
|
tx.transferObjects([tx.coin({ balance: amount, type: asset })], payTo);
|
|
599
|
+
tx.setSender(this.signer.toSuiAddress());
|
|
600
|
+
await tx.build({ client: this.client });
|
|
601
|
+
const resolvedExpiration = tx.getData().expiration;
|
|
602
|
+
const validity = resolvedExpiration?.$kind === "ValidDuring" ? resolvedExpiration.ValidDuring : fail("X402_TRANSACTION_EXPIRY_INVALID", "Sui did not resolve a bounded transaction validity window");
|
|
603
|
+
if (validity.minEpoch == null || validity.maxEpoch == null || !validity.chain || validity.nonce == null)
|
|
604
|
+
fail("X402_TRANSACTION_EXPIRY_INVALID", "Sui resolved an incomplete transaction validity window");
|
|
605
|
+
tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: expiresAt, chain: validity.chain, nonce: validity.nonce } });
|
|
606
|
+
try {
|
|
607
|
+
const probe = await this.client.simulateTransaction({ transaction: tx, include: { effects: true } });
|
|
608
|
+
const simulated = probe.Transaction ?? probe.FailedTransaction;
|
|
609
|
+
if (!simulated?.status.success && timestampExpirationUnsupported(simulated?.status?.error))
|
|
610
|
+
tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: null, chain: validity.chain, nonce: validity.nonce } });
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
if (!timestampExpirationUnsupported(error))
|
|
614
|
+
throw error;
|
|
615
|
+
tx.setExpiration({ ValidDuring: { minEpoch: validity.minEpoch, maxEpoch: validity.maxEpoch, minTimestamp: validity.minTimestamp ?? null, maxTimestamp: null, chain: validity.chain, nonce: validity.nonce } });
|
|
616
|
+
}
|
|
543
617
|
const legacyIntentHash = hash(JSON.stringify({ domain: "supercuts-x402-v2", operationId, resourceUrl, payTo, asset, amount: amount.toString() }));
|
|
544
618
|
const intentHash = hash(JSON.stringify({ domain: "supercuts-x402-v2", operationId, resourceUrl, payTo, asset, amount: amount.toString(), requestId, requirementsHash: String(extra?.requirementsHash) })), signed = await this.signed(operationId, intentHash, tx, legacyIntentHash);
|
|
545
|
-
const unsignedBinding = { requestId, nonce, requirementsHash: requirementsHash(
|
|
619
|
+
const unsignedBinding = { requestId, nonce, requirementsHash: requirementsHash(unsignedRequirements), transactionDigest: signed.digest, payer: this.signer.toSuiAddress() };
|
|
546
620
|
const payerSignature = (await this.signer.signPersonalMessage(paymentBindingBytes(unsignedBinding))).signature;
|
|
547
621
|
const paymentPayload = { x402Version: 2, resource, accepted: requirements, payload: { signature: signed.signature, transaction: Buffer.from(signed.transaction).toString("base64") } };
|
|
548
622
|
const paid = await fetch(paymentUrl, { redirect: "error", headers: { "payment-signature": Buffer.from(JSON.stringify(paymentPayload)).toString("base64"), "x-supercuts-binding": Buffer.from(JSON.stringify({ ...unsignedBinding, payerSignature })).toString("base64") }, signal: AbortSignal.timeout(60_000) });
|
|
549
623
|
const response = Buffer.from(await paid.arrayBuffer());
|
|
550
624
|
if (!paid.ok)
|
|
551
|
-
fail("X402_DELIVERY_FAILED",
|
|
625
|
+
fail("X402_DELIVERY_FAILED", x402ResourceFailure(paid.status, response));
|
|
552
626
|
const settlementHeader = paid.headers.get("payment-response") ?? fail("X402_SETTLEMENT_INVALID", "payment-response header is missing");
|
|
553
627
|
let settlement;
|
|
554
628
|
try {
|
package/dist/mandate.js
CHANGED
|
@@ -14,8 +14,8 @@ const definitions = {
|
|
|
14
14
|
"wallet.balance": object({}, [], false, "Read event-derived private-note balance."),
|
|
15
15
|
"wallet.recover": object({ commitment: string("Optional payment-note commitment."), amount: string("Optional matching amount.", { pattern: "^[1-9][0-9]*$" }) }, [], true, "Recover notes from pinned finalized pool events."),
|
|
16
16
|
"wallet.withdraw": object({ commitment: string("Note commitment to spend."), recipient: string("Sui recipient address.") }, ["commitment"], true, "Withdraw one owned shielded note."),
|
|
17
|
-
"wallet.exit.queue": object({ commitment: string("Recovered note commitment."), amount: string("Public exit amount in MIST.", { pattern: "^[1-9][0-9]*$" }), "exact-exit": boolean("Approve a nonstandard amount that is publicly visible."), "window-start": string("Exit window start as Unix milliseconds."), "window-end": string("Exit window end as Unix milliseconds.") }, ["commitment", "amount", "window-start", "window-end"], true, "Queue a
|
|
18
|
-
"wallet.exit.status": object({ "operation-id": string("Queued exit operation ID.") }, ["operation-id"], false, "Read a queued
|
|
17
|
+
"wallet.exit.queue": object({ commitment: string("Recovered note commitment."), amount: string("Public exit amount in MIST.", { pattern: "^[1-9][0-9]*$" }), "exact-exit": boolean("Approve a nonstandard amount that is publicly visible."), "window-start": string("Exit window start as Unix milliseconds."), "window-end": string("Exit window end as Unix milliseconds.") }, ["commitment", "amount", "window-start", "window-end"], true, "Queue a delayed pool exit to a fresh payout address."),
|
|
18
|
+
"wallet.exit.status": object({ "operation-id": string("Queued exit operation ID.") }, ["operation-id"], false, "Read a queued pool exit without exposing its proof or note opening."),
|
|
19
19
|
"wallet.exit.finalize": object({ "exit-id": string("Queued exit operation ID."), digest: string("Finalized relayer transaction digest.") }, ["exit-id", "digest"], true, "Verify Sui and Tatum exit evidence and reconcile the owner note journal."),
|
|
20
20
|
"offer.open": object({ session: string("Optional stable negotiation session ID.") }, [], true, "Open a durable negotiation session."),
|
|
21
21
|
"offer.propose": object({ session: string("Negotiation session ID."), revision: string("Positive revision.", { pattern: "^[1-9][0-9]*$" }), terms: string("Plain-language or canonical JSON terms.") }, ["session", "revision", "terms"], true, "Submit an offer revision."),
|
|
@@ -24,7 +24,7 @@ const definitions = {
|
|
|
24
24
|
"offer.show": object({ session: string("Negotiation session ID.") }, ["session"], false, "Read the latest durable offer revision."),
|
|
25
25
|
"pay.quote": object({ rail: string("Requested settlement rail.", { enum: ["x402", "shielded"] }), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved payee."), asset: string("Sui coin type.") }, ["rail", "amount", "counterparty"], false, "Evaluate payment policy without signing."),
|
|
26
26
|
"pay.shielded": object({ session: string("Accepted agreement session."), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved seller."), asset: string("Sui coin type."), "request-id": string("Bound delivery request ID."), "broadcast-only": boolean("Stop after broadcast for recovery testing."), "plan-hash": string("Approved settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, [], true, "Settle one accepted agreement through the experimental shielded pool."),
|
|
27
|
-
"pay.x402": object({ "resource-url": string("Seller-owned protected resource URL."), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved public payee."), asset: string("Sui coin type."), "request-id": string("Bound delivery request ID."), "broadcast-only": boolean("Stop after broadcast for recovery testing."), "plan-hash": string("Approved settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, [], true, "
|
|
27
|
+
"pay.x402": object({ "resource-url": string("Seller-owned protected resource URL."), amount: string("Unsigned base-unit amount.", { pattern: "^[1-9][0-9]*$" }), counterparty: string("Mandate-approved public payee."), asset: string("Sui coin type."), "request-id": string("Bound delivery request ID."), "broadcast-only": boolean("Stop after broadcast for recovery testing."), "plan-hash": string("Approved settlement plan hash.", { pattern: "^[0-9a-f]{64}$" }) }, [], true, "Make a public Sui payment through an x402 exact payment challenge."),
|
|
28
28
|
"pay.status": object({ "operation-id": string("Payment operation identity."), reconcile: boolean("Reconcile an ambiguous broadcast.") }, ["operation-id"], false, "Read or reconcile a previously journaled payment."),
|
|
29
29
|
"resource.serve": object({ "request-id": string("Unique request identity."), body: string("Base64-encoded artifact bytes.") }, ["request-id", "body"], true, "Bind one generated artifact to one request."),
|
|
30
30
|
"resource.fetch": object({ "request-id": string("Bound request identity."), "operation-id": string("Finalized payment operation."), response: string("Base64-encoded paid response.") }, ["request-id", "operation-id", "response"], false, "Record delivery separately from payment evidence."),
|
|
@@ -43,13 +43,14 @@ const definitions = {
|
|
|
43
43
|
"deal.delivery": object({ dealId: string("Existing delivered deal ID.") }, ["dealId"], false, "Read a verified paid artifact from the buyer's encrypted journal."),
|
|
44
44
|
"remote.catalog": object({}, [], false, "Discover the signed remote seller catalog through Walrus."),
|
|
45
45
|
"remote.rank-briefs": object({ budget: string("Total budget in MIST.", { pattern: "^[1-9][0-9]*$" }), topic: string("Sui research topic.", { maxLength: 256 }) }, ["budget", "topic"], false, "Compare eligible market briefs with a visible quality and price rubric."),
|
|
46
|
-
"remote.negotiate": object({ capabilityId: string("Selected capability ID."), sessionId: string("Stable negotiation session ID."), runId: string("
|
|
47
|
-
"remote.quote": object({ capabilityId: string("Selected capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("
|
|
48
|
-
"remote.purchase-brief": object({ capabilityId: string("Selected market brief capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("
|
|
49
|
-
"remote.agreement": object({ capabilityId: string("Analysis capability ID."), operationId: string("Stable shielded payment operation ID."), sessionId: string("Stable negotiation session ID."), runId: string("
|
|
46
|
+
"remote.negotiate": object({ capabilityId: string("Selected capability ID."), sessionId: string("Stable negotiation session ID."), runId: string("Run ID."), task: string("Private task summary.", { maxLength: 512 }), opening: string("Opening amount in MIST.", { pattern: "^[1-9][0-9]*$" }), maximum: string("Maximum amount in MIST.", { pattern: "^[1-9][0-9]*$" }), briefHash: string("Purchased brief SHA-256 if analyzing it.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "sessionId", "runId", "task", "opening", "maximum"], false, "Exchange an encrypted buyer offer and seller response through Walrus."),
|
|
47
|
+
"remote.quote": object({ capabilityId: string("Selected capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("Run ID."), task: string("Brief task summary.", { maxLength: 512 }), briefHash: string("Purchased brief SHA-256 for an analysis variant.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "operationId", "requestId", "runId", "task"], false, "Ask a remote seller for its exact x402 quote and protected resource URL."),
|
|
48
|
+
"remote.purchase-brief": object({ capabilityId: string("Selected market brief capability ID."), operationId: string("Stable public payment operation ID."), requestId: string("Stable artifact request ID."), runId: string("Run ID."), task: string("Brief task summary.", { maxLength: 512 }), budget: string("Maximum brief spend in MIST.", { pattern: "^[1-9][0-9]*$" }) }, ["capabilityId", "operationId", "requestId", "runId", "task", "budget"], false, "Plan, buy, verify, and deliver the selected market brief through x402."),
|
|
49
|
+
"remote.agreement": object({ capabilityId: string("Analysis capability ID."), operationId: string("Stable shielded payment operation ID."), sessionId: string("Stable negotiation session ID."), runId: string("Run ID."), briefHash: string("Delivered brief SHA-256.", { pattern: "^[0-9a-f]{64}$" }) }, ["capabilityId", "operationId", "sessionId", "runId", "briefHash"], false, "Request and accept a seller-signed analysis agreement bound to the purchased brief and saved privacy rule."),
|
|
50
50
|
"remote.settle-analysis": object({ sessionId: string("Accepted analysis session ID."), operationId: string("Stable shielded payment operation ID.") }, ["sessionId", "operationId"], false, "Settle the accepted analysis agreement through the approved shielded pool plan."),
|
|
51
51
|
"remote.delivery": object({ sessionId: string("Accepted analysis session ID."), operationId: string("Finalized shielded payment operation ID.") }, ["sessionId", "operationId"], false, "Recover the seller payment and decrypt the delivered analysis."),
|
|
52
|
-
"remote.events": object({ runId: string("
|
|
52
|
+
"remote.events": object({ runId: string("Run ID.") }, ["runId"], false, "Read redacted remote seller activity for the run."),
|
|
53
|
+
"remote.research": object({ topic: string("Sui research topic.", { maxLength: 256 }), budget: string("Total workflow budget in MIST.", { pattern: "^[1-9][0-9]*$" }), runId: string("Optional active run ID; normally supplied by the MCP session.") }, ["topic", "budget"], true, "Rank, purchase, and deliver a public Sui brief plus its matching privacy-aware analysis using one resumable operation."),
|
|
53
54
|
disclose: object({ "deal-id": string("Deal scope."), auditor: string("Designated auditor identity."), "payment-digest": string("Finalized payment digest."), "transcript-root": string("Authenticated agreement transcript root.") }, ["deal-id", "auditor", "payment-digest", "transcript-root"], true, "Create a deal- and auditor-scoped disclosure package."),
|
|
54
55
|
"audit.verify": object({ package: string("Seal ciphertext or mock disclosure package."), auditor: string("This auditor identity."), "deal-id": string("Exact deal scope.") }, ["package", "auditor", "deal-id"], false, "Verify agreement and payment only; delivery remains a separate observation."),
|
|
55
56
|
};
|
package/dist/mcp-bootstrap.js
CHANGED
|
@@ -12,6 +12,13 @@ if (existsSync(join(home, "profile.json"))) {
|
|
|
12
12
|
else {
|
|
13
13
|
const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
14
14
|
const rl = createInterface({ input: process.stdin });
|
|
15
|
+
let promoting = false;
|
|
16
|
+
const promote = () => {
|
|
17
|
+
if (promoting)
|
|
18
|
+
return;
|
|
19
|
+
promoting = true;
|
|
20
|
+
setImmediate(() => { rl.close(); void import("./mcp.js"); });
|
|
21
|
+
};
|
|
15
22
|
rl.on("line", (line) => {
|
|
16
23
|
if (Buffer.byteLength(line) > 1024 * 1024)
|
|
17
24
|
return send({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Request exceeds 1 MiB" } });
|
|
@@ -29,11 +36,26 @@ else {
|
|
|
29
36
|
if (request.method === "notifications/initialized")
|
|
30
37
|
return;
|
|
31
38
|
if (request.method === "tools/list")
|
|
32
|
-
return send({ jsonrpc: "2.0", id: request.id, result: { tools: [
|
|
39
|
+
return send({ jsonrpc: "2.0", id: request.id, result: { tools: [
|
|
40
|
+
{ name: "setup.status", description: "Explain how the owner completes one-time buyer setup locally. Do not request secrets in chat.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
41
|
+
{ name: "setup.refresh", description: "Reload this MCP connection after the owner completes local setup.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
42
|
+
] } });
|
|
33
43
|
if (request.method === "tools/call" && request.params?.name === "setup.status") {
|
|
34
|
-
const
|
|
44
|
+
const configured = existsSync(join(home, "profile.json"));
|
|
45
|
+
const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured, home, nextStep: configured ? "Call setup.refresh to load the configured buyer tools without restarting the connector." : `Run supercuts onboard buyer --home ${JSON.stringify(home)} in a terminal, then call setup.refresh. The command asks for policy limits, seller origins and local credential file paths. Do not paste keys or tokens into Codex.` };
|
|
35
46
|
return send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
|
|
36
47
|
}
|
|
48
|
+
if (request.method === "tools/call" && request.params?.name === "setup.refresh") {
|
|
49
|
+
if (!existsSync(join(home, "profile.json"))) {
|
|
50
|
+
const result = { schemaVersion: "supercuts.setup.v1", ok: false, error: { code: "SETUP_REQUIRED", message: "buyer setup is not complete", recovery: `Run supercuts onboard buyer --home ${JSON.stringify(home)} locally, then retry setup.refresh.` } };
|
|
51
|
+
return send({ jsonrpc: "2.0", id: request.id, result: { isError: true, content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
|
|
52
|
+
}
|
|
53
|
+
const result = { schemaVersion: "supercuts.setup.v1", ok: true, configured: true, reloading: true };
|
|
54
|
+
send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
|
|
55
|
+
send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
56
|
+
promote();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
37
59
|
return send({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } });
|
|
38
60
|
});
|
|
39
61
|
}
|