@dvmkit/sdk 0.1.2-rc.7 → 0.1.4-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 (45) hide show
  1. package/README.md +121 -8
  2. package/dist/chunk-BIFLRKMO.js +87 -0
  3. package/dist/chunk-BQ2NMWKE.js +160 -0
  4. package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
  5. package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
  6. package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
  7. package/dist/chunk-E4EVGPDX.js +391 -0
  8. package/dist/chunk-EDDYHZ6W.js +1010 -0
  9. package/dist/{chunk-2ABMGUDS.js → chunk-EVBK675R.js} +88 -10
  10. package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
  11. package/dist/chunk-KXZUCCEY.js +142 -0
  12. package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
  13. package/dist/{chunk-N4VTG3KH.js → chunk-QK3VJNCK.js} +930 -3011
  14. package/dist/chunk-RW5LP57K.js +44 -0
  15. package/dist/{chunk-6JZIX5WW.js → chunk-SSSZUVWM.js} +178 -8
  16. package/dist/{chunk-TKA6ZP4M.js → chunk-U6M3ATSG.js} +56 -426
  17. package/dist/chunk-VRQDX5P4.js +1742 -0
  18. package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
  19. package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
  20. package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C1ezIFlJ.d.ts} +1699 -1941
  21. package/dist/{fx-C-liI3oY.d.ts → fx-C6dl2LVI.d.ts} +1 -1
  22. package/dist/index.d.ts +7 -6
  23. package/dist/index.js +8 -4
  24. package/dist/internal/caller.d.ts +10441 -0
  25. package/dist/internal/caller.js +10078 -0
  26. package/dist/internal/index.d.ts +2 -10816
  27. package/dist/internal/index.js +2 -10130
  28. package/dist/internal/server.d.ts +404 -0
  29. package/dist/internal/server.js +202 -0
  30. package/dist/job-store-DHnW4Cg_.d.ts +591 -0
  31. package/dist/lightning-backend-Ci1nogk_.d.ts +367 -0
  32. package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
  33. package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-3RAXMNSY.js} +1 -1
  34. package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-ASZ7SHHH.js} +1 -1
  35. package/dist/server/index.d.ts +41 -11
  36. package/dist/server/index.js +93 -69
  37. package/dist/{job-store-C53VQ5uu.d.ts → step-cache-BLPZNizw.d.ts} +176 -585
  38. package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
  39. package/dist/testing/index.d.ts +25 -3
  40. package/dist/testing/index.js +36 -3
  41. package/dist/{usd-gLcJB1ps.d.ts → usd-BgOfZlk6.d.ts} +1 -1
  42. package/dist/wallet-CJC8lwxx.d.ts +29 -0
  43. package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
  44. package/package.json +11 -5
  45. package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
package/README.md CHANGED
@@ -2,31 +2,144 @@
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
+
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 when no Cashu mint 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
+ ## Production next steps
80
+
81
+ - Use Postgres through `DATABASE_URL`; production boot refuses an in-memory job store unless you deliberately supply another durable implementation.
82
+ - 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.
83
+ - 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.
84
+ - 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.
85
+ - 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.
86
+
87
+ 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.
88
+
89
+ ## Repository development
6
90
 
7
91
  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.
8
92
 
9
93
  `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
94
 
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).
95
+ The public surface is exactly `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`.
12
96
 
13
97
  ## Compatibility and versioning
14
98
 
15
99
  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
100
 
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.
101
+ 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.
102
+
103
+ 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).
18
104
 
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).
105
+ ## Job credentials
106
+
107
+ An unauthenticated `POST /v1/job` returns a one-time `job_token` alongside the `job_id`. Callers must send it as the `X-Job-Token` header on subsequent status reads, message reads (JSON or SSE), message writes, and cancellation. A request with no credential gets a 401 `job_credential_required`; a wrong credential remains the opaque 404 `Job not found` response.
108
+
109
+ When a caller has lost its local job record, the standalone `@dvmkit/dvm-cli` can use the provider endpoint and token explicitly. Prefer environment variables so the bearer token does not enter shell history or the process list:
110
+
111
+ ```sh
112
+ DVM_JOB_ENDPOINT=https://provider.example \
113
+ DVM_JOB_TOKEN=<64-character-hex-token> \
114
+ dvm status <job-id>
115
+ ```
116
+
117
+ The same values are accepted as `--endpoint <url>` and `--job-token <hex>`. These overrides are request-scoped and are not saved; `dvm pay` still requires the local pending request and spending policy, so an endpoint and token alone cannot authorize a payment.
20
118
 
21
119
  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.
22
120
 
23
- ## Internal entry point
121
+ ## Per-job costs
122
+
123
+ 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.
124
+
125
+ ```ts
126
+ const transcript = await ctx.step("transcribe", async () => {
127
+ const result = await whisper(audio);
128
+ ctx.cost({ amount: result.seconds * 0.0001, currency: "usd" });
129
+ return result;
130
+ });
131
+ ctx.artifact({ mime_type: "text/plain", data: transcript.text });
132
+ ```
133
+
134
+ `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.
24
135
 
25
- `@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.
136
+ Three things worth knowing:
26
137
 
27
- 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.
138
+ - **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.
139
+ - **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.
140
+ - **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.
28
141
 
29
- 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.
142
+ 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.
30
143
 
31
144
  ## Contributing
32
145
 
@@ -0,0 +1,87 @@
1
+ import {
2
+ createX402FacilitatorAuthHeaders,
3
+ settleWithFacilitator,
4
+ verifyWithFacilitator
5
+ } from "./chunk-BQ2NMWKE.js";
6
+ import {
7
+ buildPaymentRequirements,
8
+ decodePayment,
9
+ encodeSettleResponseHeader,
10
+ exactEvmAuthorization
11
+ } from "./chunk-Z4BNLUZF.js";
12
+ import {
13
+ usdcToMsats
14
+ } from "./chunk-5URG56JJ.js";
15
+ import {
16
+ callerLoggers
17
+ } from "./chunk-66HGCPBU.js";
18
+
19
+ // src/lib/x402/verify.ts
20
+ async function verifyX402Payment(paymentHeader, config, requiredUsdcMicro, resource, btcUsdRate, exactVersions = { v1: true, v2: true }) {
21
+ return callerLoggers.x402.span(
22
+ "x402.verify_payment",
23
+ {
24
+ resource,
25
+ required_usdc_micro: Number(requiredUsdcMicro),
26
+ facilitator_url: config.facilitator
27
+ },
28
+ async () => {
29
+ let payload;
30
+ try {
31
+ payload = decodePayment(paymentHeader);
32
+ } catch {
33
+ return { verified: false, amountMsats: 0, invalidReason: "decode_failed" };
34
+ }
35
+ if (payload.x402Version === 1 && !exactVersions.v1 || payload.x402Version === 2 && !exactVersions.v2) {
36
+ return { verified: false, amountMsats: 0, invalidReason: "unsupported_version" };
37
+ }
38
+ const requirementOpts = { x402Config: config, requiredUsdcMicro, resource };
39
+ const requirements = payload.x402Version === 1 ? buildPaymentRequirements({ ...requirementOpts, version: 1 }) : buildPaymentRequirements({ ...requirementOpts, version: 2 });
40
+ const createAuthHeaders = createX402FacilitatorAuthHeaders(config);
41
+ const verifyRes = await verifyWithFacilitator(
42
+ payload,
43
+ requirements,
44
+ config,
45
+ createAuthHeaders
46
+ );
47
+ if (!verifyRes.isValid) {
48
+ return {
49
+ verified: false,
50
+ amountMsats: 0,
51
+ invalidReason: verifyRes.invalidReason ?? "verify_failed"
52
+ };
53
+ }
54
+ const settleRes = await settleWithFacilitator(
55
+ payload,
56
+ requirements,
57
+ config,
58
+ createAuthHeaders
59
+ );
60
+ if (!settleRes.success) {
61
+ return {
62
+ verified: false,
63
+ amountMsats: 0,
64
+ invalidReason: settleRes.errorReason ?? "settle_failed"
65
+ };
66
+ }
67
+ const amountUsdcMicro = Number.parseInt(exactEvmAuthorization(payload).value, 10);
68
+ const amountMsats = btcUsdRate > 0 ? usdcToMsats(amountUsdcMicro / 1e6, btcUsdRate) : 0;
69
+ callerLoggers.x402.info("x402.verify_payment.result", {
70
+ verified: true,
71
+ tx_hash: settleRes.transaction,
72
+ amount_msats: amountMsats
73
+ });
74
+ return {
75
+ verified: true,
76
+ amountMsats,
77
+ amountUsdcMicro,
78
+ txHash: settleRes.transaction,
79
+ settleResponseHeader: encodeSettleResponseHeader(settleRes)
80
+ };
81
+ }
82
+ );
83
+ }
84
+
85
+ export {
86
+ verifyX402Payment
87
+ };
@@ -0,0 +1,160 @@
1
+ import {
2
+ X402_DEFAULT_FACILITATOR
3
+ } from "./chunk-Z4BNLUZF.js";
4
+ import {
5
+ callerLoggers
6
+ } from "./chunk-66HGCPBU.js";
7
+
8
+ // src/lib/x402/facilitator.ts
9
+ import { createPrivateKey, randomBytes } from "crypto";
10
+ import { HTTPFacilitatorClient } from "@x402/core/server";
11
+ import { importJWK, importPKCS8, SignJWT } from "jose";
12
+ var AUTH_HEADER_TTL_MS = 6e4;
13
+ var AUTH_TOKEN_TTL_SECONDS = 120;
14
+ var FACILITATOR_PATHS = {
15
+ verify: "POST",
16
+ settle: "POST",
17
+ supported: "GET"
18
+ };
19
+ function createX402FacilitatorAuthHeaders(config) {
20
+ const auth = config.facilitatorAuth;
21
+ if (!auth) return void 0;
22
+ const facilitator = new URL(config.facilitator ?? X402_DEFAULT_FACILITATOR);
23
+ const basePath = facilitator.pathname.replace(/\/+$/, "");
24
+ let cached;
25
+ return async () => {
26
+ const nowMs = Date.now();
27
+ if (cached && nowMs < cached.refreshAtMs) return cached.headers;
28
+ const entries = await Promise.all(
29
+ Object.entries(FACILITATOR_PATHS).map(async ([path, method]) => {
30
+ const requestPath = `${basePath}/${path}`;
31
+ const jwt = await generateCdpJwt(
32
+ auth.keyId,
33
+ auth.keySecret,
34
+ `${method} ${facilitator.host}${requestPath}`
35
+ );
36
+ return [path, { Authorization: `Bearer ${jwt}` }];
37
+ })
38
+ );
39
+ const headers = Object.fromEntries(entries);
40
+ cached = { headers, refreshAtMs: nowMs + AUTH_HEADER_TTL_MS };
41
+ return headers;
42
+ };
43
+ }
44
+ function createX402FacilitatorClient(config, opts = {}) {
45
+ return new HTTPFacilitatorClient({
46
+ url: config.facilitator ?? X402_DEFAULT_FACILITATOR,
47
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
48
+ ...config.facilitatorAuth ? { createAuthHeaders: createX402FacilitatorAuthHeaders(config) } : {}
49
+ });
50
+ }
51
+ async function generateCdpJwt(keyId, keySecret, uri) {
52
+ const now = Math.floor(Date.now() / 1e3);
53
+ const nonce = randomBytes(16).toString("hex");
54
+ const claims = { sub: keyId, iss: "cdp", uris: [uri] };
55
+ try {
56
+ const ecKey = await importPKCS8(normalizeEs256KeySecret(keySecret), "ES256");
57
+ return await new SignJWT(claims).setProtectedHeader({ alg: "ES256", kid: keyId, typ: "JWT", nonce }).setIssuedAt(now).setNotBefore(now).setExpirationTime(now + AUTH_TOKEN_TTL_SECONDS).sign(ecKey);
58
+ } catch {
59
+ }
60
+ const decoded = Buffer.from(keySecret, "base64");
61
+ if (decoded.length !== 64) {
62
+ throw new Error("Invalid CDP key secret: expected an ES256 PEM or base64 Ed25519 key");
63
+ }
64
+ const jwk = {
65
+ kty: "OKP",
66
+ crv: "Ed25519",
67
+ d: decoded.subarray(0, 32).toString("base64url"),
68
+ x: decoded.subarray(32).toString("base64url")
69
+ };
70
+ const edKey = await importJWK(jwk, "EdDSA");
71
+ return new SignJWT(claims).setProtectedHeader({ alg: "EdDSA", kid: keyId, typ: "JWT", nonce }).setIssuedAt(now).setNotBefore(now).setExpirationTime(now + AUTH_TOKEN_TTL_SECONDS).sign(edKey);
72
+ }
73
+ function normalizeEs256KeySecret(keySecret) {
74
+ if (!keySecret.includes("-----BEGIN EC PRIVATE KEY-----")) return keySecret;
75
+ return createPrivateKey(keySecret).export({ format: "pem", type: "pkcs8" });
76
+ }
77
+
78
+ // src/lib/x402/facilitator-request.ts
79
+ async function verifyWithFacilitator(payload, requirements, config = {}, createAuthHeaders = createX402FacilitatorAuthHeaders(config)) {
80
+ const facilitator = config.facilitator ?? X402_DEFAULT_FACILITATOR;
81
+ const url = facilitator.replace(/\/$/, "") + "/verify";
82
+ return callerLoggers.x402.span(
83
+ "x402.verify_with_facilitator",
84
+ { facilitator_url: url },
85
+ async () => {
86
+ const authHeaders = (await createAuthHeaders?.())?.verify ?? {};
87
+ const res = await fetch(url, {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json", ...authHeaders },
90
+ body: JSON.stringify({
91
+ x402Version: payload.x402Version,
92
+ paymentPayload: payload,
93
+ paymentRequirements: requirements
94
+ })
95
+ });
96
+ if (!res.ok) {
97
+ const rejection = await readFacilitatorRejection(res);
98
+ if (rejection?.isValid === false && typeof rejection.invalidReason === "string" && rejection.invalidReason.length > 0) {
99
+ return rejection;
100
+ }
101
+ return { isValid: false, invalidReason: `facilitator_status_${res.status}` };
102
+ }
103
+ const data = await res.json();
104
+ callerLoggers.x402.info("x402.verify_with_facilitator.result", { is_valid: data.isValid });
105
+ return data;
106
+ }
107
+ );
108
+ }
109
+ async function settleWithFacilitator(payload, requirements, config = {}, createAuthHeaders = createX402FacilitatorAuthHeaders(config)) {
110
+ const facilitator = config.facilitator ?? X402_DEFAULT_FACILITATOR;
111
+ const url = facilitator.replace(/\/$/, "") + "/settle";
112
+ return callerLoggers.x402.span(
113
+ "x402.settle_with_facilitator",
114
+ { facilitator_url: url },
115
+ async () => {
116
+ const authHeaders = (await createAuthHeaders?.())?.settle ?? {};
117
+ const res = await fetch(url, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json", ...authHeaders },
120
+ body: JSON.stringify({
121
+ x402Version: payload.x402Version,
122
+ paymentPayload: payload,
123
+ paymentRequirements: requirements
124
+ })
125
+ });
126
+ if (!res.ok) {
127
+ const rejection = await readFacilitatorRejection(res);
128
+ if (rejection?.success === false && typeof rejection.errorReason === "string" && rejection.errorReason.length > 0) {
129
+ return rejection;
130
+ }
131
+ return { success: false, errorReason: `facilitator_status_${res.status}` };
132
+ }
133
+ const data = await res.json();
134
+ callerLoggers.x402.info("x402.settle_with_facilitator.result", {
135
+ success: data.success,
136
+ tx_hash: data.transaction
137
+ });
138
+ return data;
139
+ }
140
+ );
141
+ }
142
+ async function readFacilitatorRejection(res) {
143
+ if (res.status !== 400) return null;
144
+ try {
145
+ const body = await res.json();
146
+ return isRecord(body) ? body : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+ function isRecord(value) {
152
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
153
+ }
154
+
155
+ export {
156
+ createX402FacilitatorAuthHeaders,
157
+ createX402FacilitatorClient,
158
+ verifyWithFacilitator,
159
+ settleWithFacilitator
160
+ };
@@ -2,8 +2,9 @@ import {
2
2
  DEFAULT_CREDIT_MAX,
3
3
  DEFAULT_CREDIT_MIN,
4
4
  DEFAULT_CREDIT_TTL_SECONDS,
5
+ DEFAULT_JOB_RETENTION_DAYS,
5
6
  isZodSchema
6
- } from "./chunk-P4RUVDU7.js";
7
+ } from "./chunk-KXZUCCEY.js";
7
8
  import {
8
9
  InvalidCurrencyError,
9
10
  validateCurrency
@@ -20,6 +21,7 @@ function configureDVM(config) {
20
21
  const tags = collectTags(config);
21
22
  const currency = resolvePricingCurrency(config.currency);
22
23
  const capabilities = buildCapabilityMap(config, currency);
24
+ const jobRetentionDays = resolveJobRetentionDays(config.jobRetentionDays);
23
25
  const onlyCapability = singleCapabilityEntry(capabilities);
24
26
  const descriptor = {
25
27
  name: config.name,
@@ -31,6 +33,7 @@ function configureDVM(config) {
31
33
  state: onlyCapability?.state ?? {},
32
34
  idleTimeout: config.idleTimeout ?? 3600,
33
35
  processingWatchdog: config.processingWatchdog,
36
+ jobRetentionDays,
34
37
  config,
35
38
  capabilities,
36
39
  paymentMethods: config.paymentMethods,
@@ -42,6 +45,15 @@ function configureDVM(config) {
42
45
  CONFIGURED_DVM_DESCRIPTORS.add(descriptor);
43
46
  return Object.freeze(descriptor);
44
47
  }
48
+ function resolveJobRetentionDays(raw) {
49
+ const days = raw ?? DEFAULT_JOB_RETENTION_DAYS;
50
+ if (!Number.isSafeInteger(days) || days < 0) {
51
+ throw new Error(
52
+ `configureDVM: jobRetentionDays must be a non-negative whole number of days; received ${JSON.stringify(raw)}`
53
+ );
54
+ }
55
+ return days;
56
+ }
45
57
  function isConfiguredDVMDescriptor(value) {
46
58
  return typeof value === "object" && value !== null && CONFIGURED_DVM_DESCRIPTORS.has(value);
47
59
  }
@@ -18,20 +18,14 @@ import {
18
18
  assertSupportedX402Network,
19
19
  buildPaymentRequirements,
20
20
  chainIdFromCaip2,
21
- createX402FacilitatorAuthHeaders,
22
- decodePayment,
23
21
  encodePayment,
24
- encodeSettleResponseHeader,
25
- exactEvmAuthorization,
26
- settleWithFacilitator,
27
22
  usdcContractFor,
28
23
  usdcDomainNameFor,
29
24
  usdcDomainVersionFor,
30
- verifyWithFacilitator,
31
25
  x402NetworkByCaip2,
32
26
  x402NetworkToCaip2,
33
27
  x402SupportedNetworksHint
34
- } from "./chunk-JGGI65I3.js";
28
+ } from "./chunk-Z4BNLUZF.js";
35
29
  import {
36
30
  costAnchor,
37
31
  fetchBtcUsdRate,
@@ -48,9 +42,6 @@ import {
48
42
  resolveRpcHttpTransportOptions,
49
43
  resolveRpcOverride
50
44
  } from "./chunk-MKI6OVW4.js";
51
- import {
52
- callerLoggers
53
- } from "./chunk-66HGCPBU.js";
54
45
  import {
55
46
  redactUrl,
56
47
  redactUrlsInText
@@ -77,9 +68,6 @@ function x402WrongAssetHint(asset) {
77
68
  return `This rail reads only ${asset.label} (${asset.address}) on ${asset.networkLabel}. ${wrongToken} USDC on a different chain is invisible here too \u2014 this wallet reads ${asset.networkLabel} alone. If you funded the wrong one, swap it to ${asset.label} on ${asset.networkLabel}, or bridge the funds across to ${asset.networkLabel}; either way they land where this rail can spend them.`;
78
69
  }
79
70
 
80
- // src/lib/x402/client.ts
81
- import { randomBytes } from "crypto";
82
-
83
71
  // src/lib/btc-rate.ts
84
72
  async function resolveBtcUsdRate(purpose) {
85
73
  try {
@@ -783,6 +771,7 @@ async function getX402Balance(wallet) {
783
771
  }
784
772
 
785
773
  // src/lib/x402/client.ts
774
+ import { randomBytes } from "crypto";
786
775
  async function tryX402Payment(request, wallet) {
787
776
  const selection = selectPaymentRequirement(request);
788
777
  let networkSlug;
@@ -990,72 +979,6 @@ async function priceableRate(btcRate) {
990
979
  }
991
980
  }
992
981
 
993
- // src/lib/x402/verify.ts
994
- async function verifyX402Payment(paymentHeader, config, requiredUsdcMicro, resource, btcUsdRate, exactVersions = { v1: true, v2: true }) {
995
- return callerLoggers.x402.span(
996
- "x402.verify_payment",
997
- {
998
- resource,
999
- required_usdc_micro: Number(requiredUsdcMicro),
1000
- facilitator_url: config.facilitator
1001
- },
1002
- async () => {
1003
- let payload;
1004
- try {
1005
- payload = decodePayment(paymentHeader);
1006
- } catch {
1007
- return { verified: false, amountMsats: 0, invalidReason: "decode_failed" };
1008
- }
1009
- if (payload.x402Version === 1 && !exactVersions.v1 || payload.x402Version === 2 && !exactVersions.v2) {
1010
- return { verified: false, amountMsats: 0, invalidReason: "unsupported_version" };
1011
- }
1012
- const requirementOpts = { x402Config: config, requiredUsdcMicro, resource };
1013
- const requirements = payload.x402Version === 1 ? buildPaymentRequirements({ ...requirementOpts, version: 1 }) : buildPaymentRequirements({ ...requirementOpts, version: 2 });
1014
- const createAuthHeaders = createX402FacilitatorAuthHeaders(config);
1015
- const verifyRes = await verifyWithFacilitator(
1016
- payload,
1017
- requirements,
1018
- config,
1019
- createAuthHeaders
1020
- );
1021
- if (!verifyRes.isValid) {
1022
- return {
1023
- verified: false,
1024
- amountMsats: 0,
1025
- invalidReason: verifyRes.invalidReason ?? "verify_failed"
1026
- };
1027
- }
1028
- const settleRes = await settleWithFacilitator(
1029
- payload,
1030
- requirements,
1031
- config,
1032
- createAuthHeaders
1033
- );
1034
- if (!settleRes.success) {
1035
- return {
1036
- verified: false,
1037
- amountMsats: 0,
1038
- invalidReason: settleRes.errorReason ?? "settle_failed"
1039
- };
1040
- }
1041
- const amountUsdcMicro = Number.parseInt(exactEvmAuthorization(payload).value, 10);
1042
- const amountMsats = usdcToMsats(amountUsdcMicro / 1e6, btcUsdRate);
1043
- callerLoggers.x402.info("x402.verify_payment.result", {
1044
- verified: true,
1045
- tx_hash: settleRes.transaction,
1046
- amount_msats: amountMsats
1047
- });
1048
- return {
1049
- verified: true,
1050
- amountMsats,
1051
- amountUsdcMicro,
1052
- txHash: settleRes.transaction,
1053
- settleResponseHeader: encodeSettleResponseHeader(settleRes)
1054
- };
1055
- }
1056
- );
1057
- }
1058
-
1059
982
  export {
1060
983
  resolveX402Asset,
1061
984
  x402WrongAssetHint,
@@ -1099,6 +1022,5 @@ export {
1099
1022
  removeX402Wallet,
1100
1023
  getX402Balance,
1101
1024
  tryX402Payment,
1102
- isSignableExactRequirement,
1103
- verifyX402Payment
1025
+ isSignableExactRequirement
1104
1026
  };
@@ -15,7 +15,7 @@ import {
15
15
  lotOwedSats,
16
16
  netOwedSats,
17
17
  x402SettlementPending
18
- } from "./chunk-LWUR4CGG.js";
18
+ } from "./chunk-MLRCSJYX.js";
19
19
 
20
20
  // src/sdk/server/memory-credit-ledger.ts
21
21
  import { randomUUID } from "crypto";
@@ -624,6 +624,18 @@ var MemoryCreditLedger = class {
624
624
  }
625
625
  return Promise.resolve(matches.length === 1 ? matches[0] : void 0);
626
626
  }
627
+ listDrawsByJobId(jobId) {
628
+ const matches = [];
629
+ for (const credit of this.credits.values()) {
630
+ for (const draw of credit.draws.values()) {
631
+ if (draw.jobId === jobId) matches.push(drawRecord(draw, credit));
632
+ }
633
+ }
634
+ matches.sort(
635
+ (a, b) => a.createdAt - b.createdAt || a.creditId.localeCompare(b.creditId) || a.ledgerSeq - b.ledgerSeq
636
+ );
637
+ return Promise.resolve(matches);
638
+ }
627
639
  listPendingDraws(creditId) {
628
640
  const credit = this.credits.get(creditId);
629
641
  if (!credit) return Promise.resolve([]);