@supercuts-walrus/backend 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +249 -2
  3. package/dist/audit.d.ts +52 -0
  4. package/dist/audit.js +157 -0
  5. package/dist/backend.d.ts +27 -0
  6. package/dist/backend.js +62 -0
  7. package/dist/buyer-setup.d.ts +6 -0
  8. package/dist/buyer-setup.js +241 -0
  9. package/dist/capability.d.ts +32 -0
  10. package/dist/capability.js +87 -0
  11. package/dist/claude.d.ts +44 -0
  12. package/dist/claude.js +98 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +29 -0
  15. package/dist/config.d.ts +20 -0
  16. package/dist/config.js +58 -0
  17. package/dist/dashboard.d.ts +5 -0
  18. package/dist/dashboard.js +21 -0
  19. package/dist/deal-adapter.d.ts +3 -0
  20. package/dist/deal-adapter.js +39 -0
  21. package/dist/deal-engine.d.ts +62 -0
  22. package/dist/deal-engine.js +211 -0
  23. package/dist/deal-wire.d.ts +16 -0
  24. package/dist/deal-wire.js +65 -0
  25. package/dist/deal.d.ts +79 -0
  26. package/dist/deal.js +228 -0
  27. package/dist/demo.d.ts +2 -0
  28. package/dist/demo.js +449 -0
  29. package/dist/errors.d.ts +6 -0
  30. package/dist/errors.js +13 -0
  31. package/dist/events.d.ts +27 -0
  32. package/dist/events.js +68 -0
  33. package/dist/facilitator.d.ts +21 -0
  34. package/dist/facilitator.js +173 -0
  35. package/dist/finality.d.ts +2 -0
  36. package/dist/finality.js +27 -0
  37. package/dist/hosted-exit-cli.d.ts +2 -0
  38. package/dist/hosted-exit-cli.js +37 -0
  39. package/dist/hosted-exit.d.ts +24 -0
  40. package/dist/hosted-exit.js +70 -0
  41. package/dist/hosted-seller-import.d.ts +2 -0
  42. package/dist/hosted-seller-import.js +70 -0
  43. package/dist/hosted-seller-repository.d.ts +31 -0
  44. package/dist/hosted-seller-repository.js +214 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.js +37 -0
  47. package/dist/indexer.d.ts +63 -0
  48. package/dist/indexer.js +60 -0
  49. package/dist/live-deal-ports.d.ts +41 -0
  50. package/dist/live-deal-ports.js +212 -0
  51. package/dist/live-index.d.ts +28 -0
  52. package/dist/live-index.js +149 -0
  53. package/dist/live-runtime.d.ts +82 -0
  54. package/dist/live-runtime.js +570 -0
  55. package/dist/live-seller-transport.d.ts +30 -0
  56. package/dist/live-seller-transport.js +169 -0
  57. package/dist/live-seller-verifier.d.ts +11 -0
  58. package/dist/live-seller-verifier.js +65 -0
  59. package/dist/mandate.d.ts +34 -0
  60. package/dist/mandate.js +178 -0
  61. package/dist/mcp-bootstrap.d.ts +2 -0
  62. package/dist/mcp-bootstrap.js +39 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +187 -0
  65. package/dist/memory-worker.d.ts +2 -0
  66. package/dist/memory-worker.js +38 -0
  67. package/dist/memory.d.ts +15 -0
  68. package/dist/memory.js +17 -0
  69. package/dist/memwal.d.ts +35 -0
  70. package/dist/memwal.js +83 -0
  71. package/dist/negotiation.d.ts +23 -0
  72. package/dist/negotiation.js +58 -0
  73. package/dist/notes.d.ts +11 -0
  74. package/dist/notes.js +26 -0
  75. package/dist/onboarding-doctor.d.ts +1 -0
  76. package/dist/onboarding-doctor.js +76 -0
  77. package/dist/phase3.d.ts +100 -0
  78. package/dist/phase3.js +1144 -0
  79. package/dist/procurement.d.ts +8 -0
  80. package/dist/procurement.js +19 -0
  81. package/dist/protocol-engine.d.ts +121 -0
  82. package/dist/protocol-engine.js +149 -0
  83. package/dist/prover.d.ts +28 -0
  84. package/dist/prover.js +70 -0
  85. package/dist/relayer.d.ts +32 -0
  86. package/dist/relayer.js +43 -0
  87. package/dist/remote-buyer.d.ts +67 -0
  88. package/dist/remote-buyer.js +231 -0
  89. package/dist/resource-server.d.ts +2 -0
  90. package/dist/resource-server.js +24 -0
  91. package/dist/resource.d.ts +22 -0
  92. package/dist/resource.js +55 -0
  93. package/dist/seam.d.ts +2 -0
  94. package/dist/seam.js +117 -0
  95. package/dist/seller-catalog-generate.d.ts +2 -0
  96. package/dist/seller-catalog-generate.js +68 -0
  97. package/dist/seller-database.d.ts +41 -0
  98. package/dist/seller-database.js +80 -0
  99. package/dist/seller-directory-cli.d.ts +2 -0
  100. package/dist/seller-directory-cli.js +24 -0
  101. package/dist/seller-directory.d.ts +5 -0
  102. package/dist/seller-directory.js +50 -0
  103. package/dist/seller-live-cli.d.ts +2 -0
  104. package/dist/seller-live-cli.js +53 -0
  105. package/dist/seller-provision.d.ts +2 -0
  106. package/dist/seller-provision.js +84 -0
  107. package/dist/seller-register-cli.d.ts +2 -0
  108. package/dist/seller-register-cli.js +40 -0
  109. package/dist/seller-runtime.d.ts +60 -0
  110. package/dist/seller-runtime.js +96 -0
  111. package/dist/seller-service.d.ts +34 -0
  112. package/dist/seller-service.js +416 -0
  113. package/dist/seller-state.d.ts +19 -0
  114. package/dist/seller-state.js +60 -0
  115. package/dist/seller-stdio.d.ts +6 -0
  116. package/dist/seller-stdio.js +29 -0
  117. package/dist/seller-v2-provision.d.ts +2 -0
  118. package/dist/seller-v2-provision.js +71 -0
  119. package/dist/seller-watch.d.ts +2 -0
  120. package/dist/seller-watch.js +45 -0
  121. package/dist/settlement.d.ts +95 -0
  122. package/dist/settlement.js +141 -0
  123. package/dist/store.d.ts +36 -0
  124. package/dist/store.js +193 -0
  125. package/dist/sui.d.ts +49 -0
  126. package/dist/sui.js +77 -0
  127. package/dist/tatum.d.ts +17 -0
  128. package/dist/tatum.js +83 -0
  129. package/dist/transcript.d.ts +31 -0
  130. package/dist/transcript.js +52 -0
  131. package/dist/v2-exit-relayer.d.ts +34 -0
  132. package/dist/v2-exit-relayer.js +102 -0
  133. package/dist/v2-exit-worker.d.ts +2 -0
  134. package/dist/v2-exit-worker.js +97 -0
  135. package/dist/v2-exits.d.ts +30 -0
  136. package/dist/v2-exits.js +78 -0
  137. package/dist/v2-prefund.d.ts +19 -0
  138. package/dist/v2-prefund.js +43 -0
  139. package/dist/v2-relayer-cli.d.ts +2 -0
  140. package/dist/v2-relayer-cli.js +39 -0
  141. package/dist/wallet.d.ts +32 -0
  142. package/dist/wallet.js +90 -0
  143. package/dist/workflow-memory.d.ts +22 -0
  144. package/dist/workflow-memory.js +49 -0
  145. package/dist/x402-resource-service.d.ts +34 -0
  146. package/dist/x402-resource-service.js +154 -0
  147. package/dist/x402.d.ts +46 -0
  148. package/dist/x402.js +136 -0
  149. package/package.json +60 -3
@@ -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
+ }
@@ -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
+ }
package/dist/store.js ADDED
@@ -0,0 +1,193 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { fail } from "./errors.js";
6
+ const statuses = new Set(["draft", "reserved", "proof_persisted", "broadcast", "finalized", "confirmed", "delivered", "unresolved"]);
7
+ function encrypt(key, value, aad) {
8
+ const nonce = randomBytes(12);
9
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
10
+ cipher.setAAD(Buffer.from(aad));
11
+ return { nonce, value: Buffer.concat([cipher.update(value), cipher.final()]), tag: cipher.getAuthTag() };
12
+ }
13
+ function decrypt(key, encrypted, aad) {
14
+ try {
15
+ const cipher = createDecipheriv("aes-256-gcm", key, encrypted.nonce);
16
+ cipher.setAAD(Buffer.from(aad));
17
+ cipher.setAuthTag(encrypted.tag);
18
+ return Buffer.concat([cipher.update(encrypted.value), cipher.final()]);
19
+ }
20
+ catch (error) {
21
+ return fail("STATE_DECRYPT_FAILED", `could not decrypt ${aad}`, error);
22
+ }
23
+ }
24
+ export class RoleStore {
25
+ db;
26
+ #key;
27
+ constructor(path, key) {
28
+ if (key.length !== 32)
29
+ fail("STATE_KEY_INVALID", "role-store key must contain exactly 32 bytes");
30
+ this.#key = Buffer.from(key);
31
+ const directory = dirname(path);
32
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
33
+ if ((statSync(directory).mode & 0o077) !== 0)
34
+ fail("STATE_DIRECTORY_PERMISSIONS", `${directory} must not be group/world accessible`);
35
+ if (existsSync(path) && (statSync(path).mode & 0o077) !== 0)
36
+ fail("STATE_PERMISSIONS", `${path} must not be group/world accessible`);
37
+ this.db = new DatabaseSync(path);
38
+ // SQLite may create the main database before its first transaction. Ensure
39
+ // the database and any persistent WAL sidecars cannot expose encrypted
40
+ // ciphertext or public operation metadata to another local account.
41
+ for (const candidate of [path, `${path}-wal`, `${path}-shm`]) {
42
+ if (existsSync(candidate))
43
+ chmodSync(candidate, 0o600);
44
+ }
45
+ this.db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
46
+ this.db.exec(`
47
+ CREATE TABLE IF NOT EXISTS secrets (name TEXT PRIMARY KEY, nonce BLOB NOT NULL, tag BLOB NOT NULL, value BLOB NOT NULL);
48
+ CREATE TABLE IF NOT EXISTS operations (id TEXT PRIMARY KEY, status TEXT NOT NULL, payload_hash TEXT NOT NULL, tx_digest TEXT, updated_at INTEGER NOT NULL);
49
+ CREATE TABLE IF NOT EXISTS reservations (note_commitment TEXT PRIMARY KEY, operation_id TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL);
50
+ CREATE TABLE IF NOT EXISTS replay_ledger (scope TEXT NOT NULL, value TEXT NOT NULL, result TEXT, PRIMARY KEY(scope, value));
51
+ CREATE TABLE IF NOT EXISTS checkpoints (stream TEXT PRIMARY KEY, checkpoint TEXT NOT NULL, cursor BLOB, root TEXT NOT NULL, leaf_index TEXT NOT NULL);
52
+ CREATE TABLE IF NOT EXISTS messages (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, sequence INTEGER NOT NULL, ciphertext_hash TEXT NOT NULL, blob_id TEXT, UNIQUE(session_id, sequence));
53
+ CREATE TABLE IF NOT EXISTS deliveries (request_id TEXT PRIMARY KEY, payment_key TEXT NOT NULL, response BLOB NOT NULL, finalized INTEGER NOT NULL);
54
+ CREATE UNIQUE INDEX IF NOT EXISTS deliveries_payment_key ON deliveries(payment_key);
55
+ CREATE TABLE IF NOT EXISTS x402_challenges (request_id TEXT PRIMARY KEY, nonce TEXT NOT NULL, requirements_hash TEXT NOT NULL, created_at INTEGER NOT NULL);
56
+ CREATE TABLE IF NOT EXISTS delivery_jobs (request_id TEXT PRIMARY KEY, status TEXT NOT NULL, lease_until INTEGER NOT NULL DEFAULT 0);
57
+ CREATE TABLE IF NOT EXISTS notes (commitment TEXT PRIMARY KEY, operation_id TEXT, role TEXT NOT NULL, leaf_index TEXT, nullifier TEXT, spent INTEGER NOT NULL DEFAULT 0);
58
+ CREATE TABLE IF NOT EXISTS memory_jobs (operation_id TEXT PRIMARY KEY, kind TEXT NOT NULL, fact_hash TEXT NOT NULL, blob_id TEXT, status TEXT NOT NULL, created_at INTEGER NOT NULL);
59
+ CREATE TABLE IF NOT EXISTS memory_authority (blob_id TEXT PRIMARY KEY, fact_hash TEXT NOT NULL, provenance TEXT NOT NULL, evidence_hash TEXT, created_at INTEGER NOT NULL);
60
+ CREATE TABLE IF NOT EXISTS memory_inventory (blob_id TEXT PRIMARY KEY, memory_id TEXT NOT NULL, namespace TEXT NOT NULL, status TEXT NOT NULL, expires_at TEXT, deleted INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL);
61
+ CREATE TABLE IF NOT EXISTS memory_inventory_cursor (namespace TEXT PRIMARY KEY, cursor TEXT, updated_at INTEGER NOT NULL);
62
+ CREATE TABLE IF NOT EXISTS deal_operations (operation_id TEXT PRIMARY KEY, deal_id TEXT NOT NULL UNIQUE, input_hash TEXT NOT NULL, state TEXT NOT NULL, rail TEXT, plan_hash TEXT, payment_operation_id TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
63
+ CREATE TABLE IF NOT EXISTS deal_steps (operation_id TEXT NOT NULL, step TEXT NOT NULL, input_hash TEXT NOT NULL, state TEXT NOT NULL, result_hash TEXT, updated_at INTEGER NOT NULL, PRIMARY KEY(operation_id,step), FOREIGN KEY(operation_id) REFERENCES deal_operations(operation_id));
64
+ CREATE TABLE IF NOT EXISTS deal_blobs (operation_id TEXT NOT NULL, kind TEXT NOT NULL, ciphertext_hash TEXT NOT NULL, blob_id TEXT, status TEXT NOT NULL, PRIMARY KEY(operation_id,kind), FOREIGN KEY(operation_id) REFERENCES deal_operations(operation_id));
65
+ CREATE TABLE IF NOT EXISTS deal_memory_outbox (operation_id TEXT NOT NULL, event_kind TEXT NOT NULL, payload_hash TEXT NOT NULL, blob_id TEXT, status TEXT NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY(operation_id,event_kind), FOREIGN KEY(operation_id) REFERENCES deal_operations(operation_id));
66
+ CREATE TABLE IF NOT EXISTS deal_timings (operation_id TEXT NOT NULL, stage TEXT NOT NULL, duration_ms INTEGER NOT NULL, PRIMARY KEY(operation_id,stage), FOREIGN KEY(operation_id) REFERENCES deal_operations(operation_id));
67
+ CREATE TABLE IF NOT EXISTS settlement_plans (plan_hash TEXT PRIMARY KEY, operation_id TEXT NOT NULL UNIQUE, plan_json TEXT NOT NULL, status TEXT NOT NULL, expires_at INTEGER NOT NULL, consumed_at INTEGER, created_at INTEGER NOT NULL);
68
+ CREATE TABLE IF NOT EXISTS settlement_execution_bindings (operation_id TEXT PRIMARY KEY, plan_hash TEXT NOT NULL UNIQUE, binding_hash TEXT NOT NULL, binding_json TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(plan_hash) REFERENCES settlement_plans(plan_hash));
69
+ CREATE TABLE IF NOT EXISTS settlement_agreement_bindings (session_id TEXT PRIMARY KEY, plan_hash TEXT NOT NULL UNIQUE, binding_hash TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(plan_hash) REFERENCES settlement_plans(plan_hash));
70
+ CREATE TABLE IF NOT EXISTS settlement_decision_traces (plan_hash TEXT PRIMARY KEY, mandate_hash TEXT NOT NULL, manifest_hash TEXT NOT NULL, risk_level TEXT NOT NULL, reason_codes TEXT NOT NULL, citations TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(plan_hash) REFERENCES settlement_plans(plan_hash));
71
+ `);
72
+ const memoryColumns = this.db.prepare("PRAGMA table_info(memory_jobs)").all();
73
+ if (!memoryColumns.some((column) => column.name === "fact_hash"))
74
+ this.db.exec("ALTER TABLE memory_jobs ADD COLUMN fact_hash TEXT");
75
+ if (!memoryColumns.some((column) => column.name === "claimed_at"))
76
+ this.db.exec("ALTER TABLE memory_jobs ADD COLUMN claimed_at INTEGER");
77
+ for (const candidate of [path, `${path}-wal`, `${path}-shm`])
78
+ if (existsSync(candidate))
79
+ chmodSync(candidate, 0o600);
80
+ }
81
+ close() { this.db.close(); }
82
+ putSecret(name, value) {
83
+ const encrypted = encrypt(this.#key, Buffer.from(value), `secret:${name}`);
84
+ this.db.prepare("INSERT OR REPLACE INTO secrets(name, nonce, tag, value) VALUES (?, ?, ?, ?)")
85
+ .run(name, encrypted.nonce, encrypted.tag, encrypted.value);
86
+ }
87
+ putSecretOnce(name, value) {
88
+ const existing = this.getSecret(name);
89
+ if (existing) {
90
+ if (!existing.equals(Buffer.from(value)))
91
+ fail("SECRET_ID_CONFLICT", `secret ${name} already contains different bytes`);
92
+ return;
93
+ }
94
+ this.putSecret(name, value);
95
+ }
96
+ getSecret(name) {
97
+ const row = this.db.prepare("SELECT nonce, tag, value FROM secrets WHERE name = ?").get(name);
98
+ return row ? decrypt(this.#key, row, `secret:${name}`) : undefined;
99
+ }
100
+ beginOperation(id, payloadHash) {
101
+ const existing = this.getOperation(id);
102
+ if (existing) {
103
+ if (existing.payloadHash !== payloadHash)
104
+ fail("OPERATION_ID_CONFLICT", `operation ${id} already has a different payload`);
105
+ return;
106
+ }
107
+ this.db.prepare("INSERT INTO operations(id, status, payload_hash, updated_at) VALUES (?, 'draft', ?, ?)").run(id, payloadHash, Date.now());
108
+ }
109
+ getOperation(id) {
110
+ const row = this.db.prepare("SELECT id, status, payload_hash, tx_digest, updated_at FROM operations WHERE id = ?").get(id);
111
+ return row ? { id: row.id, status: row.status, payloadHash: row.payload_hash, ...(row.tx_digest ? { txDigest: row.tx_digest } : {}), updatedAt: row.updated_at } : undefined;
112
+ }
113
+ transitionOperation(id, next, txDigest) {
114
+ if (!statuses.has(next))
115
+ fail("OPERATION_STATUS_INVALID", `unknown operation status ${next}`);
116
+ const row = this.db.prepare("SELECT status, tx_digest FROM operations WHERE id = ?").get(id);
117
+ const current = row?.status ?? fail("OPERATION_NOT_FOUND", `operation ${id} does not exist`);
118
+ if (row?.tx_digest && txDigest && row.tx_digest !== txDigest)
119
+ fail("OPERATION_DIGEST_CONFLICT", "operation cannot transition with a different transaction digest");
120
+ if (current === next)
121
+ return;
122
+ const allowed = {
123
+ draft: ["reserved", "unresolved"], reserved: ["proof_persisted", "unresolved"],
124
+ proof_persisted: ["broadcast", "unresolved"], broadcast: ["finalized", "unresolved"],
125
+ finalized: ["confirmed", "unresolved"], confirmed: ["delivered", "unresolved"], delivered: [],
126
+ unresolved: ["broadcast", "finalized", "confirmed"],
127
+ };
128
+ if (!allowed[current].includes(next))
129
+ fail("OPERATION_TRANSITION_INVALID", `${current} cannot transition to ${next}`);
130
+ this.db.prepare("UPDATE operations SET status = ?, tx_digest = COALESCE(?, tx_digest), updated_at = ? WHERE id = ?")
131
+ .run(next, txDigest ?? null, Date.now(), id);
132
+ }
133
+ reserveNote(noteCommitment, operationId) {
134
+ this.db.exec("BEGIN IMMEDIATE");
135
+ try {
136
+ const existing = this.db.prepare("SELECT operation_id FROM reservations WHERE note_commitment = ?").get(noteCommitment);
137
+ if (existing && existing.operation_id !== operationId)
138
+ fail("NOTE_RESERVED", "note is already reserved by another operation");
139
+ this.db.prepare("INSERT OR IGNORE INTO reservations(note_commitment, operation_id, created_at) VALUES (?, ?, ?)").run(noteCommitment, operationId, Date.now());
140
+ if (this.getOperation(operationId)?.status === "draft")
141
+ this.transitionOperation(operationId, "reserved");
142
+ this.db.exec("COMMIT");
143
+ }
144
+ catch (error) {
145
+ this.db.exec("ROLLBACK");
146
+ throw error;
147
+ }
148
+ }
149
+ claimOnce(scope, value, result) {
150
+ const info = this.db.prepare("INSERT OR IGNORE INTO replay_ledger(scope, value, result) VALUES (?, ?, ?)").run(scope, value, result ?? null);
151
+ return info.changes === 1;
152
+ }
153
+ prepareMessage(message) {
154
+ const existing = this.getMessage(message.id);
155
+ if (existing) {
156
+ if (existing.sessionId !== message.sessionId || existing.sequence !== message.sequence || existing.ciphertextHash !== message.ciphertextHash) {
157
+ fail("MESSAGE_ID_CONFLICT", `message ${message.id} already identifies different ciphertext`);
158
+ }
159
+ return;
160
+ }
161
+ try {
162
+ this.db.prepare("INSERT INTO messages(id, session_id, sequence, ciphertext_hash, blob_id) VALUES (?, ?, ?, ?, NULL)")
163
+ .run(message.id, message.sessionId, message.sequence, message.ciphertextHash);
164
+ }
165
+ catch (error) {
166
+ fail("MESSAGE_SEQUENCE_CONFLICT", "session sequence is already occupied", error);
167
+ }
168
+ }
169
+ attachMessageBlob(id, ciphertextHash, blobId) {
170
+ const row = this.getMessage(id) ?? fail("MESSAGE_NOT_FOUND", `message ${id} was not prepared`);
171
+ if (row.ciphertextHash !== ciphertextHash)
172
+ fail("CIPHERTEXT_TAMPERED", "uploaded ciphertext differs from the durable message");
173
+ if (row.blobId && row.blobId !== blobId)
174
+ fail("MESSAGE_BLOB_CONFLICT", "message already resolved to another blob");
175
+ this.db.prepare("UPDATE messages SET blob_id = ? WHERE id = ?").run(blobId, id);
176
+ }
177
+ getMessage(id) {
178
+ const row = this.db.prepare("SELECT id, session_id, sequence, ciphertext_hash, blob_id FROM messages WHERE id = ?").get(id);
179
+ return row ? { id: row.id, sessionId: row.session_id, sequence: row.sequence, ciphertextHash: row.ciphertext_hash, ...(row.blob_id ? { blobId: row.blob_id } : {}) } : undefined;
180
+ }
181
+ putNote(commitment, role, operationId, leafIndex, nullifier) {
182
+ const existing = this.db.prepare("SELECT operation_id, role, leaf_index, nullifier FROM notes WHERE commitment = ?").get(commitment);
183
+ const values = { operationId: operationId ?? null, role, leafIndex: leafIndex?.toString() ?? null, nullifier: nullifier ?? null };
184
+ if (existing && (existing.operation_id !== values.operationId || existing.role !== role || (existing.leaf_index && values.leafIndex && existing.leaf_index !== values.leafIndex) || (existing.nullifier && values.nullifier && existing.nullifier !== values.nullifier)))
185
+ fail("NOTE_ID_CONFLICT", "note commitment is already associated with different recovery data");
186
+ this.db.prepare("INSERT INTO notes(commitment, operation_id, role, leaf_index, nullifier, spent) VALUES (?, ?, ?, ?, ?, 0) ON CONFLICT(commitment) DO UPDATE SET leaf_index=COALESCE(notes.leaf_index, excluded.leaf_index), nullifier=COALESCE(notes.nullifier, excluded.nullifier)")
187
+ .run(commitment, values.operationId, role, values.leafIndex, values.nullifier);
188
+ }
189
+ markNoteSpent(nullifier) { this.db.prepare("UPDATE notes SET spent = 1 WHERE nullifier = ?").run(nullifier); }
190
+ hasUnspentNote(commitment) {
191
+ return Boolean(this.db.prepare("SELECT 1 AS found FROM notes WHERE commitment = ? AND spent = 0").get(commitment));
192
+ }
193
+ }
package/dist/sui.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { SuiGrpcClient } from "@mysten/sui/grpc";
2
+ import { Transaction } from "@mysten/sui/transactions";
3
+ export interface TransferCall {
4
+ packageId: string;
5
+ poolId: string;
6
+ coinType: string;
7
+ poolModule?: "pool" | "pool_v2";
8
+ expiryMs: bigint;
9
+ newIndex: bigint;
10
+ dealCommitment: Uint8Array;
11
+ dealNullifier: Uint8Array;
12
+ inputNullifier: Uint8Array;
13
+ paymentCommitment: Uint8Array;
14
+ changeCommitment: Uint8Array;
15
+ newRoot: Uint8Array;
16
+ proof: Uint8Array;
17
+ }
18
+ export interface DepositCall {
19
+ packageId: string;
20
+ poolId: string;
21
+ coinType: string;
22
+ poolModule?: "pool" | "pool_v2";
23
+ amount: bigint;
24
+ newIndex: bigint;
25
+ noteCommitment: Uint8Array;
26
+ newRoot: Uint8Array;
27
+ proof: Uint8Array;
28
+ }
29
+ export interface WithdrawCall {
30
+ packageId: string;
31
+ poolId: string;
32
+ coinType: string;
33
+ amount: bigint;
34
+ recipient: string;
35
+ root: Uint8Array;
36
+ noteNullifier: Uint8Array;
37
+ proof: Uint8Array;
38
+ }
39
+ export interface WithdrawV2Call extends WithdrawCall {
40
+ exactExit: boolean;
41
+ changeCommitment: Uint8Array;
42
+ newRoot: Uint8Array;
43
+ newIndex: bigint;
44
+ }
45
+ export declare function buildDeposit(call: DepositCall): Transaction;
46
+ export declare function buildPrivateTransfer(call: TransferCall): Transaction;
47
+ export declare function buildWithdraw(call: WithdrawCall): Transaction;
48
+ export declare function buildWithdrawV2(call: WithdrawV2Call): Transaction;
49
+ export declare function writeClient(endpoint: string): SuiGrpcClient;