@supercuts-walrus/backend 0.1.2 → 0.1.3

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/phase3.js CHANGED
@@ -8,9 +8,10 @@ import { DealJournal } from "./deal.js";
8
8
  import { LiveRuntime } from "./live-runtime.js";
9
9
  import { V2ExitQueue } from "./v2-exits.js";
10
10
  import { validateCapabilityManifest } from "./capability.js";
11
- import { ManualMemory } from "./memwal.js";
11
+ import { ManualMemory, openMemoryText, withMemoryDefaults } from "./memwal.js";
12
12
  import { WorkflowMemory } from "./workflow-memory.js";
13
- import { canonicalJson, createSettlementPlan, settlementMandateHash, settlementPlanHash, validateSettlementMemory, validateSettlementPlan } from "./settlement.js";
13
+ import { buildScorecard } from "./memory-scorecard.js";
14
+ import { canonicalJson, createSettlementPlan, explainMemoryInfluence, isRelevantMemory, settlementMandateHash, settlementPlanHash, validateSettlementMemory, validateSettlementPlan } from "./settlement.js";
14
15
  const PACKAGE = "0xf887433ddfd1068f6df3020cd757939b99ceb418f0d8b9924092b01c7aee846e";
15
16
  const POOL = "0x873e37b4745316f21a2780893f5fc36f87da559b08923fb4dad6237cd2e95d98";
16
17
  const V2_PACKAGE = "0x32d104c1ab422d497df4ae3a6861258694949c86428f886ca338e07bd055cf76";
@@ -56,6 +57,7 @@ function matchesPlannedEndpoint(planned, actual, requestId) {
56
57
  return false;
57
58
  }
58
59
  }
60
+ const memoryQuery = (manifest) => `${manifest.capabilityId} ${manifest.capability} ${manifest.sellerId}`;
59
61
  function record(store, scope, id, value) {
60
62
  store.db.prepare("INSERT INTO app_records(scope,id,value,updated_at) VALUES(?,?,?,?) ON CONFLICT(scope,id) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at")
61
63
  .run(scope, id, json(value), Date.now());
@@ -118,11 +120,12 @@ export function initialize(home, args) {
118
120
  if (liveCredentials?.memory) {
119
121
  let memoryConfig;
120
122
  try {
121
- memoryConfig = JSON.parse(liveCredentials.memory.toString());
123
+ memoryConfig = withMemoryDefaults(JSON.parse(liveCredentials.memory.toString()), role);
122
124
  }
123
125
  catch (error) {
124
- return fail("LIVE_MEMORY_CONFIG_INVALID", "memory config is not valid JSON", error);
126
+ return fail("LIVE_MEMORY_CONFIG_INVALID", `memory config is invalid: ${error instanceof TypeError ? error.message : "not valid JSON"}`, error);
125
127
  }
128
+ liveCredentials.memory = Buffer.from(JSON.stringify(memoryConfig));
126
129
  let embedding;
127
130
  try {
128
131
  embedding = new URL(memoryConfig.embeddingApiBase);
@@ -403,9 +406,14 @@ export class Phase3App {
403
406
  return { blobId: String(item.blobId), distance: 0, legacy: true };
404
407
  });
405
408
  }
406
- authenticatedMemory(blobId, memory) {
409
+ /** Trusted provenance needs local authority or this owner's memory-key signature (portable across machines). */
410
+ authenticatedMemory(blobId, memory, signedByOwner = false) {
407
411
  if (memory.provenance !== "owner_policy" && memory.provenance !== "verified_outcome")
408
412
  return memory;
413
+ if (signedByOwner && (memory.provenance !== "verified_outcome" || memory.evidenceHash)) {
414
+ this.registerMemoryAuthority(blobId, memory);
415
+ return memory;
416
+ }
409
417
  const authority = this.store.db.prepare("SELECT fact_hash,provenance,evidence_hash FROM memory_authority WHERE blob_id=?").get(blobId);
410
418
  if (authority?.fact_hash === hash(canonicalJson(memory)) && authority.provenance === memory.provenance && (memory.provenance !== "verified_outcome" || authority.evidence_hash === memory.evidenceHash))
411
419
  return memory;
@@ -456,31 +464,126 @@ export class Phase3App {
456
464
  }
457
465
  return existing;
458
466
  }
459
- let recalled = this.recalledMockMemories();
460
- if (!this.profile.mock && this.store.getSecret("live:memory-config")) {
461
- const encoded = this.store.getSecret("live:memory-config");
462
- let config;
467
+ const manifests = [primary, ...alternatives], recalled = await this.recallSettlementMemories(manifests.map(memoryQuery));
468
+ const input = { operationId, mandate, manifests, ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 };
469
+ const plan = this.persistPlan(createSettlementPlan(input));
470
+ if (!get(this.store, "memory-influence", plan.planHash))
471
+ record(this.store, "memory-influence", plan.planHash, explainMemoryInfluence(input, plan));
472
+ return plan;
473
+ }
474
+ /** Recall settlement memories for several scoped queries; plaintext stays in this process. */
475
+ async recallSettlementMemories(queries, limit = 20) {
476
+ const local = this.localVerifiedOutcomes(), merge = (recalled) => [...local, ...recalled.filter((item) => !local.some((other) => other.blobId === item.blobId))]; // the local authoritative copy wins
477
+ const encoded = this.profile.mock ? undefined : this.store.getSecret("live:memory-config");
478
+ if (!encoded)
479
+ return merge(this.recalledMockMemories());
480
+ let config;
481
+ try {
482
+ config = JSON.parse(encoded.toString());
483
+ }
484
+ catch (error) {
485
+ return fail("LIVE_MEMORY_CONFIG_INVALID", "encrypted memory config is invalid", error);
486
+ }
487
+ const manual = new ManualMemory(this.store, config), byBlob = new Map();
488
+ try {
489
+ for (const query of [...new Set(queries)])
490
+ for (const item of await manual.recall(query, limit)) {
491
+ const prior = byBlob.get(item.blobId);
492
+ if (prior && prior.distance <= item.distance)
493
+ continue;
494
+ let memory;
495
+ try {
496
+ const opened = openMemoryText(item.text);
497
+ validateSettlementMemory(opened.memory);
498
+ memory = this.authenticatedMemory(item.blobId, opened.memory, Boolean(opened.signedBy && opened.signedBy === manual.signerAddress));
499
+ }
500
+ catch { /* malformed or legacy memory remains advisory */ }
501
+ byBlob.set(item.blobId, { blobId: item.blobId, distance: item.distance, ...(memory ? { memory } : { legacy: true }) });
502
+ }
503
+ }
504
+ finally {
505
+ manual.destroy();
506
+ }
507
+ return merge([...byBlob.values()]);
508
+ }
509
+ /**
510
+ * Verified outcomes this runtime wrote itself, including ones still queued for Walrus. They are authoritative
511
+ * for spend and reliability on this machine; semantic recall alone can miss or lag them.
512
+ */
513
+ localVerifiedOutcomes() {
514
+ const rows = this.store.db.prepare("SELECT operation_id,blob_id FROM memory_jobs WHERE operation_id LIKE 'verified:deal:%' OR operation_id LIKE 'verified:deal-failure:%' ORDER BY created_at").all();
515
+ return rows.flatMap((row) => {
516
+ const payload = this.store.getSecret(`memory-outbox:${row.operation_id}`);
517
+ if (!payload)
518
+ return [];
463
519
  try {
464
- config = JSON.parse(encoded.toString());
520
+ const memory = JSON.parse(payload.toString());
521
+ validateSettlementMemory(memory, "verified_outcome");
522
+ return [{ blobId: row.blob_id ?? `local:${row.operation_id}`, distance: 0, memory }];
465
523
  }
466
- catch (error) {
467
- return fail("LIVE_MEMORY_CONFIG_INVALID", "encrypted memory config is invalid", error);
524
+ catch {
525
+ return [];
468
526
  }
469
- const manual = new ManualMemory(this.store, config);
527
+ });
528
+ }
529
+ /**
530
+ * Orders candidate sellers by what Walrus Memory has verified about them, instead of by price alone.
531
+ * Sellers outside the owner mandate are dropped first so memory can never widen authority.
532
+ */
533
+ async rankDealCandidates(operationId, candidates, mandate, request = {}) {
534
+ const permitted = candidates.filter((item) => mandate.allowedCounterparties.includes(item.sellerId) && (!mandate.allowedCapabilityIds || mandate.allowedCapabilityIds.includes(item.capabilityId)) && (!mandate.allowedCapabilityClasses || mandate.allowedCapabilityClasses.includes(item.capability)));
535
+ const allowed = permitted.length ? permitted : candidates;
536
+ const recalled = await this.recallSettlementMemories(allowed.map(memoryQuery)), now = Date.now(), maxDistance = this.profile.policy.maxMemoryDistance ?? 0.35;
537
+ const relevant = recalled.filter((item) => isRelevantMemory(item, maxDistance));
538
+ // Only rank sellers that the deal could actually plan and pay: budget, rail, privacy, and price bounds.
539
+ const payable = allowed.filter((manifest) => {
470
540
  try {
471
- recalled = (await manual.recall(`${primary.capabilityId} ${primary.capability} ${primary.sellerId}`, 20)).map((item) => { let memory; try {
472
- const value = JSON.parse(item.text);
473
- validateSettlementMemory(value);
474
- memory = this.authenticatedMemory(item.blobId, value);
475
- }
476
- catch { /* malformed or legacy memory remains advisory */ } return { blobId: item.blobId, distance: item.distance, ...(memory ? { memory } : { legacy: true }) }; });
541
+ const plan = createSettlementPlan({ operationId: `rank:${operationId}`, mandate, manifests: [manifest], requestedAmount: manifest.priceBand.minimum, recalled, maxMemoryDistance: maxDistance, now, ...(request.requestedRail ? { requestedRail: request.requestedRail } : {}), ...(request.privacyHint ? { privacyHint: request.privacyHint } : {}) });
542
+ if (request.budgetMaximum !== undefined && BigInt(plan.maximumAmount) > BigInt(request.budgetMaximum))
543
+ return false;
544
+ return request.requestedRail ? plan.rail === request.requestedRail : mandate.version !== "supercuts.mandate.v2" || !manifest.supportedRails.includes("shielded") || plan.rail === "shielded";
477
545
  }
478
- finally {
479
- manual.destroy();
546
+ catch {
547
+ return false;
480
548
  }
481
- }
482
- const plan = createSettlementPlan({ operationId, mandate, manifests: [primary, ...alternatives], ...(typeof args["requested-amount"] === "string" ? { requestedAmount: args["requested-amount"] } : {}), ...(args["privacy-hint"] === "shielded" ? { privacyHint: "shielded" } : {}), ...(args["requested-rail"] === "x402" || args["requested-rail"] === "shielded" ? { requestedRail: args["requested-rail"] } : {}), recalled, maxMemoryDistance: this.profile.policy.maxMemoryDistance ?? 0.35 });
483
- return this.persistPlan(plan);
549
+ });
550
+ const pool = payable.length ? payable : allowed;
551
+ const scored = pool.map((manifest) => ({ manifest, card: buildScorecard(relevant, { counterparty: manifest.sellerId, capabilityClass: manifest.capability }, now) }));
552
+ const review = (card) => Number(card.lastOutcome === "unresolved" || card.observations > 0 && card.reliability < 0.5);
553
+ scored.sort((a, b) => review(a.card) - review(b.card) || b.card.reliability - a.card.reliability || (BigInt(a.manifest.priceBand.minimum) < BigInt(b.manifest.priceBand.minimum) ? -1 : BigInt(a.manifest.priceBand.minimum) > BigInt(b.manifest.priceBand.minimum) ? 1 : 0) || a.manifest.capabilityId.localeCompare(b.manifest.capabilityId));
554
+ const result = { ordered: scored.map((item) => item.manifest), scorecards: scored.map((item) => ({ ...item.card, capabilityId: item.manifest.capabilityId, priceMinimum: item.manifest.priceBand.minimum })) };
555
+ record(this.store, "deal-ranking", operationId, { candidates: pool, scorecards: result.scorecards, chosen: result.ordered[0]?.capabilityId, createdAt: now });
556
+ return result;
557
+ }
558
+ /** Stores the counterfactual (same mandate, no memory, every candidate) next to the binding deal plan. */
559
+ recordDealMemoryInfluence(dealOperationId, plan, mandate, request = {}) {
560
+ const ranking = get(this.store, "deal-ranking", dealOperationId);
561
+ if (!ranking)
562
+ return undefined;
563
+ const existing = get(this.store, "memory-influence", `deal:${dealOperationId}`);
564
+ if (existing)
565
+ return existing;
566
+ const influence = explainMemoryInfluence({ operationId: plan.operationId, mandate, manifests: ranking.candidates, requestedAmount: plan.openingAmount, ...request }, plan);
567
+ const cited = [...new Set([...influence.citedBlobIds, ...ranking.scorecards.flatMap((item) => item.citedBlobIds)])].sort();
568
+ const value = { ...influence, citedBlobIds: cited };
569
+ record(this.store, "memory-influence", `deal:${dealOperationId}`, value);
570
+ return value;
571
+ }
572
+ memoryInfluence(id) { return get(this.store, "memory-influence", id); }
573
+ /**
574
+ * Owner view of what Supercuts has learned: fresh scorecards recalled from Walrus Memory for every seller
575
+ * it has ranked, plus how often memory changed a payment decision. Counts and blob IDs only, never plaintext.
576
+ */
577
+ async memoryInsights() {
578
+ const rankings = all(this.store, "deal-ranking");
579
+ const manifests = [...new Map(rankings.flatMap((item) => item.candidates).map((item) => [item.capabilityId, item])).values()];
580
+ const recalled = manifests.length ? await this.recallSettlementMemories(manifests.map(memoryQuery)) : [], now = Date.now(), maxDistance = this.profile.policy.maxMemoryDistance ?? 0.35;
581
+ const relevant = recalled.filter((item) => isRelevantMemory(item, maxDistance));
582
+ const sellers = manifests.map((manifest) => { const card = buildScorecard(relevant, { counterparty: manifest.sellerId, capabilityClass: manifest.capability }, now); return { capabilityId: manifest.capabilityId, capabilityClass: manifest.capability, priceMinimum: manifest.priceBand.minimum, ...card }; }).sort((a, b) => b.reliability - a.reliability || a.capabilityId.localeCompare(b.capabilityId));
583
+ const influences = this.store.db.prepare("SELECT value FROM app_records WHERE scope='memory-influence' AND id LIKE 'deal:%'").all().map((row) => JSON.parse(row.value));
584
+ const jobs = this.store.db.prepare("SELECT status, COUNT(*) AS count FROM memory_jobs GROUP BY status").all();
585
+ const indexed = this.store.db.prepare("SELECT COUNT(*) AS count FROM memory_inventory WHERE deleted=0").get().count;
586
+ return { version: "supercuts.memory-insights.v1", memories: { byStatus: Object.fromEntries(jobs.map((row) => [row.status, row.count])), indexed, recalled: recalled.length }, decisions: { deals: influences.length, changedSeller: influences.filter((item) => item.changedSeller).length, changedRail: influences.filter((item) => item.changedRail).length, changedRisk: influences.filter((item) => item.changedRisk).length, changedMaximum: influences.filter((item) => item.changedMaximum).length }, sellers };
484
587
  }
485
588
  typedMemory(args, provenance) {
486
589
  const encoded = required(args, "memory");
@@ -545,7 +648,7 @@ export class Phase3App {
545
648
  manual.destroy();
546
649
  }
547
650
  }
548
- enqueueVerifiedOutcome(event, operationId, evidence, planHash, fallback) {
651
+ enqueueVerifiedOutcome(event, operationId, evidence, planHash, fallback, telemetry = {}) {
549
652
  const plan = planHash ? this.storedPlan(planHash) : undefined, jobId = `verified:${event}:${operationId}`, evidenceHash = hash(canonicalJson(evidence));
550
653
  const prior = this.store.getSecret(`memory-outbox:${jobId}`);
551
654
  if (prior) {
@@ -555,7 +658,7 @@ export class Phase3App {
555
658
  fail("MEMORY_OPERATION_CONFLICT", "verified outcome ID identifies different evidence or scope");
556
659
  return;
557
660
  }
558
- const memory = { version: "supercuts.settlement-memory.v1", kind: "outcome", provenance: "verified_outcome", scope: { ...(plan ? { capabilityId: plan.capabilityId, capabilityClass: plan.capabilityClass, counterparty: plan.sellerId, network: plan.network, asset: plan.asset } : { ...(fallback?.counterparty ? { counterparty: fallback.counterparty } : {}), ...(fallback?.asset ? { asset: fallback.asset } : {}), network: "sui:testnet" }) }, createdAt: Date.now(), evidenceHash, signal: { outcome: String(evidence.status ?? "") === "unresolved" ? "unresolved" : String(evidence.status ?? "") === "failed" ? "failed" : "succeeded" } };
661
+ const memory = { version: "supercuts.settlement-memory.v1", kind: "outcome", provenance: "verified_outcome", scope: { ...(plan ? { capabilityId: plan.capabilityId, capabilityClass: plan.capabilityClass, counterparty: plan.sellerId, network: plan.network, asset: plan.asset } : { ...(fallback?.counterparty ? { counterparty: fallback.counterparty } : {}), ...(fallback?.asset ? { asset: fallback.asset } : {}), network: "sui:testnet" }) }, createdAt: Date.now(), evidenceHash, signal: { outcome: String(evidence.status ?? "") === "unresolved" ? "unresolved" : String(evidence.status ?? "") === "failed" ? "failed" : "succeeded", ...telemetry } };
559
662
  validateSettlementMemory(memory, "verified_outcome");
560
663
  const factHash = hash(canonicalJson(memory));
561
664
  if (this.profile.mock) {
@@ -567,11 +670,21 @@ export class Phase3App {
567
670
  this.store.putSecretOnce(`memory-outbox:${jobId}`, Buffer.from(canonicalJson(memory)));
568
671
  this.store.db.prepare("INSERT OR IGNORE INTO memory_jobs(operation_id,kind,fact_hash,blob_id,status,created_at) VALUES(?,?,?,NULL,'pending',?)").run(jobId, memory.kind, factHash, Date.now());
569
672
  }
673
+ /** Queues a verified seller fault as a Walrus outcome memory so later discovery demotes that seller. */
674
+ recordVerifiedDealFailure(operationId, planHash, fault) {
675
+ if (this.profile.mock || this.profile.role !== "buyer")
676
+ fail("DEAL_LIVE_REQUIRED", "verified deal failures require a live buyer role");
677
+ if (this.store.getSecret(`memory-outbox:verified:deal-failure:${operationId}`))
678
+ return;
679
+ const plan = this.storedPlan(planHash);
680
+ this.enqueueVerifiedOutcome("deal-failure", operationId, { status: "failed", stage: fault.stage, code: fault.code, ...(fault.paymentDigest ? { paymentDigest: fault.paymentDigest } : {}) }, planHash, undefined, { rail: plan.rail, ...(fault.paymentDigest ? { amount: plan.openingAmount } : {}) });
681
+ }
570
682
  recordVerifiedDealOutcome(operationId, planHash, paymentDigest, artifactSha256) {
571
683
  if (this.profile.mock || this.profile.role !== "buyer")
572
684
  fail("DEAL_LIVE_REQUIRED", "verified deal outcome requires a live buyer role");
573
685
  const jobId = `verified:deal:${operationId}`;
574
- this.enqueueVerifiedOutcome("deal", operationId, { status: "succeeded", paymentDigest, artifactSha256 }, planHash);
686
+ const plan = this.storedPlan(planHash), timings = this.store.db.prepare("SELECT COALESCE(SUM(duration_ms),0) AS total FROM deal_timings WHERE operation_id=? AND stage IN ('negotiation','payment','delivery')").get(operationId);
687
+ this.enqueueVerifiedOutcome("deal", operationId, { status: "succeeded", paymentDigest, artifactSha256 }, planHash, undefined, { amount: plan.openingAmount, rail: plan.rail, ...(Number.isSafeInteger(timings.total) && timings.total > 0 ? { latencyMs: timings.total } : {}) });
575
688
  const payload = this.store.getSecret(`memory-outbox:${jobId}`) ?? fail("DEAL_MEMORY_INVALID", "verified outcome payload is missing");
576
689
  return { payloadHash: hash(payload), queued: true };
577
690
  }
@@ -645,7 +758,7 @@ export class Phase3App {
645
758
  const query = [memory.scope.capabilityId, memory.scope.counterparty, memory.signal.outcome].filter(Boolean).join(" ");
646
759
  const recalled = await manual.recall(query, 100);
647
760
  blobId = recalled.find((item) => { try {
648
- return canonicalJson(JSON.parse(item.text)) === canonicalJson(memory);
761
+ return canonicalJson(openMemoryText(item.text).memory) === canonicalJson(memory);
649
762
  }
650
763
  catch {
651
764
  return false;
@@ -945,9 +1058,9 @@ export class Phase3App {
945
1058
  if (command === "memory.recall") {
946
1059
  const recalled = await manual.recall(required(args, "query"));
947
1060
  const citations = recalled.filter((item) => item.distance <= (this.profile.policy.maxMemoryDistance ?? 0.35)).map((item) => { try {
948
- const memory = JSON.parse(item.text);
1061
+ const opened = openMemoryText(item.text), memory = opened.memory;
949
1062
  validateSettlementMemory(memory);
950
- const trusted = this.authenticatedMemory(item.blobId, memory);
1063
+ const trusted = this.authenticatedMemory(item.blobId, memory, Boolean(opened.signedBy && opened.signedBy === manual.signerAddress));
951
1064
  return { blobId: item.blobId, kind: trusted.kind, provenance: trusted.provenance, distance: item.distance };
952
1065
  }
953
1066
  catch {
package/dist/seam.js CHANGED
@@ -112,6 +112,7 @@ process.stdin.on("end", async () => {
112
112
  catch (error) {
113
113
  const shaped = errorOutput(error);
114
114
  const code = String(shaped.error.code ?? "INTERNAL_ERROR");
115
- return write(envelope(request.requestId, { ok: false, error: { ...shaped.error, retryable: ["DELIVERY_IN_PROGRESS", "MEMORY_JOB_PENDING", "TATUM_TIMEOUT"].includes(code) } }), 1);
115
+ const retryable = ["DELIVERY_IN_PROGRESS", "MEMORY_JOB_PENDING", "TATUM_TIMEOUT", "X402_FACILITATOR_UNAVAILABLE"].includes(code);
116
+ return write(envelope(request.requestId, { ok: false, error: { ...shaped.error, retryable, ...(retryable ? { recovery: "transient: retry or resume later with the same operation ID; never start a second payment", retryWithSameOperationId: true } : {}) } }), 1);
116
117
  }
117
118
  });
@@ -17,6 +17,7 @@ import { captureSellerHome, decryptSellerSnapshot, encryptSellerSnapshot, restor
17
17
  import { X402ResourceService } from "./x402-resource-service.js";
18
18
  import { LiveSellerTransport } from "./live-seller-transport.js";
19
19
  import { registerDirectoryCapability } from "./seller-directory.js";
20
+ import { isMainModule } from "./main-module.js";
20
21
  const isV2 = (row) => row.preview.genericV2 === true;
21
22
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
22
23
  const validId = (value, name) => typeof value === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(value) ? value : fail("SELLER_REQUEST_INVALID", `${name} is invalid`);
@@ -399,7 +400,7 @@ export class SellerService {
399
400
  close() { for (const slot of this.#slots.values())
400
401
  slot.app.close(); rmSync(this.#root, { recursive: true, force: true }); }
401
402
  }
402
- if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
403
+ if (isMainModule(import.meta.url)) {
403
404
  const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
404
405
  const port = Number(process.env.PORT ?? "3000");
405
406
  const origin = required("SELLER_ORIGIN");
@@ -5,13 +5,14 @@ import { createInterface } from "node:readline/promises";
5
5
  import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
6
6
  import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
7
7
  import { fail } from "./errors.js";
8
+ import { TESTNET_DEFAULTS, withTestnetDefaults } from "./testnet-defaults.js";
8
9
  import { initialize } from "./phase3.js";
9
10
  function ownerFile(path) {
10
11
  if ((statSync(path).mode & 0o077) !== 0)
11
12
  fail("SETUP_PERMISSIONS", `${path} must be owner-only`);
12
13
  }
13
14
  function validateConfig(config) {
14
- config.deploymentConfig ||= join(dirname(fileURLToPath(import.meta.url)), "../config/phase1.testnet.json");
15
+ withTestnetDefaults(config);
15
16
  for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator"]) {
16
17
  if (typeof config[key] !== "string" || !config[key])
17
18
  fail("SETUP_INVALID", `${key} is required`);
@@ -81,14 +82,15 @@ export async function promptSellerSetup(home) {
81
82
  fail("SETUP_INPUT_REQUIRED", "interactive seller setup requires a terminal; use --config with an owner-only JSON file");
82
83
  const rl = createInterface({ input: process.stdin, output: process.stderr });
83
84
  try {
84
- const ask = async (label) => (await rl.question(`${label}: `)).trim();
85
+ const ask = async (label, fallback) => (await rl.question(fallback ? `${label} [${fallback}]: ` : `${label}: `)).trim() || fallback || "";
86
+ process.stderr.write("Press Enter to accept a bracketed default. Credentials are read from owner-only files, never typed here.\n");
85
87
  const config = {
86
88
  deploymentConfig: await ask("Pinned v2 Testnet deployment config path (blank for bundled config)"),
87
- tatumKeyFile: await ask("Owner-only Tatum key file path"),
88
- writeGrpc: await ask("Sui write gRPC URL"),
89
- tatumGrpc: await ask("Tatum read gRPC URL"),
90
- walrusPublisher: await ask("Walrus publisher URL"),
91
- walrusAggregator: await ask("Walrus aggregator URL"),
89
+ tatumKeyFile: await ask("Owner-only Tatum API key file path"),
90
+ writeGrpc: await ask("Sui write gRPC URL", TESTNET_DEFAULTS.writeGrpc),
91
+ tatumGrpc: await ask("Tatum read gRPC URL", TESTNET_DEFAULTS.tatumGrpc),
92
+ walrusPublisher: await ask("Walrus publisher URL", TESTNET_DEFAULTS.walrusPublisher),
93
+ walrusAggregator: await ask("Walrus aggregator URL", TESTNET_DEFAULTS.walrusAggregator),
92
94
  };
93
95
  const snarkjs = await ask("SnarkJS executable path (blank for deployment-relative default)");
94
96
  const converter = await ask("Groth16 converter path (blank for deployment-relative default)");
@@ -25,6 +25,10 @@ export interface SettlementMemoryV1 {
25
25
  reliability?: number;
26
26
  quality?: number;
27
27
  outcome?: "succeeded" | "failed" | "unresolved";
28
+ amount?: string;
29
+ latencyMs?: number;
30
+ rail?: CapabilityRail;
31
+ periodMs?: number;
28
32
  };
29
33
  }
30
34
  export interface SettlementCitation {
@@ -33,7 +37,7 @@ export interface SettlementCitation {
33
37
  provenance: SettlementMemoryProvenance;
34
38
  distance: number;
35
39
  }
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";
40
+ 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" | "MEMORY_RELIABILITY_LEARNED" | "MEMORY_SELLER_DEMOTED" | "MEMORY_SPEND_LIMIT_REACHED";
37
41
  export interface SettlementPlanV1 {
38
42
  version: typeof SETTLEMENT_PLAN_VERSION;
39
43
  planHash: string;
@@ -81,7 +85,7 @@ export declare function canonicalJson(value: unknown): string;
81
85
  export declare const settlementMandateHash: (mandate: PlanningMandate) => string;
82
86
  export declare function settlementPlanHash(plan: Omit<SettlementPlanV1, "planHash">): string;
83
87
  export declare function validateSettlementMemory(value: unknown, assignedProvenance?: SettlementMemoryProvenance): asserts value is SettlementMemoryV1;
84
- export declare function createSettlementPlan(input: {
88
+ export interface PlanningInput {
85
89
  operationId: string;
86
90
  mandate: PlanningMandate;
87
91
  manifests: CapabilityManifest[];
@@ -91,5 +95,30 @@ export declare function createSettlementPlan(input: {
91
95
  recalled?: RecalledSettlementMemory[];
92
96
  maxMemoryDistance?: number;
93
97
  now?: number;
94
- }): SettlementPlanV1;
98
+ }
99
+ /**
100
+ * Trusted typed memories (owner policy or verified outcome) are matched by their structured scope, so a
101
+ * semantic distance cutoff would only drop real evidence: embeddings of JSON records rank far from scope
102
+ * queries. Advisory and legacy text still needs semantic relevance.
103
+ */
104
+ export declare function isRelevantMemory(item: RecalledSettlementMemory, maxDistance: number): boolean;
105
+ export declare function createSettlementPlan(input: PlanningInput): SettlementPlanV1;
95
106
  export declare function validateSettlementPlan(value: unknown): asserts value is SettlementPlanV1;
107
+ export interface MemoryDecision {
108
+ sellerId: string;
109
+ capabilityId: string;
110
+ rail: CapabilityRail;
111
+ riskLevel: SettlementRisk;
112
+ maximumAmount: string;
113
+ }
114
+ export interface MemoryInfluence {
115
+ changedSeller: boolean;
116
+ changedRail: boolean;
117
+ changedRisk: boolean;
118
+ changedMaximum: boolean;
119
+ withoutMemory?: MemoryDecision;
120
+ withMemory: MemoryDecision;
121
+ citedBlobIds: string[];
122
+ }
123
+ /** Counterfactual: what the same mandate would have paid without any recalled memory. Not part of the plan hash. */
124
+ export declare function explainMemoryInfluence(input: PlanningInput, plan: SettlementPlanV1): MemoryInfluence;
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { fail } from "./errors.js";
3
+ import { buildScorecard, spentSince } from "./memory-scorecard.js";
3
4
  export const SETTLEMENT_PLAN_VERSION = "supercuts.settlement-plan.v1";
4
5
  export const SETTLEMENT_MEMORY_VERSION = "supercuts.settlement-memory.v1";
5
6
  export function canonicalJson(value) {
@@ -38,7 +39,7 @@ export function validateSettlementMemory(value, assignedProvenance) {
38
39
  fail("SETTLEMENT_MEMORY_INVALID", "memory observation must be a short non-empty string");
39
40
  const scope = raw.scope, signal = raw.signal;
40
41
  exactKeys(scope, ["counterparty", "capabilityId", "capabilityClass", "network", "asset"]);
41
- exactKeys(signal, ["requiresShielded", "maximumAmount", "reliability", "quality", "outcome"]);
42
+ exactKeys(signal, ["requiresShielded", "maximumAmount", "reliability", "quality", "outcome", "amount", "latencyMs", "rail", "periodMs"]);
42
43
  for (const item of Object.values(scope))
43
44
  if (typeof item !== "string" || !item || item.length > 1024)
44
45
  fail("SETTLEMENT_MEMORY_INVALID", "memory scope fields must be non-empty strings");
@@ -51,6 +52,13 @@ export function validateSettlementMemory(value, assignedProvenance) {
51
52
  fail("SETTLEMENT_MEMORY_INVALID", "memory scores must be finite values in [0,1]");
52
53
  if (signal.outcome !== undefined && !["succeeded", "failed", "unresolved"].includes(signal.outcome))
53
54
  fail("SETTLEMENT_MEMORY_INVALID", "memory outcome is invalid");
55
+ if (signal.amount !== undefined && !unsigned(signal.amount) || signal.rail !== undefined && !["x402", "shielded"].includes(signal.rail))
56
+ fail("SETTLEMENT_MEMORY_INVALID", "memory payment amount or rail is invalid");
57
+ for (const value of [signal.latencyMs, signal.periodMs])
58
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
59
+ fail("SETTLEMENT_MEMORY_INVALID", "memory durations must be non-negative integers");
60
+ if (signal.periodMs !== undefined && (raw.kind !== "budget_limit" || signal.periodMs === 0))
61
+ fail("SETTLEMENT_MEMORY_INVALID", "only budget limits may declare a positive period");
54
62
  const required = { privacy_rule: "requiresShielded", budget_limit: "maximumAmount", reliability: "reliability", quality: "quality", outcome: "outcome" };
55
63
  if (signal[required[raw.kind]] === undefined)
56
64
  fail("SETTLEMENT_MEMORY_INVALID", "memory kind does not have its required signal");
@@ -64,6 +72,18 @@ function specificity(scope, manifest) {
64
72
  return -1;
65
73
  return scope.capabilityId ? 4 : scope.capabilityClass ? 3 : scope.counterparty ? 2 : 1;
66
74
  }
75
+ /**
76
+ * Trusted typed memories (owner policy or verified outcome) are matched by their structured scope, so a
77
+ * semantic distance cutoff would only drop real evidence: embeddings of JSON records rank far from scope
78
+ * queries. Advisory and legacy text still needs semantic relevance.
79
+ */
80
+ export function isRelevantMemory(item, maxDistance) {
81
+ if (!Number.isFinite(item.distance) || item.distance < 0)
82
+ return false;
83
+ if (item.memory && (item.memory.provenance === "owner_policy" || item.memory.provenance === "verified_outcome"))
84
+ return true;
85
+ return item.distance <= maxDistance;
86
+ }
67
87
  export function createSettlementPlan(input) {
68
88
  const now = input.now ?? Date.now(), maxDistance = input.maxMemoryDistance ?? 0.35;
69
89
  if (!input.operationId || input.operationId.length > 256 || !input.manifests.length)
@@ -72,8 +92,9 @@ export function createSettlementPlan(input) {
72
92
  fail("SETTLEMENT_PLAN_INVALID", "requestedAmount must be canonical unsigned base units");
73
93
  if (!Number.isFinite(maxDistance) || maxDistance < 0 || maxDistance > 1 || now >= input.mandate.expiresAt)
74
94
  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);
95
+ const relevant = (item) => isRelevantMemory(item, maxDistance);
96
+ const ignored = (input.recalled ?? []).filter((item) => !relevant(item));
97
+ const recalled = (input.recalled ?? []).filter(relevant);
77
98
  for (const item of recalled)
78
99
  if (item.memory)
79
100
  validateSettlementMemory(item.memory);
@@ -99,19 +120,30 @@ export function createSettlementPlan(input) {
99
120
  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
121
  if (!rails.length)
101
122
  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))
123
+ const budgetMemories = trusted.filter((item) => item.memory.kind === "budget_limit").map((item) => item.memory);
124
+ const budgets = budgetMemories.filter((item) => item.signal.periodMs === undefined).map((item) => BigInt(item.signal.maximumAmount));
125
+ // Periodic owner budgets subtract the verified spend that Walrus Memory remembers for the same scope.
126
+ const remaining = budgetMemories.filter((item) => item.signal.periodMs !== undefined).map((item) => { const limit = BigInt(item.signal.maximumAmount), spent = spentSince(recalled, item.scope, now - item.signal.periodMs); return limit > spent ? limit - spent : 0n; });
127
+ const minimumPrice = BigInt(manifest.priceBand.minimum);
128
+ let maximum = [BigInt(manifest.priceBand.maximum), BigInt(input.mandate.perDealCeiling), ...budgets, ...remaining].reduce((a, b) => a < b ? a : b);
129
+ const spendLimited = maximum < minimumPrice && remaining.some((value) => value < minimumPrice) && [BigInt(manifest.priceBand.maximum), BigInt(input.mandate.perDealCeiling), ...budgets].every((value) => value >= minimumPrice);
130
+ if (maximum < minimumPrice && !spendLimited)
105
131
  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";
132
+ if (spendLimited)
133
+ maximum = minimumPrice;
134
+ const card = buildScorecard(recalled, { counterparty: manifest.sellerId, capabilityClass: manifest.capability }, now);
135
+ const explicit = latest("reliability")[0];
136
+ const reliability = explicit?.provenance === "owner_policy" ? explicit.signal.reliability : card.observations ? card.reliability : explicit?.signal.reliability ?? 0.5;
137
+ const quality = latest("quality")[0]?.signal.quality ?? 0.5, outcome = card.lastOutcome ?? latest("outcome")[0]?.signal.outcome;
138
+ const risk = conflict || spendLimited || outcome === "unresolved" || reliability < 0.5 ? "review_required" : reliability >= 0.8 ? "low" : "medium";
108
139
  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"] : [])] });
140
+ candidates.push({ manifest, rail, reliability, quality, maximum, risk, citations, conflict, card, reasons: ["MANDATE_DEFAULTS_APPLIED", ...(policies.length ? ["MEMORY_PRIVACY_APPLIED"] : []), ...(budgetMemories.length ? ["MEMORY_BUDGET_APPLIED"] : []), ...(spendLimited ? ["MEMORY_SPEND_LIMIT_REACHED"] : []), ...(outcome ? ["MEMORY_OUTCOME_APPLIED"] : []), ...(card.observations ? ["MEMORY_RELIABILITY_LEARNED"] : []), ...(citations.some((c) => ["agent_observation", "legacy_untrusted"].includes(c.provenance)) ? ["MEMORY_UNTRUSTED_ADVISORY"] : []), ...(ignored.length ? ["MEMORY_LOW_RELEVANCE_IGNORED"] : [])] });
110
141
  }
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));
142
+ candidates.sort((a, b) => Number(a.risk === "review_required") - Number(b.risk === "review_required") || 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
143
  const selected = candidates[0] ?? fail("SETTLEMENT_PLAN_DENIED", "no capability remains within the owner mandate, privacy rules, and price bounds");
113
144
  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"];
145
+ const demoted = candidates.some((item) => item.manifest.sellerId !== selected.manifest.sellerId && item.card.observations > 0 && item.reliability < selected.reliability);
146
+ const reasons = [...selected.reasons, ...(demoted ? ["MEMORY_SELLER_DEMOTED"] : []), ...(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
147
  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
148
  return { ...withoutHash, planHash: settlementPlanHash(withoutHash) };
117
149
  }
@@ -139,3 +171,16 @@ export function validateSettlementPlan(value) {
139
171
  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
172
  fail("SETTLEMENT_PLAN_INVALID", "settlement plan citations or reasons are invalid");
141
173
  }
174
+ const decision = (plan) => ({ sellerId: plan.sellerId, capabilityId: plan.capabilityId, rail: plan.rail, riskLevel: plan.riskLevel, maximumAmount: plan.maximumAmount });
175
+ /** Counterfactual: what the same mandate would have paid without any recalled memory. Not part of the plan hash. */
176
+ export function explainMemoryInfluence(input, plan) {
177
+ let withoutMemory;
178
+ try {
179
+ withoutMemory = decision(createSettlementPlan({ ...input, recalled: [], now: plan.createdAt }));
180
+ }
181
+ catch {
182
+ withoutMemory = undefined;
183
+ }
184
+ const withMemory = decision(plan);
185
+ return { changedSeller: withoutMemory?.sellerId !== withMemory.sellerId, changedRail: withoutMemory?.rail !== withMemory.rail, changedRisk: withoutMemory?.riskLevel !== withMemory.riskLevel, changedMaximum: withoutMemory?.maximumAmount !== withMemory.maximumAmount, ...(withoutMemory ? { withoutMemory } : {}), withMemory, citedBlobIds: plan.citations.map((item) => item.blobId) };
186
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Public Sui Testnet, Walrus Testnet, and hosted Supercuts endpoints used when an owner leaves a setup field blank.
3
+ * None of these values are credentials; owners can still override every one in setup JSON or the prompts.
4
+ */
5
+ export declare const TESTNET_DEFAULTS: {
6
+ readonly writeGrpc: "https://fullnode.testnet.sui.io:443";
7
+ readonly tatumGrpc: "https://sui-testnet-grpc.gateway.tatum.io:443";
8
+ readonly walrusPublisher: "https://publisher.walrus-testnet.walrus.space";
9
+ readonly walrusAggregator: "https://aggregator.walrus-testnet.walrus.space";
10
+ readonly directoryUrl: "https://supercuts-sellers-testnet.onrender.com";
11
+ };
12
+ /** Pinned experimental v2 Testnet deployment metadata shipped inside the npm package. */
13
+ export declare function bundledDeploymentConfig(): string;
14
+ /** Fills blank endpoint fields with the public Testnet defaults. */
15
+ export declare function withTestnetDefaults<T extends {
16
+ deploymentConfig?: string;
17
+ writeGrpc?: string;
18
+ tatumGrpc?: string;
19
+ walrusPublisher?: string;
20
+ walrusAggregator?: string;
21
+ }>(config: T): T;
@@ -0,0 +1,26 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ /**
4
+ * Public Sui Testnet, Walrus Testnet, and hosted Supercuts endpoints used when an owner leaves a setup field blank.
5
+ * None of these values are credentials; owners can still override every one in setup JSON or the prompts.
6
+ */
7
+ export const TESTNET_DEFAULTS = {
8
+ writeGrpc: "https://fullnode.testnet.sui.io:443",
9
+ tatumGrpc: "https://sui-testnet-grpc.gateway.tatum.io:443",
10
+ walrusPublisher: "https://publisher.walrus-testnet.walrus.space",
11
+ walrusAggregator: "https://aggregator.walrus-testnet.walrus.space",
12
+ directoryUrl: "https://supercuts-sellers-testnet.onrender.com",
13
+ };
14
+ /** Pinned experimental v2 Testnet deployment metadata shipped inside the npm package. */
15
+ export function bundledDeploymentConfig() {
16
+ return join(dirname(fileURLToPath(import.meta.url)), "../config/phase1.testnet.json");
17
+ }
18
+ /** Fills blank endpoint fields with the public Testnet defaults. */
19
+ export function withTestnetDefaults(config) {
20
+ config.deploymentConfig ||= bundledDeploymentConfig();
21
+ config.writeGrpc ||= TESTNET_DEFAULTS.writeGrpc;
22
+ config.tatumGrpc ||= TESTNET_DEFAULTS.tatumGrpc;
23
+ config.walrusPublisher ||= TESTNET_DEFAULTS.walrusPublisher;
24
+ config.walrusAggregator ||= TESTNET_DEFAULTS.walrusAggregator;
25
+ return config;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercuts-walrus/backend",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -25,7 +25,8 @@
25
25
  "supercuts-seller-v2-provision": "dist/seller-v2-provision.js",
26
26
  "supercuts-hosted-exit": "dist/hosted-exit-cli.js",
27
27
  "supercuts-hosted-seller-import": "dist/hosted-seller-import.js",
28
- "supercuts-seller-directory": "dist/seller-directory-cli.js"
28
+ "supercuts-seller-directory": "dist/seller-directory-cli.js",
29
+ "supercuts-embedding-adapter": "dist/embedding-adapter.js"
29
30
  },
30
31
  "files": [
31
32
  "dist",