@supercuts-walrus/backend 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +32 -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 +511 -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/funding.d.ts +38 -0
- package/dist/funding.js +78 -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 +85 -0
- package/dist/live-runtime.js +644 -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 +179 -0
- package/dist/mcp-bootstrap.d.ts +2 -0
- package/dist/mcp-bootstrap.js +61 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +207 -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 +87 -0
- package/dist/phase3.d.ts +100 -0
- package/dist/phase3.js +1174 -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 +85 -0
- package/dist/remote-buyer.js +285 -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/role-lock.d.ts +8 -0
- package/dist/role-lock.js +54 -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-package.d.ts +9 -0
- package/dist/seller-package.js +50 -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 +422 -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 +36 -0
- package/dist/x402-resource-service.js +173 -0
- package/dist/x402.d.ts +46 -0
- package/dist/x402.js +136 -0
- package/package.json +60 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { backup, DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { fail } from "./errors.js";
|
|
6
|
+
const FILES = ["profile.json", "mandate.json", "state.key", "state.sqlite"];
|
|
7
|
+
function keyBytes(encoded) {
|
|
8
|
+
const key = Buffer.from(encoded, "base64url");
|
|
9
|
+
if (key.length !== 32)
|
|
10
|
+
fail("SELLER_STATE_KEY_INVALID", "seller snapshot key must contain 32 bytes");
|
|
11
|
+
return key;
|
|
12
|
+
}
|
|
13
|
+
export function encryptSellerSnapshot(snapshot, encodedKey) {
|
|
14
|
+
const nonce = randomBytes(12), cipher = createCipheriv("aes-256-gcm", keyBytes(encodedKey), nonce);
|
|
15
|
+
cipher.setAAD(Buffer.from(`supercuts-seller-state-v1:${snapshot.sellerId}`));
|
|
16
|
+
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(snapshot)), cipher.final()]);
|
|
17
|
+
return { version: 1, sellerId: snapshot.sellerId, nonce: nonce.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), ciphertext: ciphertext.toString("base64url") };
|
|
18
|
+
}
|
|
19
|
+
export function decryptSellerSnapshot(value, encodedKey) {
|
|
20
|
+
try {
|
|
21
|
+
if (value.version !== 1 || !value.sellerId)
|
|
22
|
+
fail("SELLER_STATE_INVALID", "seller snapshot version or identity is invalid");
|
|
23
|
+
const decipher = createDecipheriv("aes-256-gcm", keyBytes(encodedKey), Buffer.from(value.nonce, "base64url"));
|
|
24
|
+
decipher.setAAD(Buffer.from(`supercuts-seller-state-v1:${value.sellerId}`));
|
|
25
|
+
decipher.setAuthTag(Buffer.from(value.tag, "base64url"));
|
|
26
|
+
const snapshot = JSON.parse(Buffer.concat([decipher.update(Buffer.from(value.ciphertext, "base64url")), decipher.final()]).toString());
|
|
27
|
+
if (snapshot.version !== 1 || snapshot.sellerId !== value.sellerId || !snapshot.artifacts || typeof snapshot.artifacts !== "object" || FILES.some((name) => typeof snapshot.files?.[name] !== "string"))
|
|
28
|
+
fail("SELLER_STATE_INVALID", "seller snapshot contents are invalid");
|
|
29
|
+
return snapshot;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return fail("SELLER_STATE_DECRYPT_FAILED", "seller snapshot could not be authenticated", error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function captureSellerHome(home, sellerId, artifacts = {}) {
|
|
36
|
+
const db = new DatabaseSync(join(home, "state.sqlite"), { readOnly: true });
|
|
37
|
+
const temporary = join(home, `snapshot-${randomBytes(8).toString("hex")}.sqlite`);
|
|
38
|
+
try {
|
|
39
|
+
await backup(db, temporary);
|
|
40
|
+
const files = {};
|
|
41
|
+
for (const name of FILES)
|
|
42
|
+
files[name] = readFileSync(name === "state.sqlite" ? temporary : join(home, name)).toString("base64url");
|
|
43
|
+
return { version: 1, sellerId, files, artifacts };
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
db.close();
|
|
47
|
+
if (existsSync(temporary)) {
|
|
48
|
+
const { unlinkSync } = await import("node:fs");
|
|
49
|
+
unlinkSync(temporary);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function restoreSellerHome(home, snapshot) {
|
|
54
|
+
if (!snapshot.sellerId || snapshot.version !== 1)
|
|
55
|
+
fail("SELLER_STATE_INVALID", "seller snapshot identity is invalid");
|
|
56
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
57
|
+
chmodSync(home, 0o700);
|
|
58
|
+
for (const name of FILES)
|
|
59
|
+
writeFileSync(join(home, name), Buffer.from(snapshot.files[name], "base64url"), { mode: 0o600, flag: "wx" });
|
|
60
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import { SellerRuntime } from "./seller-runtime.js";
|
|
3
|
+
/** Local JSONL adapter. The host owns the seller signer, payment verifier,
|
|
4
|
+
* handler, and Walrus transport; callers receive only public registration
|
|
5
|
+
* metadata and encrypted delivery references. */
|
|
6
|
+
export declare function serveSellerStdio(runtime: SellerRuntime, input?: Readable, output?: Writable): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { SupercutsError } from "./errors.js";
|
|
3
|
+
/** Local JSONL adapter. The host owns the seller signer, payment verifier,
|
|
4
|
+
* handler, and Walrus transport; callers receive only public registration
|
|
5
|
+
* metadata and encrypted delivery references. */
|
|
6
|
+
export async function serveSellerStdio(runtime, input = process.stdin, output = process.stdout) {
|
|
7
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
8
|
+
let queue = Promise.resolve();
|
|
9
|
+
const send = (value) => { output.write(`${JSON.stringify(value)}\n`); };
|
|
10
|
+
for await (const line of lines) {
|
|
11
|
+
queue = queue.then(async () => {
|
|
12
|
+
let id = null;
|
|
13
|
+
try {
|
|
14
|
+
if (Buffer.byteLength(line) > 1024 * 1024)
|
|
15
|
+
throw new SupercutsError("SELLER_REQUEST_INVALID", "seller stdio request exceeds 1 MiB");
|
|
16
|
+
const parsed = JSON.parse(line);
|
|
17
|
+
if (!parsed || typeof parsed.id !== "string" || !parsed.id || !["catalog", "fulfill"].includes(parsed.method))
|
|
18
|
+
throw new SupercutsError("SELLER_REQUEST_INVALID", "seller stdio method or request ID is invalid");
|
|
19
|
+
id = parsed.id;
|
|
20
|
+
const result = parsed.method === "catalog" ? { capabilities: runtime.catalog() } : await runtime.fulfill(parsed.params);
|
|
21
|
+
send({ id, ok: true, result });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
send({ id, ok: false, error: { code: error instanceof SupercutsError ? error.code : "SELLER_REQUEST_FAILED", message: error instanceof SupercutsError ? error.message : "seller request failed" } });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
await queue;
|
|
29
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
5
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
6
|
+
import { verifySignedCapability, validateCapabilityPayload } from "./capability.js";
|
|
7
|
+
import { fail } from "./errors.js";
|
|
8
|
+
import { SellerDatabase } from "./seller-database.js";
|
|
9
|
+
import { captureSellerHome, decryptSellerSnapshot, encryptSellerSnapshot } from "./seller-state.js";
|
|
10
|
+
import { Phase3App } from "./phase3.js";
|
|
11
|
+
async function main() {
|
|
12
|
+
const configPath = resolve(process.argv[2] ?? fail("ARGUMENT_REQUIRED", "usage: supercuts-seller-v2-provision <owner-only-config.json>"));
|
|
13
|
+
if ((statSync(configPath).mode & 0o077) !== 0)
|
|
14
|
+
fail("SELLER_CONFIG_PERMISSIONS", "provision config must be owner-only");
|
|
15
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
16
|
+
if (!Array.isArray(config.sellers) || !config.sellers.length)
|
|
17
|
+
fail("SELLER_CONFIG_INVALID", "at least one seller is required");
|
|
18
|
+
const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
|
|
19
|
+
const db = new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY"));
|
|
20
|
+
const stateKey = required("SELLER_STATE_KEY"), origin = new URL(required("SELLER_ORIGIN")).origin;
|
|
21
|
+
for (const seller of config.sellers) {
|
|
22
|
+
const stored = await db.getSnapshot(seller.sellerId);
|
|
23
|
+
let snapshot;
|
|
24
|
+
if (stored)
|
|
25
|
+
snapshot = decryptSellerSnapshot(stored.payload, stateKey);
|
|
26
|
+
else {
|
|
27
|
+
if (!seller.home)
|
|
28
|
+
fail("SELLER_STATE_MISSING", "new hosted v2 seller requires its separate local role home");
|
|
29
|
+
const home = resolve(seller.home);
|
|
30
|
+
if ((statSync(home).mode & 0o077) !== 0)
|
|
31
|
+
fail("SELLER_CONFIG_PERMISSIONS", "seller role home must be owner-only");
|
|
32
|
+
const app = new Phase3App(home);
|
|
33
|
+
try {
|
|
34
|
+
if (app.profile.role !== "seller" || app.profile.mock || app.profile.verifierVersion !== 2)
|
|
35
|
+
fail("SELLER_POOL_VERSION_MISMATCH", "new hosted v2 seller requires a live v2 seller role home");
|
|
36
|
+
const encoded = app.store.getSecret("live:sui-private-key")?.toString() ?? fail("LIVE_CREDENTIAL_MISSING", "seller signer is missing");
|
|
37
|
+
const decoded = decodeSuiPrivateKey(encoded);
|
|
38
|
+
if (decoded.scheme !== "ED25519" || Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress() !== seller.sellerId)
|
|
39
|
+
fail("SELLER_IDENTITY_MISMATCH", "v2 seller home signer differs from configured seller ID");
|
|
40
|
+
snapshot = await captureSellerHome(home, seller.sellerId);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
app.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const profile = JSON.parse(Buffer.from(snapshot.files["profile.json"], "base64url").toString());
|
|
47
|
+
if (profile.role !== "seller" || profile.mock || profile.verifierVersion !== 2)
|
|
48
|
+
fail("SELLER_POOL_VERSION_MISMATCH", "v2 capability cannot use a v1 seller snapshot; create a separate v2 role home");
|
|
49
|
+
const registrations = [];
|
|
50
|
+
for (const item of seller.capabilities) {
|
|
51
|
+
const path = resolve(item.registrationFile);
|
|
52
|
+
if ((statSync(path).mode & 0o077) !== 0)
|
|
53
|
+
fail("SELLER_CONFIG_PERMISSIONS", "registration file must be owner-only");
|
|
54
|
+
const registration = JSON.parse(readFileSync(path, "utf8"));
|
|
55
|
+
await verifySignedCapability(registration);
|
|
56
|
+
if (registration.manifest.sellerId !== seller.sellerId || new URL(registration.manifest.endpoint).origin !== origin || !new URL(registration.manifest.endpoint).pathname.startsWith("/v2/resource/"))
|
|
57
|
+
fail("SELLER_CONFIG_INVALID", "v2 registration seller or endpoint differs from the hosted service");
|
|
58
|
+
validateCapabilityPayload(registration.manifest, "output", item.outputMetadata);
|
|
59
|
+
const artifact = readFileSync(resolve(item.artifactFile));
|
|
60
|
+
if (!artifact.length || artifact.length > 10 * 1024 * 1024)
|
|
61
|
+
fail("SELLER_ARTIFACT_INVALID", "v2 artifact must be 1 byte to 10 MiB");
|
|
62
|
+
snapshot.artifacts[registration.manifest.capabilityId] = artifact.toString("base64url");
|
|
63
|
+
registrations.push({ registration, item });
|
|
64
|
+
}
|
|
65
|
+
await db.putSnapshot(encryptSellerSnapshot(snapshot, stateKey), stored?.revision);
|
|
66
|
+
for (const { registration, item } of registrations)
|
|
67
|
+
await db.putCatalog({ seller_id: seller.sellerId, capability_id: registration.manifest.capabilityId, manifest: registration.manifest, blob_id: registration.manifestBlobId, signature: registration.sellerSignature, transport_public_key: registration.transportPublicKey, preview: { genericV2: true, outputMetadata: item.outputMetadata, contentType: item.contentType ?? "application/octet-stream" } });
|
|
68
|
+
process.stdout.write(`${JSON.stringify({ ok: true, sellerId: seller.sellerId, capabilities: registrations.map(({ registration }) => registration.manifest.capabilityId), restartRequired: true })}\n`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "v2 provisioning failed"}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fail } from "./errors.js";
|
|
3
|
+
import { parseArgs } from "./phase3.js";
|
|
4
|
+
const args = parseArgs(process.argv.slice(2));
|
|
5
|
+
const required = (name) => typeof args[name] === "string" ? args[name] : fail("ARGUMENT_REQUIRED", `--${name} is required`);
|
|
6
|
+
const origin = new URL(required("seller-url")), sellerId = required("seller-id"), runId = required("run-id");
|
|
7
|
+
if (origin.protocol !== "https:" && !(origin.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(origin.hostname)))
|
|
8
|
+
fail("SELLER_ORIGIN_INVALID", "seller URL must use HTTPS except on loopback");
|
|
9
|
+
let stopped = false, seen = 0;
|
|
10
|
+
process.on("SIGINT", () => { stopped = true; });
|
|
11
|
+
process.on("SIGTERM", () => { stopped = true; });
|
|
12
|
+
const line = (value) => process.stdout.write(`${value}\n`);
|
|
13
|
+
try {
|
|
14
|
+
const catalogResponse = await fetch(new URL("/v1/catalog", origin), { signal: AbortSignal.timeout(90_000) });
|
|
15
|
+
if (!catalogResponse.ok)
|
|
16
|
+
fail("SELLER_SERVICE_UNAVAILABLE", `catalog returned ${catalogResponse.status}`);
|
|
17
|
+
const catalog = await catalogResponse.json();
|
|
18
|
+
const offered = catalog.capabilities?.filter((item) => item.manifest.sellerId === sellerId).map((item) => item.manifest.capabilityId) ?? [];
|
|
19
|
+
if (!offered.length)
|
|
20
|
+
fail("SELLER_ID_UNKNOWN", "seller does not appear in the remote catalog");
|
|
21
|
+
line(`[${sellerId.slice(0, 10)}] online · ${offered.join(", ")}`);
|
|
22
|
+
while (!stopped) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(new URL(`/v1/events?runId=${encodeURIComponent(runId)}`, origin), { signal: AbortSignal.timeout(30_000) });
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
throw new Error(`events returned ${response.status}`);
|
|
27
|
+
const body = await response.json();
|
|
28
|
+
const events = body.events ?? [];
|
|
29
|
+
for (const event of events.slice(seen))
|
|
30
|
+
if (event.seller_id === sellerId) {
|
|
31
|
+
const details = event.details ?? {}, summary = [details.capabilityId, details.sessionId, details.transcriptHash, details.artifactSha256].filter((item) => typeof item === "string").map((item) => item.length > 18 ? `${item.slice(0, 18)}…` : item).join(" · ");
|
|
32
|
+
line(`${event.created_at} ${event.stage} ${event.status}${summary ? ` ${summary}` : ""}`);
|
|
33
|
+
}
|
|
34
|
+
seen = events.length;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
line(`waiting for seller events: ${error instanceof Error ? error.message : "connection failed"}`);
|
|
38
|
+
}
|
|
39
|
+
await new Promise((done) => setTimeout(done, 2000));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
process.stderr.write(`${error instanceof Error ? error.message : "seller watch failed"}\n`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CapabilityManifest, CapabilityRail } from "./capability.js";
|
|
2
|
+
export declare const SETTLEMENT_PLAN_VERSION: "supercuts.settlement-plan.v1";
|
|
3
|
+
export declare const SETTLEMENT_MEMORY_VERSION: "supercuts.settlement-memory.v1";
|
|
4
|
+
export type SettlementRisk = "low" | "medium" | "review_required";
|
|
5
|
+
export type SettlementMemoryProvenance = "owner_policy" | "verified_outcome" | "agent_observation" | "legacy_untrusted";
|
|
6
|
+
export type SettlementMemoryKind = "privacy_rule" | "budget_limit" | "reliability" | "quality" | "outcome";
|
|
7
|
+
export interface SettlementMemoryScope {
|
|
8
|
+
counterparty?: string;
|
|
9
|
+
capabilityId?: string;
|
|
10
|
+
capabilityClass?: string;
|
|
11
|
+
network?: "sui:testnet";
|
|
12
|
+
asset?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SettlementMemoryV1 {
|
|
15
|
+
version: typeof SETTLEMENT_MEMORY_VERSION;
|
|
16
|
+
kind: SettlementMemoryKind;
|
|
17
|
+
provenance: SettlementMemoryProvenance;
|
|
18
|
+
scope: SettlementMemoryScope;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
evidenceHash?: string;
|
|
21
|
+
observation?: string;
|
|
22
|
+
signal: {
|
|
23
|
+
requiresShielded?: boolean;
|
|
24
|
+
maximumAmount?: string;
|
|
25
|
+
reliability?: number;
|
|
26
|
+
quality?: number;
|
|
27
|
+
outcome?: "succeeded" | "failed" | "unresolved";
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface SettlementCitation {
|
|
31
|
+
blobId: string;
|
|
32
|
+
kind: SettlementMemoryKind | "legacy";
|
|
33
|
+
provenance: SettlementMemoryProvenance;
|
|
34
|
+
distance: number;
|
|
35
|
+
}
|
|
36
|
+
export type SettlementReasonCode = "MANDATE_DEFAULTS_APPLIED" | "MEMORY_PRIVACY_APPLIED" | "MEMORY_BUDGET_APPLIED" | "MEMORY_OUTCOME_APPLIED" | "MEMORY_LOW_RELEVANCE_IGNORED" | "MEMORY_UNTRUSTED_ADVISORY" | "OWNER_POLICY_CONFLICT" | "RISK_LOW" | "RISK_MEDIUM" | "RISK_REVIEW_REQUIRED" | "RAIL_X402_SELECTED" | "RAIL_SHIELDED_SELECTED" | "REQUESTED_AMOUNT_CLAMPED" | "OWNER_REVIEW_APPROVED";
|
|
37
|
+
export interface SettlementPlanV1 {
|
|
38
|
+
version: typeof SETTLEMENT_PLAN_VERSION;
|
|
39
|
+
planHash: string;
|
|
40
|
+
operationId: string;
|
|
41
|
+
status: "approved" | "review_required";
|
|
42
|
+
mandateHash: string;
|
|
43
|
+
manifestHash: string;
|
|
44
|
+
capabilityId: string;
|
|
45
|
+
capabilityClass: string;
|
|
46
|
+
sellerId: string;
|
|
47
|
+
endpoint: string;
|
|
48
|
+
network: "sui:testnet";
|
|
49
|
+
asset: string;
|
|
50
|
+
rail: CapabilityRail;
|
|
51
|
+
openingAmount: string;
|
|
52
|
+
maximumAmount: string;
|
|
53
|
+
manifestMinimum: string;
|
|
54
|
+
manifestMaximum: string;
|
|
55
|
+
riskLevel: SettlementRisk;
|
|
56
|
+
createdAt: number;
|
|
57
|
+
expiresAt: number;
|
|
58
|
+
citations: SettlementCitation[];
|
|
59
|
+
reasonCodes: SettlementReasonCode[];
|
|
60
|
+
approvedFromPlanHash?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface PlanningMandate {
|
|
63
|
+
version: "supercuts.mandate.v1" | "supercuts.mandate.v2";
|
|
64
|
+
allowedCounterparties: string[];
|
|
65
|
+
allowedAssets: string[];
|
|
66
|
+
perDealCeiling: string;
|
|
67
|
+
requiredPrivacy: "any" | "shielded";
|
|
68
|
+
expiresAt: number;
|
|
69
|
+
allowedCapabilityIds?: string[];
|
|
70
|
+
allowedCapabilityClasses?: string[];
|
|
71
|
+
allowedRails?: CapabilityRail[];
|
|
72
|
+
allowPublicX402?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface RecalledSettlementMemory {
|
|
75
|
+
blobId: string;
|
|
76
|
+
distance: number;
|
|
77
|
+
memory?: SettlementMemoryV1;
|
|
78
|
+
legacy?: boolean;
|
|
79
|
+
}
|
|
80
|
+
export declare function canonicalJson(value: unknown): string;
|
|
81
|
+
export declare const settlementMandateHash: (mandate: PlanningMandate) => string;
|
|
82
|
+
export declare function settlementPlanHash(plan: Omit<SettlementPlanV1, "planHash">): string;
|
|
83
|
+
export declare function validateSettlementMemory(value: unknown, assignedProvenance?: SettlementMemoryProvenance): asserts value is SettlementMemoryV1;
|
|
84
|
+
export declare function createSettlementPlan(input: {
|
|
85
|
+
operationId: string;
|
|
86
|
+
mandate: PlanningMandate;
|
|
87
|
+
manifests: CapabilityManifest[];
|
|
88
|
+
requestedAmount?: string;
|
|
89
|
+
privacyHint?: "any" | "shielded";
|
|
90
|
+
requestedRail?: CapabilityRail;
|
|
91
|
+
recalled?: RecalledSettlementMemory[];
|
|
92
|
+
maxMemoryDistance?: number;
|
|
93
|
+
now?: number;
|
|
94
|
+
}): SettlementPlanV1;
|
|
95
|
+
export declare function validateSettlementPlan(value: unknown): asserts value is SettlementPlanV1;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { fail } from "./errors.js";
|
|
3
|
+
export const SETTLEMENT_PLAN_VERSION = "supercuts.settlement-plan.v1";
|
|
4
|
+
export const SETTLEMENT_MEMORY_VERSION = "supercuts.settlement-memory.v1";
|
|
5
|
+
export function canonicalJson(value) {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
8
|
+
if (value && typeof value === "object")
|
|
9
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
10
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
11
|
+
fail("SETTLEMENT_JSON_INVALID", "settlement records must contain finite JSON values");
|
|
12
|
+
const encoded = JSON.stringify(value);
|
|
13
|
+
if (encoded === undefined)
|
|
14
|
+
fail("SETTLEMENT_JSON_INVALID", "settlement records must be JSON-compatible");
|
|
15
|
+
return encoded;
|
|
16
|
+
}
|
|
17
|
+
const digest = (domain, value) => createHash("sha256").update(canonicalJson({ domain, value })).digest("hex");
|
|
18
|
+
export const settlementMandateHash = (mandate) => digest("supercuts-settlement-mandate-v1", mandate);
|
|
19
|
+
export function settlementPlanHash(plan) { return digest("supercuts-settlement-plan-v1", plan); }
|
|
20
|
+
const unsigned = (value) => typeof value === "string" && /^(?:0|[1-9][0-9]*)$/.test(value);
|
|
21
|
+
const object = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
const exactKeys = (value, keys) => { if (Object.keys(value).some((key) => !keys.includes(key)))
|
|
23
|
+
fail("SETTLEMENT_MEMORY_INVALID", "record contains unknown fields"); };
|
|
24
|
+
export function validateSettlementMemory(value, assignedProvenance) {
|
|
25
|
+
if (!object(value))
|
|
26
|
+
fail("SETTLEMENT_MEMORY_INVALID", "settlement memory must be an object");
|
|
27
|
+
const raw = value;
|
|
28
|
+
exactKeys(raw, ["version", "kind", "provenance", "scope", "createdAt", "evidenceHash", "observation", "signal"]);
|
|
29
|
+
if (raw.version !== SETTLEMENT_MEMORY_VERSION || !["privacy_rule", "budget_limit", "reliability", "quality", "outcome"].includes(String(raw.kind)))
|
|
30
|
+
fail("SETTLEMENT_MEMORY_INVALID", "settlement memory version or kind is invalid");
|
|
31
|
+
if (!["owner_policy", "verified_outcome", "agent_observation", "legacy_untrusted"].includes(String(raw.provenance)))
|
|
32
|
+
fail("SETTLEMENT_MEMORY_INVALID", "settlement memory provenance is invalid");
|
|
33
|
+
if (assignedProvenance && raw.provenance !== assignedProvenance)
|
|
34
|
+
fail("SETTLEMENT_MEMORY_PROVENANCE", "memory provenance is assigned by Supercuts");
|
|
35
|
+
if (!Number.isSafeInteger(raw.createdAt) || Number(raw.createdAt) < 0 || !object(raw.scope) || !object(raw.signal))
|
|
36
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory timestamp, scope, or signal is invalid");
|
|
37
|
+
if (raw.observation !== undefined && (typeof raw.observation !== "string" || !raw.observation.trim() || raw.observation.length > 512))
|
|
38
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory observation must be a short non-empty string");
|
|
39
|
+
const scope = raw.scope, signal = raw.signal;
|
|
40
|
+
exactKeys(scope, ["counterparty", "capabilityId", "capabilityClass", "network", "asset"]);
|
|
41
|
+
exactKeys(signal, ["requiresShielded", "maximumAmount", "reliability", "quality", "outcome"]);
|
|
42
|
+
for (const item of Object.values(scope))
|
|
43
|
+
if (typeof item !== "string" || !item || item.length > 1024)
|
|
44
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory scope fields must be non-empty strings");
|
|
45
|
+
if (scope.network !== undefined && scope.network !== "sui:testnet")
|
|
46
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory network must be sui:testnet");
|
|
47
|
+
if (signal.requiresShielded !== undefined && typeof signal.requiresShielded !== "boolean" || signal.maximumAmount !== undefined && !unsigned(signal.maximumAmount))
|
|
48
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory privacy or amount signal is invalid");
|
|
49
|
+
for (const score of [signal.reliability, signal.quality])
|
|
50
|
+
if (score !== undefined && (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 1))
|
|
51
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory scores must be finite values in [0,1]");
|
|
52
|
+
if (signal.outcome !== undefined && !["succeeded", "failed", "unresolved"].includes(signal.outcome))
|
|
53
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory outcome is invalid");
|
|
54
|
+
const required = { privacy_rule: "requiresShielded", budget_limit: "maximumAmount", reliability: "reliability", quality: "quality", outcome: "outcome" };
|
|
55
|
+
if (signal[required[raw.kind]] === undefined)
|
|
56
|
+
fail("SETTLEMENT_MEMORY_INVALID", "memory kind does not have its required signal");
|
|
57
|
+
if (raw.provenance === "verified_outcome" && (typeof raw.evidenceHash !== "string" || !/^[0-9a-f]{64}$/.test(raw.evidenceHash)))
|
|
58
|
+
fail("SETTLEMENT_MEMORY_EVIDENCE_REQUIRED", "verified outcomes require a SHA-256 evidence hash");
|
|
59
|
+
if (raw.evidenceHash !== undefined && (typeof raw.evidenceHash !== "string" || !/^[0-9a-f]{64}$/.test(raw.evidenceHash)))
|
|
60
|
+
fail("SETTLEMENT_MEMORY_INVALID", "evidence hash must be SHA-256 hex");
|
|
61
|
+
}
|
|
62
|
+
function specificity(scope, manifest) {
|
|
63
|
+
if (scope.network && scope.network !== "sui:testnet" || scope.asset && scope.asset !== manifest.asset || scope.counterparty && scope.counterparty !== manifest.sellerId || scope.capabilityClass && scope.capabilityClass !== manifest.capability || scope.capabilityId && scope.capabilityId !== manifest.capabilityId)
|
|
64
|
+
return -1;
|
|
65
|
+
return scope.capabilityId ? 4 : scope.capabilityClass ? 3 : scope.counterparty ? 2 : 1;
|
|
66
|
+
}
|
|
67
|
+
export function createSettlementPlan(input) {
|
|
68
|
+
const now = input.now ?? Date.now(), maxDistance = input.maxMemoryDistance ?? 0.35;
|
|
69
|
+
if (!input.operationId || input.operationId.length > 256 || !input.manifests.length)
|
|
70
|
+
fail("SETTLEMENT_PLAN_INVALID", "planning requires an operation ID and at least one capability");
|
|
71
|
+
if (input.requestedAmount !== undefined && !unsigned(input.requestedAmount))
|
|
72
|
+
fail("SETTLEMENT_PLAN_INVALID", "requestedAmount must be canonical unsigned base units");
|
|
73
|
+
if (!Number.isFinite(maxDistance) || maxDistance < 0 || maxDistance > 1 || now >= input.mandate.expiresAt)
|
|
74
|
+
fail("SETTLEMENT_PLAN_DENIED", "planning limits or owner mandate are invalid");
|
|
75
|
+
const ignored = (input.recalled ?? []).filter((item) => !Number.isFinite(item.distance) || item.distance < 0 || item.distance > maxDistance);
|
|
76
|
+
const recalled = (input.recalled ?? []).filter((item) => Number.isFinite(item.distance) && item.distance >= 0 && item.distance <= maxDistance);
|
|
77
|
+
for (const item of recalled)
|
|
78
|
+
if (item.memory)
|
|
79
|
+
validateSettlementMemory(item.memory);
|
|
80
|
+
const candidates = [];
|
|
81
|
+
for (const manifest of input.manifests) {
|
|
82
|
+
if (manifest.expiresAt <= now || input.mandate.allowedCapabilityIds && !input.mandate.allowedCapabilityIds.includes(manifest.capabilityId) || input.mandate.allowedCapabilityClasses && !input.mandate.allowedCapabilityClasses.includes(manifest.capability) || !input.mandate.allowedCounterparties.includes(manifest.sellerId) || !input.mandate.allowedAssets.includes(manifest.asset))
|
|
83
|
+
continue;
|
|
84
|
+
const matching = recalled.filter((item) => item.memory && specificity(item.memory.scope, manifest) >= 0);
|
|
85
|
+
const citations = recalled.map((item) => ({ blobId: item.blobId, kind: item.memory?.kind ?? "legacy", provenance: item.memory?.provenance ?? "legacy_untrusted", distance: item.distance }));
|
|
86
|
+
const trusted = matching.filter((item) => item.memory && ["owner_policy", "verified_outcome"].includes(item.memory.provenance));
|
|
87
|
+
const latest = (kind, provenance) => {
|
|
88
|
+
let values = trusted.filter((item) => item.memory.kind === kind && (!provenance || item.memory.provenance === provenance));
|
|
89
|
+
if (!provenance && values.some((item) => item.memory.provenance === "owner_policy"))
|
|
90
|
+
values = values.filter((item) => item.memory.provenance === "owner_policy");
|
|
91
|
+
const best = Math.max(-1, ...values.map((item) => specificity(item.memory.scope, manifest)));
|
|
92
|
+
const sorted = values.filter((item) => specificity(item.memory.scope, manifest) === best).sort((a, b) => b.memory.createdAt - a.memory.createdAt || b.blobId.localeCompare(a.blobId));
|
|
93
|
+
if (!sorted.length)
|
|
94
|
+
return [];
|
|
95
|
+
return sorted.filter((item) => item.memory.createdAt === sorted[0].memory.createdAt).map((item) => item.memory);
|
|
96
|
+
};
|
|
97
|
+
const policies = latest("privacy_rule"), conflict = policies[0]?.provenance === "owner_policy" && new Set(policies.map((item) => item.signal.requiresShielded)).size > 1;
|
|
98
|
+
const requiresShielded = input.mandate.requiredPrivacy === "shielded" || input.privacyHint === "shielded" || policies.some((item) => item.signal.requiresShielded === true);
|
|
99
|
+
const rails = manifest.supportedRails.filter((rail) => (!input.mandate.allowedRails || input.mandate.allowedRails.includes(rail)) && (!requiresShielded || rail === "shielded") && (input.mandate.version !== "supercuts.mandate.v2" || rail !== "x402" || input.mandate.allowPublicX402 === true) && (!input.requestedRail || rail === input.requestedRail));
|
|
100
|
+
if (!rails.length)
|
|
101
|
+
continue;
|
|
102
|
+
const budgets = trusted.filter((item) => item.memory.kind === "budget_limit").map((item) => BigInt(item.memory.signal.maximumAmount));
|
|
103
|
+
const maximum = [BigInt(manifest.priceBand.maximum), BigInt(input.mandate.perDealCeiling), ...budgets].reduce((a, b) => a < b ? a : b);
|
|
104
|
+
if (maximum < BigInt(manifest.priceBand.minimum))
|
|
105
|
+
continue;
|
|
106
|
+
const reliability = latest("reliability")[0]?.signal.reliability ?? 0.5, quality = latest("quality")[0]?.signal.quality ?? 0.5, outcome = latest("outcome")[0]?.signal.outcome;
|
|
107
|
+
const risk = conflict || outcome === "failed" || outcome === "unresolved" || reliability < 0.5 ? "review_required" : reliability >= 0.8 ? "low" : "medium";
|
|
108
|
+
for (const rail of rails)
|
|
109
|
+
candidates.push({ manifest, rail, reliability, quality, maximum, risk, citations, conflict, reasons: ["MANDATE_DEFAULTS_APPLIED", ...(policies.length ? ["MEMORY_PRIVACY_APPLIED"] : []), ...(budgets.length ? ["MEMORY_BUDGET_APPLIED"] : []), ...(outcome ? ["MEMORY_OUTCOME_APPLIED"] : []), ...(citations.some((c) => ["agent_observation", "legacy_untrusted"].includes(c.provenance)) ? ["MEMORY_UNTRUSTED_ADVISORY"] : []), ...(ignored.length ? ["MEMORY_LOW_RELEVANCE_IGNORED"] : [])] });
|
|
110
|
+
}
|
|
111
|
+
candidates.sort((a, b) => b.reliability - a.reliability || b.quality - a.quality || (a.maximum < b.maximum ? -1 : a.maximum > b.maximum ? 1 : 0) || a.manifest.capabilityId.localeCompare(b.manifest.capabilityId) || (a.rail === b.rail ? 0 : input.mandate.version === "supercuts.mandate.v2" ? a.rail === "shielded" ? -1 : 1 : a.rail === "x402" ? -1 : 1));
|
|
112
|
+
const selected = candidates[0] ?? fail("SETTLEMENT_PLAN_DENIED", "no capability remains within the owner mandate, privacy rules, and price bounds");
|
|
113
|
+
const minimum = BigInt(selected.manifest.priceBand.minimum), desired = input.requestedAmount === undefined ? minimum : BigInt(input.requestedAmount), opening = desired < minimum ? minimum : desired > selected.maximum ? selected.maximum : desired;
|
|
114
|
+
const reasons = [...selected.reasons, ...(selected.conflict ? ["OWNER_POLICY_CONFLICT"] : []), ...(opening !== desired ? ["REQUESTED_AMOUNT_CLAMPED"] : []), selected.rail === "shielded" ? "RAIL_SHIELDED_SELECTED" : "RAIL_X402_SELECTED", selected.risk === "low" ? "RISK_LOW" : selected.risk === "medium" ? "RISK_MEDIUM" : "RISK_REVIEW_REQUIRED"];
|
|
115
|
+
const withoutHash = { version: SETTLEMENT_PLAN_VERSION, operationId: input.operationId, status: selected.risk === "review_required" ? "review_required" : "approved", mandateHash: settlementMandateHash(input.mandate), manifestHash: selected.manifest.manifestHash, capabilityId: selected.manifest.capabilityId, capabilityClass: selected.manifest.capability, sellerId: selected.manifest.sellerId, endpoint: selected.manifest.endpoint, network: "sui:testnet", asset: selected.manifest.asset, rail: selected.rail, openingAmount: opening.toString(), maximumAmount: selected.maximum.toString(), manifestMinimum: selected.manifest.priceBand.minimum, manifestMaximum: selected.manifest.priceBand.maximum, riskLevel: selected.risk, createdAt: now, expiresAt: Math.min(now + 300_000, input.mandate.expiresAt, selected.manifest.expiresAt), citations: selected.citations.sort((a, b) => a.blobId.localeCompare(b.blobId)), reasonCodes: [...new Set(reasons)] };
|
|
116
|
+
return { ...withoutHash, planHash: settlementPlanHash(withoutHash) };
|
|
117
|
+
}
|
|
118
|
+
export function validateSettlementPlan(value) {
|
|
119
|
+
if (!object(value) || value.version !== SETTLEMENT_PLAN_VERSION)
|
|
120
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan has an invalid version");
|
|
121
|
+
const allowed = new Set(["version", "planHash", "operationId", "status", "mandateHash", "manifestHash", "capabilityId", "capabilityClass", "sellerId", "endpoint", "network", "asset", "rail", "openingAmount", "maximumAmount", "manifestMinimum", "manifestMaximum", "riskLevel", "createdAt", "expiresAt", "citations", "reasonCodes", "approvedFromPlanHash"]);
|
|
122
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
123
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan contains unknown fields");
|
|
124
|
+
const plan = value, { planHash, ...unsignedPlan } = plan;
|
|
125
|
+
if (typeof planHash !== "string" || !/^[0-9a-f]{64}$/.test(planHash) || settlementPlanHash(unsignedPlan) !== planHash)
|
|
126
|
+
fail("SETTLEMENT_PLAN_HASH_MISMATCH", "settlement plan hash does not match its canonical record");
|
|
127
|
+
for (const amount of [plan.openingAmount, plan.maximumAmount, plan.manifestMinimum, plan.manifestMaximum])
|
|
128
|
+
if (!unsigned(amount))
|
|
129
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan contains a non-canonical amount");
|
|
130
|
+
if (!Number.isSafeInteger(plan.createdAt) || !Number.isSafeInteger(plan.expiresAt) || plan.expiresAt <= plan.createdAt || BigInt(plan.openingAmount) > BigInt(plan.maximumAmount) || BigInt(plan.maximumAmount) > BigInt(plan.manifestMaximum) || BigInt(plan.openingAmount) < BigInt(plan.manifestMinimum))
|
|
131
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan bounds or timestamps are invalid");
|
|
132
|
+
if (!["approved", "review_required"].includes(plan.status) || !["low", "medium", "review_required"].includes(plan.riskLevel) || !["x402", "shielded"].includes(plan.rail))
|
|
133
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan decision fields are invalid");
|
|
134
|
+
for (const field of [plan.operationId, plan.mandateHash, plan.manifestHash, plan.capabilityId, plan.capabilityClass, plan.sellerId, plan.endpoint, plan.asset])
|
|
135
|
+
if (typeof field !== "string" || !field || field.length > 2048)
|
|
136
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan string fields are invalid");
|
|
137
|
+
if (plan.network !== "sui:testnet" || !/^[0-9a-f]{64}$/.test(plan.mandateHash) || !/^[0-9a-f]{64}$/.test(plan.manifestHash) || plan.approvedFromPlanHash !== undefined && !/^[0-9a-f]{64}$/.test(plan.approvedFromPlanHash))
|
|
138
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan hashes or network are invalid");
|
|
139
|
+
if (!Array.isArray(plan.reasonCodes) || !Array.isArray(plan.citations) || plan.citations.some((citation) => !object(citation) || Object.keys(citation).some((key) => !["blobId", "kind", "provenance", "distance"].includes(key)) || typeof citation.blobId !== "string" || !citation.blobId || typeof citation.distance !== "number" || !Number.isFinite(citation.distance) || citation.distance < 0 || citation.distance > 1))
|
|
140
|
+
fail("SETTLEMENT_PLAN_INVALID", "settlement plan citations or reasons are invalid");
|
|
141
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export type OperationStatus = "draft" | "reserved" | "proof_persisted" | "broadcast" | "finalized" | "confirmed" | "delivered" | "unresolved";
|
|
3
|
+
export interface StoredOperation {
|
|
4
|
+
id: string;
|
|
5
|
+
status: OperationStatus;
|
|
6
|
+
payloadHash: string;
|
|
7
|
+
txDigest?: string;
|
|
8
|
+
updatedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface StoredMessage {
|
|
11
|
+
id: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
sequence: number;
|
|
14
|
+
ciphertextHash: string;
|
|
15
|
+
blobId?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class RoleStore {
|
|
18
|
+
#private;
|
|
19
|
+
readonly db: DatabaseSync;
|
|
20
|
+
constructor(path: string, key: Buffer);
|
|
21
|
+
close(): void;
|
|
22
|
+
putSecret(name: string, value: Uint8Array): void;
|
|
23
|
+
putSecretOnce(name: string, value: Uint8Array): void;
|
|
24
|
+
getSecret(name: string): Buffer | undefined;
|
|
25
|
+
beginOperation(id: string, payloadHash: string): void;
|
|
26
|
+
getOperation(id: string): StoredOperation | undefined;
|
|
27
|
+
transitionOperation(id: string, next: OperationStatus, txDigest?: string): void;
|
|
28
|
+
reserveNote(noteCommitment: string, operationId: string): void;
|
|
29
|
+
claimOnce(scope: string, value: string, result?: string): boolean;
|
|
30
|
+
prepareMessage(message: StoredMessage): void;
|
|
31
|
+
attachMessageBlob(id: string, ciphertextHash: string, blobId: string): void;
|
|
32
|
+
getMessage(id: string): StoredMessage | undefined;
|
|
33
|
+
putNote(commitment: string, role: "input" | "payment" | "change", operationId?: string, leafIndex?: bigint, nullifier?: string): void;
|
|
34
|
+
markNoteSpent(nullifier: string): void;
|
|
35
|
+
hasUnspentNote(commitment: string): boolean;
|
|
36
|
+
}
|