@dvmkit/sdk 0.1.3-rc.7 → 0.1.5-rc.7

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 (38) hide show
  1. package/README.md +116 -9
  2. package/dist/{chunk-ANFX5HEG.js → chunk-2UUXIIOC.js} +43 -13
  3. package/dist/{chunk-C6JHBLMW.js → chunk-BIP6G74V.js} +1 -1
  4. package/dist/{chunk-EPNDZ5DH.js → chunk-CEOAHV2I.js} +0 -30
  5. package/dist/{chunk-TSWKITGR.js → chunk-CGKZDODG.js} +8 -1
  6. package/dist/{chunk-YDIYXGYL.js → chunk-E4EVGPDX.js} +16 -9
  7. package/dist/{chunk-NFRM5QYP.js → chunk-EVBK675R.js} +5 -0
  8. package/dist/{chunk-DBCLBYHP.js → chunk-KVEHHC7W.js} +11 -1
  9. package/dist/{chunk-2K7E3N2D.js → chunk-KXCKFIJI.js} +77 -14
  10. package/dist/{chunk-3ZHMQCYP.js → chunk-NJFOV36R.js} +663 -235
  11. package/dist/{chunk-5GFED3GJ.js → chunk-NPIW5VR5.js} +40 -6
  12. package/dist/{chunk-LLXV32HA.js → chunk-SSSZUVWM.js} +96 -3
  13. package/dist/{chunk-7AKBC4PW.js → chunk-TQWGQCNV.js} +1 -1
  14. package/dist/{chunk-4B56DEEV.js → chunk-U6M3ATSG.js} +7 -1
  15. package/dist/{credit-menu-C7zAJElJ.d.ts → credit-menu-B-e3vZGo.d.ts} +136 -21
  16. package/dist/{fx-BF_SG2i0.d.ts → fx-CGRJE8rm.d.ts} +1 -1
  17. package/dist/index.d.ts +5 -5
  18. package/dist/internal/caller.d.ts +54 -22
  19. package/dist/internal/caller.js +171 -38
  20. package/dist/internal/index.js +1 -1
  21. package/dist/internal/server.d.ts +8 -8
  22. package/dist/internal/server.js +7 -7
  23. package/dist/{job-store-Bn23V3QU.d.ts → job-store-Cnlv9pOx.d.ts} +16 -1
  24. package/dist/{lightning-backend-C04nH94l.d.ts → lightning-backend-KvQM0YHi.d.ts} +1 -1
  25. package/dist/{memory-credit-ledger-OP24Z2KO.js → memory-credit-ledger-MNUOTQO5.js} +1 -1
  26. package/dist/{mpp-setup-MOBWGTWJ.js → mpp-setup-SPBOF5AM.js} +11 -1
  27. package/dist/{postgres-job-store-TAONYLIF.js → postgres-job-store-3RAXMNSY.js} +1 -1
  28. package/dist/{revenue-reporter-XXSU5KVB.js → revenue-reporter-ASZ7SHHH.js} +1 -1
  29. package/dist/server/index.d.ts +26 -13
  30. package/dist/server/index.js +219 -55
  31. package/dist/{step-cache-3cT4Shk0.d.ts → step-cache-CwM_Q8rK.d.ts} +149 -3
  32. package/dist/{tempo-lifecycle-SQL3KLEZ.js → tempo-lifecycle-DFIXQ54Q.js} +3 -3
  33. package/dist/{tempo-wallet-QOLEIPCV.js → tempo-wallet-4QKSV65O.js} +10 -2
  34. package/dist/testing/index.d.ts +13 -3
  35. package/dist/testing/index.js +31 -2
  36. package/dist/{usd-BnuXoFl5.d.ts → usd-nYZSb7KQ.d.ts} +1 -1
  37. package/dist/{x402-T2C5MX3T.js → x402-5H27DCBE.js} +2 -2
  38. package/package.json +6 -4
package/README.md CHANGED
@@ -2,21 +2,113 @@
2
2
 
3
3
  Build Digital Vending Machines: accountless HTTPS services with typed inputs, jobs, and payment rails.
4
4
 
5
- ## Development
5
+ Requires Node.js 22 or later. This is pre-1.0 alpha software: pin the exact package version and read the [release notes](./CHANGELOG.md) before upgrading.
6
6
 
7
- Use the Node.js release in `.nvmrc` (Node 22). Install with `npm ci`, then run the same gate CI runs with `npm run verify`: the toolchain check, lint, build, typecheck (including the release scripts under `checkJs`), unit and database tests, the public API surface and package-shape checks, a consumer install, a public-safety scan, the release contract, and a clean-install smoke.
7
+ ## Quickstart: a free DVM
8
+
9
+ Create a project and install the SDK:
10
+
11
+ ```sh
12
+ mkdir hello-dvm && cd hello-dvm
13
+ npm init -y
14
+ npm install --save-exact @dvmkit/sdk
15
+ ```
16
+
17
+ Save this as `handler.mjs`:
18
+
19
+ ```js
20
+ import { configureDVM, z } from "@dvmkit/sdk";
21
+
22
+ export default configureDVM({
23
+ name: "hello",
24
+ capability: "echo",
25
+ input: z.object({ text: z.string().min(1) }),
26
+ example: { text: "hello" },
27
+ onJob(ctx) {
28
+ ctx.artifact({ mime_type: "text/plain", data: ctx.input.text });
29
+ ctx.complete("Echoed the input");
30
+ },
31
+ });
32
+ ```
33
+
34
+ Save this as `server.mjs`, then run it with `node server.mjs`:
35
+
36
+ ```js
37
+ import { serve } from "@dvmkit/sdk/server";
38
+ import dvm from "./handler.mjs";
39
+
40
+ await serve(dvm, { devMode: true });
41
+ ```
42
+
43
+ Submit a local request. `devMode` makes Postgres optional and skips payment verification only when no Cashu, x402, or Tempo rail is configured:
44
+
45
+ ```sh
46
+ curl -sS http://localhost:8080/v1/job \
47
+ -H 'content-type: application/json' \
48
+ --data '{"capability":"echo","data":{"text":"hello"}}'
49
+ ```
50
+
51
+ Test the handler without opening a port or configuring a payment rail:
52
+
53
+ ```js
54
+ import assert from "node:assert/strict";
55
+ import test from "node:test";
56
+ import { createTestContext } from "@dvmkit/sdk/testing";
57
+ import dvm from "./handler.mjs";
58
+
59
+ test("echoes its input", async () => {
60
+ const ctx = createTestContext({ input: { text: "hello" } });
61
+ await dvm.capabilities.echo.onJob(ctx);
62
+ assert.equal(ctx.completed, true);
63
+ assert.equal(ctx.messages[0].content.data, "hello");
64
+ });
65
+ ```
66
+
67
+ Run that file with `node --test handler.test.mjs`. The repository carries the complete version under [`examples/quickstart`](./examples/quickstart), plus runnable [signed-auth/custom-route](./examples/authenticated-route) and [payment/lifecycle](./examples/payments-and-lifecycle) examples.
68
+
69
+ ## Public entry points
70
+
71
+ | Import | Use it for |
72
+ | --- | --- |
73
+ | `@dvmkit/sdk` | DVM descriptors, handler context, bundled Zod, pricing, signing and safe-fetch helpers |
74
+ | `@dvmkit/sdk/server` | Node hosting, Postgres stores, signed-request auth, compatibility gates and payment infrastructure |
75
+ | `@dvmkit/sdk/testing` | Deterministic handler contexts and in-memory stores |
76
+
77
+ The generated [API reference](./docs/API.md) lists every declaration under these three entries. Deep imports from `dist/` are unsupported.
78
+
79
+ ## Verified payments in development
80
+
81
+ `serve()` returns a `paymentModes` snapshot describing whether each configured rail is verifying, unavailable, or absent. A priced development server skips verification only when it has no configured payment rail. Configuring an unusable rail fails closed instead of turning the request into a synthetic development payment.
82
+
83
+ Cashu development without Postgres is limited to known test or loopback mints. It uses a fresh lock identity and process-local accounting on every boot, so proofs, balances, and jobs do not survive restart. Production Cashu remains PostgreSQL-backed. Both paths require valid mint-provided NUT-12 DLEQ evidence on every proof before accepting its value; older tokens without that evidence must be replaced with fresh tokens from a compatible mint.
84
+
85
+ Builders can pass `onPaidJobCompleted` to observe settled customer charges after successful completion. The event contains `jobId`, verified rail, checked integer `amountMicro`, the credit currency, and whether configuration identifies test funds. Reconciliation can repeat an event, so consumers deduplicate by `jobId`. Free jobs, synthetic development payments, failed jobs, and released draws do not emit.
86
+
87
+ ## Production next steps
88
+
89
+ - Use Postgres through `DATABASE_URL`; production boot refuses an in-memory job store unless you deliberately supply another durable implementation.
90
+ - Add `auth: secp256k1Auth()` for caller-owned state. The host automatically uses a Postgres replay store when one is available; a per-process replay window is unsafe across replicas.
91
+ - Configure only the rails you operate: Cashu, x402, and Tempo are job payment methods. Lightning is a receive-only prepaid-credit funding leg. Keep every private key, wallet connection, and platform token in your secret store rather than source.
92
+ - A `credit` block opts into reusable prepaid balances and their reclaim obligations. Reusable channels return undrawn collateral through their channel lifecycle; one-payment stablecoin funding is disabled unless `allowOneShotStablecoin: true` because it creates manual refunds. Run and monitor the documented reclaim/payout worker.
93
+ - Honour `ctx.signal`, use `ctx.fetch` for abortable provider calls, and declare provider cost with `ctx.cost` only after the durable step that incurred it.
94
+
95
+ The public [SDK guide](https://dvmkit.com/docs/sdk) covers signed requests, custom routes, rail configuration, credit reclaim, persistence, and deployment in depth. The [protocol guide](https://dvmkit.com/docs/protocol) specifies the HTTP contract.
96
+
97
+ ## Repository development
98
+
99
+ Use the Node.js release in `.nvmrc` (Node 22). Install with `npm ci`, then run the same gate CI runs with `npm run verify`. The gate checks repository metadata in the checkout, copies only tracked and unignored source into an isolated directory, installs its locked dependencies, and runs lint, build, typecheck (including the release scripts under `checkJs`), unit and database tests, API, package, consumer, packed-artifact safety, and release checks there. The clean source tree owns the complete build and test pass, so verification never depends on the checkout's `node_modules` or pre-existing `dist` and does not repeat the full suite; after every check passes, its verified `dist` replaces the checkout's build output for release packaging.
8
100
 
9
101
  `DATABASE_URL` is optional locally. Without it the database suite is skipped rather than passed, and `verify` says so in its summary, because a skipped suite verified nothing and must not be reported as green. Point `DATABASE_URL` at a scratch Postgres to run it, and pass `npm run verify -- --require-database` to make a missing one a failure, which is what CI does.
10
102
 
11
- The public surface is exactly `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. There is a fourth entry point, `@dvmkit/sdk/internal`, which is not part of it; see below. Documentation is at [dvmkit.ai](https://dvmkit.ai/docs/sdk).
103
+ The public surface is exactly `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`.
12
104
 
13
105
  ## Compatibility and versioning
14
106
 
15
107
  This is pre-1.0 alpha software and the promises match that. Breaking changes can land in any release, they are announced in the release notes for the release that carries them, and there is no long-term support release. Security fixes are made on the latest published release rather than backported.
16
108
 
17
- The public surface is exactly three entry points: `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. Nothing reachable only through a deep path into `dist/` is public, nothing under `@dvmkit/sdk/internal` is public, and `npm run check:api` pins every exported declaration name against `api-surface.json`, so a change to that surface is a deliberate, reviewed act rather than a side effect.
109
+ Nothing reachable only through a deep path into `dist/` is public. `npm run check:api` pins every exported declaration name against `api-surface.json` and the generated API reference, so a change to the supported surface is a deliberate, reviewed act rather than a side effect.
18
110
 
19
- Wire compatibility with callers is separate from the package version and is declared per endpoint. A server built on this SDK answers `DVM-Protocol-Version: 1`, and an individual endpoint may require a set of named capability tokens the caller advertises, a minimum caller version, or both. Capability tokens are additive, so an endpoint that declares no requirement stays reachable by a caller that sends no compatibility headers at all. A caller that cannot meet what an endpoint declares gets HTTP 426 naming the required capabilities and, where one is declared, the minimum version. The protocol is specified at [dvmkit.ai/docs/protocol](https://dvmkit.ai/docs/protocol).
111
+ Wire compatibility with callers is separate from the package version and is declared per endpoint. A server built on this SDK answers `DVM-Protocol-Version: 1`, and an individual endpoint may require a set of named capability tokens the caller advertises, a minimum caller version, or both. Capability tokens are additive, so an endpoint that declares no requirement stays reachable by a caller that sends no compatibility headers at all. A caller that cannot meet what an endpoint declares gets HTTP 426 naming the required capabilities and, where one is declared, the minimum version. The protocol is specified at [dvmkit.com/docs/protocol](https://dvmkit.com/docs/protocol).
20
112
 
21
113
  ## Job credentials
22
114
 
@@ -34,13 +126,28 @@ The same values are accepted as `--endpoint <url>` and `--job-token <hex>`. Thes
34
126
 
35
127
  If an upgrade broke something for you, an [API compatibility report](https://github.com/dvmkit/sdk/issues/new/choose) is the most useful thing you can send us. It tells us which parts of the published surface people actually build on, which no test of ours can.
36
128
 
37
- ## Internal entry point
129
+ ## Per-job costs
130
+
131
+ A handler can declare what a job cost **you**, the builder, to serve — a vendor API call, a GPU minute, a per-request licence fee — and the SDK reports it to the platform alongside what the caller paid, so your dashboard can state a margin instead of guessing at one.
132
+
133
+ ```ts
134
+ const transcript = await ctx.step("transcribe", async () => {
135
+ const result = await whisper(audio);
136
+ ctx.cost({ amount: result.seconds * 0.0001, currency: "usd" });
137
+ return result;
138
+ });
139
+ ctx.artifact({ mime_type: "text/plain", data: transcript.text });
140
+ ```
141
+
142
+ `amount` is in whole currency units, like every other money figure you hand the SDK. Call it once per cost you incur; the declarations for one job are summed, and only the total is rounded, to a millionth of a unit. That ordering is what makes fine-grained declarations safe: a per-token charge of $0.0000003 is below the reported resolution on its own, but a thousand of them still add up to $0.0003 rather than to nothing. Every call in a job must name the same currency.
38
143
 
39
- `@dvmkit/sdk/internal` exists for one consumer: the dvmkit platform, which runs on this source instead of keeping a fork of it. Do not build on it.
144
+ Three things worth knowing:
40
145
 
41
- It carries no stability promise of any kind. A name under it can change meaning, change signature, or disappear in any release, including a patch release, and that is not a breaking change: it needs no `!` marker, no `BREAKING CHANGE:` footer, and no note in the release notes. It is not documented, its contents track what the platform happens to need, and it is excluded from everything the three public entry points promise above.
146
+ - **Declaring nothing is not declaring zero.** A job whose handler never calls `ctx.cost` reports no cost at all, and the dashboard says the margin is unknown rather than showing it as 100%. `ctx.cost({ amount: 0, currency: "usd" })` is how you say a job genuinely cost you nothing.
147
+ - **Declare the cost where you incur it, including inside `ctx.step`.** The step cache carries those declarations with the result and applies them once when a replay skips the body.
148
+ - **Every terminal job reports its declared cost.** A paid completion carries the cost on its revenue report. A free completion, failure, or cancellation sends a separate cost-only report and does not create revenue.
42
149
 
43
- The names in it are still pinned in `api-surface.json`, so a change to them is visible in review rather than silent; the pin records what moved, it does not promise that it will not move. If something under `./internal` is genuinely useful to a third party, the way to get it is to ask for it: graduating a name to `.`, `./server` or `./testing` is a deliberate, reviewed `feat`, and only then does it acquire the compatibility promises on this page.
150
+ The cost is yours, never the caller's: it is recorded separately from what the caller paid and is never netted against it. Both report shapes use the same durable outbox. Cost-only reports are keyed on the job id and revisioned, so retries and cross-machine updates converge on the latest cumulative value. With the Postgres job store, boot-time and periodic reconciliation enqueue any terminal cost revision that a process exited before reporting.
44
151
 
45
152
  ## Contributing
46
153
 
@@ -2,7 +2,7 @@ import {
2
2
  isValidEvmPrivateKey,
3
3
  loadConfig,
4
4
  updateConfig
5
- } from "./chunk-TSWKITGR.js";
5
+ } from "./chunk-CGKZDODG.js";
6
6
  import {
7
7
  DvmError,
8
8
  resolveRpcHttpTransportOptions,
@@ -16,8 +16,24 @@ import {
16
16
  // src/lib/payment-rails/tempo-wallet.ts
17
17
  var TEMPO_USDC_MAINNET = "0x20C000000000000000000000b9537d11c60E8b50";
18
18
  var TEMPO_CHAIN_ID = 4217;
19
- function resolveTempoAsset(env = process.env) {
20
- const address = env.DVM_TEMPO_CURRENCY ?? TEMPO_USDC_MAINNET;
19
+ var TEMPO_MODERATO_CHAIN_ID = 42431;
20
+ var TEMPO_PATH_USD_MODERATO = "0x20c0000000000000000000000000000000000000";
21
+ var TEMPO_MODERATO_RPC = "https://rpc.moderato.tempo.xyz";
22
+ function resolveTempoNetwork(network = "mainnet") {
23
+ return network === "moderato" ? {
24
+ network,
25
+ chainId: TEMPO_MODERATO_CHAIN_ID,
26
+ rpcUrl: TEMPO_MODERATO_RPC,
27
+ defaultToken: TEMPO_PATH_USD_MODERATO
28
+ } : {
29
+ network,
30
+ chainId: TEMPO_CHAIN_ID,
31
+ rpcUrl: "https://rpc.tempo.xyz",
32
+ defaultToken: TEMPO_USDC_MAINNET
33
+ };
34
+ }
35
+ function resolveTempoAsset(env = process.env, network = "mainnet") {
36
+ const address = env.DVM_TEMPO_CURRENCY ?? resolveTempoNetwork(network).defaultToken;
21
37
  if (!HEX_ADDRESS_RE.test(address)) {
22
38
  throw new DvmError(
23
39
  "tempo_config_invalid",
@@ -58,7 +74,9 @@ function tempoWrongAssetHint(asset) {
58
74
  return `This rail settles only in ${asset.label} (${asset.address}) on Tempo. Another Tempo dollar \u2014 USDT0 or any other stablecoin \u2014 is a different contract and will never show up here, however much of it your wallet says you hold. If you funded with the wrong one, swap it to ${asset.label} on Tempo (an on-chain swap, fees are sub-cent) or send ${asset.label} to this address.`;
59
75
  }
60
76
  function tempoTokenSymbol(address) {
61
- return address.toLowerCase() === TEMPO_USDC_MAINNET.toLowerCase() ? "USDC.e" : null;
77
+ if (address.toLowerCase() === TEMPO_USDC_MAINNET.toLowerCase()) return "USDC.e";
78
+ if (address.toLowerCase() === TEMPO_PATH_USD_MODERATO.toLowerCase()) return "pathUSD";
79
+ return null;
62
80
  }
63
81
  var HEX_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
64
82
  async function connectedTempoAccount() {
@@ -83,7 +101,7 @@ async function generateTempoKey(opts = {}) {
83
101
  );
84
102
  }
85
103
  if (existing && opts.force) {
86
- const { assertTempoWalletIdle } = await import("./tempo-lifecycle-SQL3KLEZ.js");
104
+ const { assertTempoWalletIdle } = await import("./tempo-lifecycle-DFIXQ54Q.js");
87
105
  await assertTempoWalletIdle("replace");
88
106
  }
89
107
  const { privateKeyToAccount, generatePrivateKey } = await import("viem/accounts");
@@ -91,22 +109,30 @@ async function generateTempoKey(opts = {}) {
91
109
  const account = privateKeyToAccount(privateKey);
92
110
  const generated = {
93
111
  privateKey,
94
- address: account.address.toLowerCase()
112
+ address: account.address.toLowerCase(),
113
+ network: opts.network ?? "mainnet"
95
114
  };
96
115
  await opts.beforePersist?.(generated);
97
116
  await updateConfig({
98
117
  reason: "tempo_wallet_generate",
99
- patch: () => ({ set: { tempo: { method: "tempo", apiKey: privateKey } } })
118
+ patch: () => ({
119
+ set: {
120
+ tempo: { method: "tempo", apiKey: privateKey, network: generated.network }
121
+ }
122
+ })
100
123
  });
101
124
  return generated;
102
125
  }
103
126
  async function getTempoUsdcBalance(account) {
104
- const tokenAddress = resolveTempoAsset().address;
127
+ const accountNetwork = account.network ?? "mainnet";
128
+ const network = resolveTempoNetwork(accountNetwork);
129
+ const tokenAddress = resolveTempoAsset(process.env, accountNetwork).address;
105
130
  const { createPublicClient, http, erc20Abi } = await import("viem");
106
- const { tempo } = await import("viem/chains");
131
+ const { tempo, tempoModerato } = await import("viem/chains");
132
+ const chain = accountNetwork === "moderato" ? tempoModerato : tempo;
107
133
  const { url: rpcUrl, source: rpcSource } = resolveRpcOverride(
108
134
  process.env.DVM_TEMPO_RPC_URL,
109
- tempo.rpcUrls.default.http[0]
135
+ network.rpcUrl
110
136
  );
111
137
  try {
112
138
  const transportOptions = resolveRpcHttpTransportOptions(
@@ -114,7 +140,7 @@ async function getTempoUsdcBalance(account) {
114
140
  process.env.DVM_TEMPO_RPC_HEADERS_JSON
115
141
  );
116
142
  const client = createPublicClient({
117
- chain: tempo,
143
+ chain,
118
144
  transport: transportOptions ? http(rpcUrl, transportOptions) : http(rpcUrl)
119
145
  });
120
146
  const raw = await client.readContract({
@@ -128,7 +154,7 @@ async function getTempoUsdcBalance(account) {
128
154
  rawBaseUnits: raw,
129
155
  usdc: Number(raw) / 1e6,
130
156
  tokenAddress,
131
- chainId: TEMPO_CHAIN_ID,
157
+ chainId: network.chainId,
132
158
  // internal-review: redacted here rather than at a display surface, so the clear
133
159
  // endpoint never leaves this function on the success path either.
134
160
  rpcUrlRedacted: redactUrl(rpcUrl),
@@ -160,7 +186,7 @@ async function tempoAccountFromConfig(config) {
160
186
  if (!apiKey || !isValidEvmPrivateKey(apiKey)) return null;
161
187
  const privateKey = apiKey;
162
188
  const address = await deriveTempoAddress(privateKey);
163
- return { privateKey, address };
189
+ return { privateKey, address, network: config.tempo.network ?? "mainnet" };
164
190
  }
165
191
  function invalidTempoPrivateKey() {
166
192
  return new DvmError(
@@ -173,6 +199,10 @@ function invalidTempoPrivateKey() {
173
199
  export {
174
200
  TEMPO_USDC_MAINNET,
175
201
  TEMPO_CHAIN_ID,
202
+ TEMPO_MODERATO_CHAIN_ID,
203
+ TEMPO_PATH_USD_MODERATO,
204
+ TEMPO_MODERATO_RPC,
205
+ resolveTempoNetwork,
176
206
  resolveTempoAsset,
177
207
  tempoTokenLabel,
178
208
  tempoTokenReference,
@@ -10,7 +10,7 @@ import {
10
10
  isValidEvmPrivateKey,
11
11
  loadConfig,
12
12
  updateConfig
13
- } from "./chunk-TSWKITGR.js";
13
+ } from "./chunk-CGKZDODG.js";
14
14
  import {
15
15
  X402_DEFAULT_NETWORK,
16
16
  X402_V1_VERSION,
@@ -2,34 +2,6 @@ import {
2
2
  callerLoggers
3
3
  } from "./chunk-66HGCPBU.js";
4
4
 
5
- // src/lib/mints.ts
6
- var RECOMMENDED_MINTS = [
7
- {
8
- name: "Testnut",
9
- description: "Cashu test mint \u2014 use for testing only, not real funds",
10
- url: "https://testnut.cashu.space"
11
- },
12
- {
13
- name: "Voltz Mint",
14
- description: "Production Cashu mint by the Voltz Wallet team",
15
- url: "https://mint.lnvoltz.com"
16
- }
17
- ];
18
- function isTestMintUrl(url) {
19
- let host;
20
- try {
21
- host = new URL(url).hostname.toLowerCase();
22
- } catch {
23
- return false;
24
- }
25
- if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
26
- if (LOOPBACK_HOSTS.has(host)) return true;
27
- if (host.endsWith(".localhost")) return true;
28
- return TEST_MINT_HOSTS.has(host);
29
- }
30
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
31
- var TEST_MINT_HOSTS = /* @__PURE__ */ new Set(["testnut.cashu.space"]);
32
-
33
5
  // src/lib/x402/env.ts
34
6
  import { isAddress } from "viem";
35
7
  import { privateKeyToAccount } from "viem/accounts";
@@ -981,8 +953,6 @@ var NwcBackend = class {
981
953
  };
982
954
 
983
955
  export {
984
- RECOMMENDED_MINTS,
985
- isTestMintUrl,
986
956
  X402_SELF_RELAY_KEY,
987
957
  X402_RECEIVER_AUTHORIZER_KEY,
988
958
  X402_BATCH_SETTLEMENT_KEY,
@@ -181,6 +181,13 @@ function assertValidConfig(config, fields) {
181
181
  const value = owner[leaf];
182
182
  if (value === void 0 || !matchesKind(value, kind)) throw invalidFieldError(path, kind);
183
183
  }
184
+ if ((!scope || scope.has("tempo")) && isPlainObject(record.tempo) && record.tempo.network !== void 0 && record.tempo.network !== "mainnet" && record.tempo.network !== "moderato") {
185
+ throw new DvmError(
186
+ "config_invalid",
187
+ `The caller configuration field 'tempo.network' must be 'mainnet' or 'moderato'.`,
188
+ `Correct tempo.network in ${CONFIG_FILE}.${rollbackSuffix()}`
189
+ );
190
+ }
184
191
  }
185
192
  function readRawConfig() {
186
193
  if (!existsSync(CONFIG_FILE)) return null;
@@ -308,7 +315,7 @@ var PROTECTED_FIELDS = {
308
315
  var CONFIG_PATH_SHAPE = {
309
316
  float: { statedBudget: {}, connectedAt: {}, alias: {}, methods: {} },
310
317
  x402: { privateKey: {}, network: {} },
311
- tempo: { method: {}, accountId: {}, apiKey: {} },
318
+ tempo: { method: {}, accountId: {}, apiKey: {}, network: {} },
312
319
  credit: {
313
320
  targetJobs: {},
314
321
  posture: {},
@@ -11,24 +11,31 @@ var StepCache = class _StepCache {
11
11
  }
12
12
  /** Get a cached step result. Throws if not present. */
13
13
  get(id) {
14
- if (!this.cache.has(id)) {
15
- throw new Error(`StepCache: no cached result for step "${id}"`);
16
- }
17
- return this.cache.get(id);
14
+ return this.getRecord(id).value;
15
+ }
16
+ /** Get a cached step record, including replay metadata. Throws if not present. */
17
+ getRecord(id) {
18
+ const record = this.cache.get(id);
19
+ if (record === void 0) throw new Error(`StepCache: no cached result for step "${id}"`);
20
+ return record;
18
21
  }
19
22
  /** Cache a step result. */
20
- set(id, value) {
21
- this.cache.set(id, value);
23
+ set(id, value, costs) {
24
+ this.cache.set(id, {
25
+ id,
26
+ value,
27
+ ...costs !== void 0 && costs.length > 0 ? { costs: costs.map(({ amount, currency }) => ({ amount, currency })) } : {}
28
+ });
22
29
  }
23
30
  /** Export all cached steps for persistence. */
24
31
  serialize() {
25
- return [...this.cache.entries()].map(([id, value]) => ({ id, value }));
32
+ return [...this.cache.values()];
26
33
  }
27
34
  /** Restore a StepCache from persisted records. */
28
35
  static deserialize(records) {
29
36
  const cache = new _StepCache();
30
- for (const { id, value } of records) {
31
- cache.set(id, value);
37
+ for (const { id, value, costs } of records) {
38
+ cache.set(id, value, costs);
32
39
  }
33
40
  return cache;
34
41
  }
@@ -6,6 +6,7 @@ import {
6
6
  import { randomUUID } from "crypto";
7
7
  var ENDPOINTS = {
8
8
  revenue: "/_internal/job-revenue",
9
+ cost: "/_internal/job-cost",
9
10
  deposit: "/_internal/credit-deposit",
10
11
  release: "/_internal/credit-draw-release",
11
12
  drain: "/_internal/credit-drain",
@@ -129,6 +130,10 @@ var RevenueReporter = class _RevenueReporter {
129
130
  async report(payload) {
130
131
  return this.enqueue("revenue", payload, payload.jobId);
131
132
  }
133
+ /** Persist a zero-revenue job cost and attempt immediate delivery. */
134
+ async reportCost(payload) {
135
+ return this.enqueue("cost", payload, payload.jobId);
136
+ }
132
137
  /**
133
138
  * Join a credit deposit to the caller-owned rail transaction.
134
139
  *
@@ -19,7 +19,7 @@ import {
19
19
 
20
20
  // src/sdk/server/memory-credit-ledger.ts
21
21
  import { randomUUID } from "crypto";
22
- var MemoryCreditLedger = class {
22
+ var MemoryCreditLedger = class _MemoryCreditLedger {
23
23
  /**
24
24
  * Per-process and lost on restart, so a DVM backed by this ledger never
25
25
  * advertises the funding menu outside devMode (internal-review) — funding on one
@@ -32,6 +32,16 @@ var MemoryCreditLedger = class {
32
32
  tempoLosses = /* @__PURE__ */ new Map();
33
33
  x402Losses = /* @__PURE__ */ new Map();
34
34
  x402Settlements;
35
+ /** Internal disposable transaction snapshot. The caller must serialize every ledger access. */
36
+ forkForTransaction() {
37
+ const fork = new _MemoryCreditLedger();
38
+ for (const name of ["credits", "fundings", "invoices", "tempoLosses", "x402Losses"]) {
39
+ const target = fork[name];
40
+ for (const [key, value] of this[name]) target.set(key, structuredClone(value));
41
+ }
42
+ fork.x402Settlements = this.x402Settlements;
43
+ return fork;
44
+ }
35
45
  /** See {@link CreditLedger.useX402SettlementGate} — same contract (internal-review). */
36
46
  useX402SettlementGate(gate) {
37
47
  this.x402Settlements = gate;
@@ -29,6 +29,34 @@ function isX402SettlementReconciliationReason(value) {
29
29
  return value === "settlement_not_on_chain" || value === "chain_unreachable" || value === "settlement_unbookmarked" || value === "facilitator_channel_state_unavailable";
30
30
  }
31
31
 
32
+ // src/lib/mints.ts
33
+ var RECOMMENDED_MINTS = [
34
+ {
35
+ name: "Coinos",
36
+ description: "Primary Cashu mint accepted by dvmkit DVMs",
37
+ url: "https://mint.coinos.io"
38
+ },
39
+ {
40
+ name: "Voltz Mint",
41
+ description: "Fallback Cashu mint accepted by dvmkit DVMs",
42
+ url: "https://mint.lnvoltz.com"
43
+ }
44
+ ];
45
+ function isTestMintUrl(url) {
46
+ let host;
47
+ try {
48
+ host = new URL(url).hostname.toLowerCase();
49
+ } catch {
50
+ return false;
51
+ }
52
+ if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
53
+ if (LOOPBACK_HOSTS.has(host)) return true;
54
+ if (host.endsWith(".localhost")) return true;
55
+ return TEST_MINT_HOSTS.has(host);
56
+ }
57
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
58
+ var TEST_MINT_HOSTS = /* @__PURE__ */ new Set(["testnut.cashu.space", "dvmkit-testmint.fly.dev"]);
59
+
32
60
  // src/lib/secret-backup.ts
33
61
  import { existsSync, readFileSync, writeFileSync } from "fs";
34
62
  function backupSecretFile(file, nextContent) {
@@ -162,7 +190,7 @@ var LOCK_FILE = `${WALLET_FILE}.lock`;
162
190
  var DEFAULT_LOCK_STALE_MS = 3e4;
163
191
  var DEFAULT_LOCK_RETRIES = 30;
164
192
  var DEFAULT_LOCK_RETRY_INTERVAL_MS = 1e3;
165
- var WALLET_VERSION = 8;
193
+ var WALLET_VERSION = 9;
166
194
  var RECENT_SPENDS_CAP = 10;
167
195
  var DEFAULT_AGENT_MINT_URL = "https://mint.coinos.io";
168
196
  function loadAgentWallet() {
@@ -187,37 +215,54 @@ function loadAgentWallet() {
187
215
  }
188
216
  const version = parsed.version;
189
217
  if (version === 1) {
190
- return migrateV7ToV8(
191
- migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(migrateV1ToV4(parsed))))
218
+ return migrateV8ToV9(
219
+ migrateV7ToV8(
220
+ migrateV6ToV7(
221
+ migrateV5ToV6(migrateV4ToV5(migrateV1ToV4(parsed)))
222
+ )
223
+ )
192
224
  );
193
225
  }
194
226
  if (version === 2) {
195
- return migrateV7ToV8(
196
- migrateV6ToV7(
197
- migrateV5ToV6(
198
- migrateV4ToV5(migrateV3ToV4(migrateV2ToV3(parsed)))
227
+ return migrateV8ToV9(
228
+ migrateV7ToV8(
229
+ migrateV6ToV7(
230
+ migrateV5ToV6(
231
+ migrateV4ToV5(migrateV3ToV4(migrateV2ToV3(parsed)))
232
+ )
199
233
  )
200
234
  )
201
235
  );
202
236
  }
203
237
  if (version === 3) {
204
- return migrateV7ToV8(
205
- migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(migrateV3ToV4(parsed))))
238
+ return migrateV8ToV9(
239
+ migrateV7ToV8(
240
+ migrateV6ToV7(
241
+ migrateV5ToV6(migrateV4ToV5(migrateV3ToV4(parsed)))
242
+ )
243
+ )
206
244
  );
207
245
  }
208
246
  if (version === 4) {
209
- return migrateV7ToV8(
210
- migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(parsed)))
247
+ return migrateV8ToV9(
248
+ migrateV7ToV8(
249
+ migrateV6ToV7(migrateV5ToV6(migrateV4ToV5(parsed)))
250
+ )
211
251
  );
212
252
  }
213
253
  if (version === 5) {
214
- return migrateV7ToV8(migrateV6ToV7(migrateV5ToV6(parsed)));
254
+ return migrateV8ToV9(
255
+ migrateV7ToV8(migrateV6ToV7(migrateV5ToV6(parsed)))
256
+ );
215
257
  }
216
258
  if (version === 6) {
217
- return migrateV7ToV8(migrateV6ToV7(parsed));
259
+ return migrateV8ToV9(migrateV7ToV8(migrateV6ToV7(parsed)));
218
260
  }
219
261
  if (version === 7) {
220
- return migrateV7ToV8(parsed);
262
+ return migrateV8ToV9(migrateV7ToV8(parsed));
263
+ }
264
+ if (version === 8) {
265
+ return migrateV8ToV9(parsed);
221
266
  }
222
267
  if (version !== WALLET_VERSION) {
223
268
  throw new DvmError(
@@ -366,6 +411,22 @@ function migrateV6ToV7(parsed) {
366
411
  };
367
412
  }
368
413
  function migrateV7ToV8(parsed) {
414
+ return {
415
+ version: 8,
416
+ lock_privkey: String(parsed.lock_privkey),
417
+ lock_pubkey: String(parsed.lock_pubkey),
418
+ mnemonic_fingerprint: typeof parsed.mnemonic_fingerprint === "string" ? parsed.mnemonic_fingerprint : null,
419
+ mints: Array.isArray(parsed.mints) ? parsed.mints : [],
420
+ proofs: Array.isArray(parsed.proofs) ? parsed.proofs : [],
421
+ nut13_counters: isCounterMap(parsed.nut13_counters) ? parsed.nut13_counters : {},
422
+ pending_mints: Array.isArray(parsed.pending_mints) ? parsed.pending_mints : [],
423
+ pending_melts: Array.isArray(parsed.pending_melts) ? parsed.pending_melts : [],
424
+ pending_submissions: Array.isArray(parsed.pending_submissions) ? parsed.pending_submissions : [],
425
+ recent_spends: Array.isArray(parsed.recent_spends) ? parsed.recent_spends : [],
426
+ created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
427
+ };
428
+ }
429
+ function migrateV8ToV9(parsed) {
369
430
  return {
370
431
  version: WALLET_VERSION,
371
432
  lock_privkey: String(parsed.lock_privkey),
@@ -1633,6 +1694,8 @@ export {
1633
1694
  verifyCashuToken,
1634
1695
  normaliseMintUrl,
1635
1696
  TEMPO_SESSION_UPDATED_REASON,
1697
+ RECOMMENDED_MINTS,
1698
+ isTestMintUrl,
1636
1699
  backupSecretFile,
1637
1700
  readAgentMnemonic,
1638
1701
  assertAgentMnemonicReplaceable,