@dvmkit/sdk 0.1.2-rc.7 → 0.1.3-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.
- package/README.md +14 -0
- package/dist/chunk-2K7E3N2D.js +1709 -0
- package/dist/{chunk-N4VTG3KH.js → chunk-3ZHMQCYP.js} +645 -3027
- package/dist/{chunk-TKA6ZP4M.js → chunk-4B56DEEV.js} +50 -426
- package/dist/chunk-BIFLRKMO.js +87 -0
- package/dist/chunk-BQ2NMWKE.js +160 -0
- package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
- package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
- package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
- package/dist/chunk-EPNDZ5DH.js +1010 -0
- package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
- package/dist/chunk-KXZUCCEY.js +142 -0
- package/dist/{chunk-6JZIX5WW.js → chunk-LLXV32HA.js} +82 -5
- package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
- package/dist/{chunk-2ABMGUDS.js → chunk-NFRM5QYP.js} +83 -10
- package/dist/chunk-RW5LP57K.js +44 -0
- package/dist/chunk-YDIYXGYL.js +384 -0
- package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
- package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
- package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C7zAJElJ.d.ts} +1632 -1946
- package/dist/{fx-C-liI3oY.d.ts → fx-BF_SG2i0.d.ts} +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +8 -4
- package/dist/internal/caller.d.ts +10429 -0
- package/dist/internal/caller.js +9957 -0
- package/dist/internal/index.d.ts +2 -10816
- package/dist/internal/index.js +2 -10130
- package/dist/internal/server.d.ts +404 -0
- package/dist/internal/server.js +202 -0
- package/dist/job-store-Bn23V3QU.d.ts +576 -0
- package/dist/lightning-backend-C04nH94l.d.ts +367 -0
- package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
- package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-TAONYLIF.js} +1 -1
- package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-XXSU5KVB.js} +1 -1
- package/dist/server/index.d.ts +27 -11
- package/dist/server/index.js +93 -69
- package/dist/{job-store-C53VQ5uu.d.ts → step-cache-3cT4Shk0.d.ts} +31 -585
- package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
- package/dist/testing/index.d.ts +16 -4
- package/dist/testing/index.js +7 -3
- package/dist/{usd-gLcJB1ps.d.ts → usd-BnuXoFl5.d.ts} +1 -1
- package/dist/wallet-CJC8lwxx.d.ts +29 -0
- package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
- package/package.json +6 -2
- package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
|
@@ -1,125 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
tempo: false
|
|
9
|
-
};
|
|
10
|
-
var DEFAULT_CREDIT_MIN = "$0.10";
|
|
11
|
-
var DEFAULT_CREDIT_MAX = "$5.00";
|
|
12
|
-
var DEFAULT_CREDIT_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
13
|
-
|
|
14
|
-
// src/lib/cast-auth/canonical-json.ts
|
|
15
|
-
function canonicalize(value) {
|
|
16
|
-
return JSON.stringify(sortDeep(value));
|
|
17
|
-
}
|
|
18
|
-
function canonicaliseForSigning(value) {
|
|
19
|
-
return new TextEncoder().encode(canonicalize(value));
|
|
20
|
-
}
|
|
21
|
-
function sortDeep(value) {
|
|
22
|
-
if (value === null) return null;
|
|
23
|
-
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
24
|
-
if (typeof value === "number") {
|
|
25
|
-
if (!Number.isFinite(value)) {
|
|
26
|
-
throw new TypeError(`canonicalize: non-finite number is not JSON-serialisable`);
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
if (Array.isArray(value)) return value.map(sortDeep);
|
|
31
|
-
if (typeof value === "object") {
|
|
32
|
-
const sorted = {};
|
|
33
|
-
for (const key of Object.keys(value).sort()) {
|
|
34
|
-
const v = value[key];
|
|
35
|
-
if (v === void 0) continue;
|
|
36
|
-
sorted[key] = sortDeep(v);
|
|
37
|
-
}
|
|
38
|
-
return sorted;
|
|
39
|
-
}
|
|
40
|
-
throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// src/lib/cast-auth/verify.ts
|
|
44
|
-
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
45
|
-
function verifyCanonicalSignedRequest(payload, signatureHex, pubkeyHex) {
|
|
46
|
-
if (!/^[0-9a-f]{64}$/i.test(pubkeyHex) || !/^[0-9a-f]{128}$/i.test(signatureHex)) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
const data = canonicaliseForSigning(payload);
|
|
50
|
-
const signature = hexToBytes(signatureHex);
|
|
51
|
-
const pubkey = hexToBytes(pubkeyHex);
|
|
52
|
-
try {
|
|
53
|
-
return schnorr.verify(signature, data, pubkey);
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function hexToBytes(hex) {
|
|
59
|
-
const out = new Uint8Array(hex.length / 2);
|
|
60
|
-
for (let i = 0; i < out.length; i++) {
|
|
61
|
-
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/lib/signed-request-statement.ts
|
|
67
|
-
var SIGNED_REQUEST_AUTH_ID = "secp256k1-schnorr-v2";
|
|
68
|
-
var SIGNED_REQUEST_STATEMENT_VERSION = 2;
|
|
69
|
-
var TEST_DOMAIN_SENTINEL = "__dvmkit_test_domain__";
|
|
70
|
-
function normalizeSignedRequestDomain(domain) {
|
|
71
|
-
return {
|
|
72
|
-
audience: {
|
|
73
|
-
dvm_id: domain.audience.dvm_id,
|
|
74
|
-
builder_pubkey: domain.audience.builder_pubkey.trim().toLowerCase()
|
|
75
|
-
},
|
|
76
|
-
method: domain.method.trim().toUpperCase(),
|
|
77
|
-
path: canonicalRequestPath(domain.path),
|
|
78
|
-
capability: domain.capability
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function signedRequestStatementHeader(domain) {
|
|
82
|
-
const normalized = normalizeSignedRequestDomain(domain);
|
|
83
|
-
return {
|
|
84
|
-
v: SIGNED_REQUEST_STATEMENT_VERSION,
|
|
85
|
-
protocol: "dvmkit",
|
|
86
|
-
protocol_version: 1,
|
|
87
|
-
auth: SIGNED_REQUEST_AUTH_ID,
|
|
88
|
-
audience: normalized.audience,
|
|
89
|
-
method: normalized.method,
|
|
90
|
-
path: normalized.path,
|
|
91
|
-
capability: normalized.capability
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function canonicalSignedRequestStatement(header, input) {
|
|
95
|
-
return canonicaliseForSigning({
|
|
96
|
-
...header,
|
|
97
|
-
input
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
function signedRequestHeaderMatchesDomain(header, domain) {
|
|
101
|
-
if (!header || typeof header !== "object" || Array.isArray(header)) return false;
|
|
102
|
-
const actual = header;
|
|
103
|
-
const expected = signedRequestStatementHeader(domain);
|
|
104
|
-
const audience = actual.audience;
|
|
105
|
-
if (process.env.NODE_ENV === "test" && actual.method === TEST_DOMAIN_SENTINEL.toUpperCase() && actual.path === `/${TEST_DOMAIN_SENTINEL}` && actual.capability === TEST_DOMAIN_SENTINEL && audience?.dvm_id === TEST_DOMAIN_SENTINEL) {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
return actual.v === expected.v && actual.protocol === expected.protocol && actual.protocol_version === expected.protocol_version && actual.auth === expected.auth && actual.method === expected.method && actual.path === expected.path && actual.capability === expected.capability && !!audience && typeof audience === "object" && audience.dvm_id === expected.audience.dvm_id && audience.builder_pubkey === expected.audience.builder_pubkey;
|
|
109
|
-
}
|
|
110
|
-
function testOnlySignedRequestDomain() {
|
|
111
|
-
if (process.env.NODE_ENV !== "test") return void 0;
|
|
112
|
-
return {
|
|
113
|
-
audience: { dvm_id: TEST_DOMAIN_SENTINEL, builder_pubkey: "00".repeat(32) },
|
|
114
|
-
method: TEST_DOMAIN_SENTINEL,
|
|
115
|
-
path: TEST_DOMAIN_SENTINEL,
|
|
116
|
-
capability: TEST_DOMAIN_SENTINEL
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function canonicalRequestPath(pathOrUrl) {
|
|
120
|
-
const parsed = new URL(pathOrUrl, "https://signed-request.invalid");
|
|
121
|
-
return parsed.pathname.startsWith("/") ? parsed.pathname : `/${parsed.pathname}`;
|
|
122
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
canonicalSignedRequestStatement,
|
|
3
|
+
isZodSchema,
|
|
4
|
+
signedRequestHeaderMatchesDomain,
|
|
5
|
+
signedRequestStatementHeader,
|
|
6
|
+
testOnlySignedRequestDomain
|
|
7
|
+
} from "./chunk-KXZUCCEY.js";
|
|
123
8
|
|
|
124
9
|
// src/sdk/server/zod-error.ts
|
|
125
10
|
function humanizeZodError(err, opts) {
|
|
@@ -207,7 +92,7 @@ function pickHint(issues, schemaUrl) {
|
|
|
207
92
|
}
|
|
208
93
|
|
|
209
94
|
// src/sdk/server/signed-request.ts
|
|
210
|
-
import { schnorr
|
|
95
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
211
96
|
var SIGNED_ENVELOPE_TYPES = {
|
|
212
97
|
pubkey: "string",
|
|
213
98
|
signature: "string",
|
|
@@ -317,7 +202,7 @@ function createSignedRequestVerifier(schema, opts = {}) {
|
|
|
317
202
|
}
|
|
318
203
|
}
|
|
319
204
|
function signRequest(privkey, body, signOpts, domain) {
|
|
320
|
-
const pubkey = bytesToHex(
|
|
205
|
+
const pubkey = bytesToHex(schnorr.getPublicKey(privkey));
|
|
321
206
|
const timestamp = signOpts?.timestamp ?? now();
|
|
322
207
|
const nonce = signOpts?.nonce ?? randomNonce();
|
|
323
208
|
const unsigned = {
|
|
@@ -328,7 +213,7 @@ function createSignedRequestVerifier(schema, opts = {}) {
|
|
|
328
213
|
};
|
|
329
214
|
const authStatement = signedRequestStatementHeader(requiredDomain(domain));
|
|
330
215
|
const signature = bytesToHex(
|
|
331
|
-
|
|
216
|
+
schnorr.sign(canonicalBytes({ ...unsigned, auth_statement: authStatement }, domain), privkey)
|
|
332
217
|
);
|
|
333
218
|
return { ...unsigned, auth_statement: authStatement, signature };
|
|
334
219
|
}
|
|
@@ -370,12 +255,12 @@ function statementHeader(input) {
|
|
|
370
255
|
}
|
|
371
256
|
function verifyBytes(bytes, signatureHex, pubkeyHex) {
|
|
372
257
|
try {
|
|
373
|
-
return
|
|
258
|
+
return schnorr.verify(hexToBytes(signatureHex), bytes, hexToBytes(pubkeyHex));
|
|
374
259
|
} catch {
|
|
375
260
|
return false;
|
|
376
261
|
}
|
|
377
262
|
}
|
|
378
|
-
function
|
|
263
|
+
function hexToBytes(hex) {
|
|
379
264
|
const out = new Uint8Array(hex.length / 2);
|
|
380
265
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
381
266
|
return out;
|
|
@@ -427,20 +312,6 @@ function createDefaultReplayStore() {
|
|
|
427
312
|
var SATURATION_WARN_INTERVAL_SECONDS = 60;
|
|
428
313
|
|
|
429
314
|
export {
|
|
430
|
-
isZodSchema,
|
|
431
|
-
RAIL_REFUNDABLE,
|
|
432
|
-
DEFAULT_CREDIT_MIN,
|
|
433
|
-
DEFAULT_CREDIT_MAX,
|
|
434
|
-
DEFAULT_CREDIT_TTL_SECONDS,
|
|
435
|
-
canonicalize,
|
|
436
|
-
canonicaliseForSigning,
|
|
437
|
-
verifyCanonicalSignedRequest,
|
|
438
|
-
SIGNED_REQUEST_AUTH_ID,
|
|
439
|
-
SIGNED_REQUEST_STATEMENT_VERSION,
|
|
440
|
-
signedRequestStatementHeader,
|
|
441
|
-
canonicalSignedRequestStatement,
|
|
442
|
-
testOnlySignedRequestDomain,
|
|
443
|
-
canonicalRequestPath,
|
|
444
315
|
humanizeZodError,
|
|
445
316
|
humanizeZodIssues,
|
|
446
317
|
zodIssuesOf,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/sdk/types.ts
|
|
2
|
+
function isZodSchema(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && "_zod" in value && "parse" in value && "safeParse" in value;
|
|
4
|
+
}
|
|
5
|
+
var RAIL_REFUNDABLE = {
|
|
6
|
+
cashu: false,
|
|
7
|
+
x402: false,
|
|
8
|
+
tempo: false
|
|
9
|
+
};
|
|
10
|
+
var DEFAULT_CREDIT_MIN = "$0.10";
|
|
11
|
+
var DEFAULT_CREDIT_MAX = "$5.00";
|
|
12
|
+
var DEFAULT_CREDIT_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
13
|
+
var DEFAULT_JOB_RETENTION_DAYS = 90;
|
|
14
|
+
|
|
15
|
+
// src/lib/cast-auth/canonical-json.ts
|
|
16
|
+
function canonicalize(value) {
|
|
17
|
+
return JSON.stringify(sortDeep(value));
|
|
18
|
+
}
|
|
19
|
+
function canonicaliseForSigning(value) {
|
|
20
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
21
|
+
}
|
|
22
|
+
function sortDeep(value) {
|
|
23
|
+
if (value === null) return null;
|
|
24
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
25
|
+
if (typeof value === "number") {
|
|
26
|
+
if (!Number.isFinite(value)) {
|
|
27
|
+
throw new TypeError(`canonicalize: non-finite number is not JSON-serialisable`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
const sorted = {};
|
|
34
|
+
for (const key of Object.keys(value).sort()) {
|
|
35
|
+
const v = value[key];
|
|
36
|
+
if (v === void 0) continue;
|
|
37
|
+
sorted[key] = sortDeep(v);
|
|
38
|
+
}
|
|
39
|
+
return sorted;
|
|
40
|
+
}
|
|
41
|
+
throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/lib/cast-auth/verify.ts
|
|
45
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
46
|
+
function verifyCanonicalSignedRequest(payload, signatureHex, pubkeyHex) {
|
|
47
|
+
if (!/^[0-9a-f]{64}$/i.test(pubkeyHex) || !/^[0-9a-f]{128}$/i.test(signatureHex)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const data = canonicaliseForSigning(payload);
|
|
51
|
+
const signature = hexToBytes(signatureHex);
|
|
52
|
+
const pubkey = hexToBytes(pubkeyHex);
|
|
53
|
+
try {
|
|
54
|
+
return schnorr.verify(signature, data, pubkey);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function hexToBytes(hex) {
|
|
60
|
+
const out = new Uint8Array(hex.length / 2);
|
|
61
|
+
for (let i = 0; i < out.length; i++) {
|
|
62
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/lib/signed-request-statement.ts
|
|
68
|
+
var SIGNED_REQUEST_AUTH_ID = "secp256k1-schnorr-v2";
|
|
69
|
+
var SIGNED_REQUEST_STATEMENT_VERSION = 2;
|
|
70
|
+
var TEST_DOMAIN_SENTINEL = "__dvmkit_test_domain__";
|
|
71
|
+
function normalizeSignedRequestDomain(domain) {
|
|
72
|
+
return {
|
|
73
|
+
audience: {
|
|
74
|
+
dvm_id: domain.audience.dvm_id,
|
|
75
|
+
builder_pubkey: domain.audience.builder_pubkey.trim().toLowerCase()
|
|
76
|
+
},
|
|
77
|
+
method: domain.method.trim().toUpperCase(),
|
|
78
|
+
path: canonicalRequestPath(domain.path),
|
|
79
|
+
capability: domain.capability
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function signedRequestStatementHeader(domain) {
|
|
83
|
+
const normalized = normalizeSignedRequestDomain(domain);
|
|
84
|
+
return {
|
|
85
|
+
v: SIGNED_REQUEST_STATEMENT_VERSION,
|
|
86
|
+
protocol: "dvmkit",
|
|
87
|
+
protocol_version: 1,
|
|
88
|
+
auth: SIGNED_REQUEST_AUTH_ID,
|
|
89
|
+
audience: normalized.audience,
|
|
90
|
+
method: normalized.method,
|
|
91
|
+
path: normalized.path,
|
|
92
|
+
capability: normalized.capability
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function canonicalSignedRequestStatement(header, input) {
|
|
96
|
+
return canonicaliseForSigning({
|
|
97
|
+
...header,
|
|
98
|
+
input
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function signedRequestHeaderMatchesDomain(header, domain) {
|
|
102
|
+
if (!header || typeof header !== "object" || Array.isArray(header)) return false;
|
|
103
|
+
const actual = header;
|
|
104
|
+
const expected = signedRequestStatementHeader(domain);
|
|
105
|
+
const audience = actual.audience;
|
|
106
|
+
if (process.env.NODE_ENV === "test" && actual.method === TEST_DOMAIN_SENTINEL.toUpperCase() && actual.path === `/${TEST_DOMAIN_SENTINEL}` && actual.capability === TEST_DOMAIN_SENTINEL && audience?.dvm_id === TEST_DOMAIN_SENTINEL) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return actual.v === expected.v && actual.protocol === expected.protocol && actual.protocol_version === expected.protocol_version && actual.auth === expected.auth && actual.method === expected.method && actual.path === expected.path && actual.capability === expected.capability && !!audience && typeof audience === "object" && audience.dvm_id === expected.audience.dvm_id && audience.builder_pubkey === expected.audience.builder_pubkey;
|
|
110
|
+
}
|
|
111
|
+
function testOnlySignedRequestDomain() {
|
|
112
|
+
if (process.env.NODE_ENV !== "test") return void 0;
|
|
113
|
+
return {
|
|
114
|
+
audience: { dvm_id: TEST_DOMAIN_SENTINEL, builder_pubkey: "00".repeat(32) },
|
|
115
|
+
method: TEST_DOMAIN_SENTINEL,
|
|
116
|
+
path: TEST_DOMAIN_SENTINEL,
|
|
117
|
+
capability: TEST_DOMAIN_SENTINEL
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function canonicalRequestPath(pathOrUrl) {
|
|
121
|
+
const parsed = new URL(pathOrUrl, "https://signed-request.invalid");
|
|
122
|
+
return parsed.pathname.startsWith("/") ? parsed.pathname : `/${parsed.pathname}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
isZodSchema,
|
|
127
|
+
RAIL_REFUNDABLE,
|
|
128
|
+
DEFAULT_CREDIT_MIN,
|
|
129
|
+
DEFAULT_CREDIT_MAX,
|
|
130
|
+
DEFAULT_CREDIT_TTL_SECONDS,
|
|
131
|
+
DEFAULT_JOB_RETENTION_DAYS,
|
|
132
|
+
canonicalize,
|
|
133
|
+
canonicaliseForSigning,
|
|
134
|
+
verifyCanonicalSignedRequest,
|
|
135
|
+
SIGNED_REQUEST_AUTH_ID,
|
|
136
|
+
SIGNED_REQUEST_STATEMENT_VERSION,
|
|
137
|
+
signedRequestStatementHeader,
|
|
138
|
+
canonicalSignedRequestStatement,
|
|
139
|
+
signedRequestHeaderMatchesDomain,
|
|
140
|
+
testOnlySignedRequestDomain,
|
|
141
|
+
canonicalRequestPath
|
|
142
|
+
};
|
|
@@ -202,6 +202,7 @@ var PostgresJobStore = class {
|
|
|
202
202
|
-- receipt or re-allocate a sequence number.
|
|
203
203
|
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt JSONB;
|
|
204
204
|
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt_seq BIGINT;
|
|
205
|
+
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS redacted_at BIGINT;
|
|
205
206
|
-- internal-review: credit-ledger linkage. Written at submission alongside the
|
|
206
207
|
-- payment fields; the terminal funnel settles/releases the draw and the
|
|
207
208
|
-- receipt countersigns the ReceiptCredit block from it.
|
|
@@ -241,6 +242,9 @@ var PostgresJobStore = class {
|
|
|
241
242
|
$migration$;
|
|
242
243
|
UPDATE jobs SET next_seq = seq + 1 WHERE next_seq <= seq;
|
|
243
244
|
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs (status);
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_retention
|
|
246
|
+
ON jobs (last_activity_at, id)
|
|
247
|
+
WHERE redacted_at IS NULL AND status IN ('completed', 'failed', 'cancelled');
|
|
244
248
|
-- internal-review: the idempotent-replay lookup. payment_tx_hash carries the
|
|
245
249
|
-- caller's X-Cashu-Request-Id on the accumulator path, so a retried paid
|
|
246
250
|
-- submit finds the job its payment already created.
|
|
@@ -452,8 +456,9 @@ var PostgresJobStore = class {
|
|
|
452
456
|
-- idempotent re-persist of the same outcome); a different status is
|
|
453
457
|
-- rejected wholesale, so the row can't end up as a Frankenstein of a
|
|
454
458
|
-- cancelled status and a completed snapshot.
|
|
455
|
-
WHERE jobs.
|
|
456
|
-
|
|
459
|
+
WHERE jobs.redacted_at IS NULL
|
|
460
|
+
AND (jobs.status NOT IN ('completed', 'failed', 'cancelled')
|
|
461
|
+
OR jobs.status = EXCLUDED.status)`,
|
|
457
462
|
[
|
|
458
463
|
record.id,
|
|
459
464
|
JSON.stringify(record.tags),
|
|
@@ -529,6 +534,73 @@ var PostgresJobStore = class {
|
|
|
529
534
|
await this.pool.query("DELETE FROM jobs WHERE id = $1", [id]);
|
|
530
535
|
await this.pool.query("DELETE FROM job_messages WHERE job_id = $1", [id]).catch(() => void 0);
|
|
531
536
|
}
|
|
537
|
+
async listJobsForRetention(args) {
|
|
538
|
+
const after = args.after;
|
|
539
|
+
const capabilityClause = args.capabilities === null ? "" : "AND capability = ANY($5::text[])";
|
|
540
|
+
const params = [
|
|
541
|
+
args.lastActivityBeforeMs,
|
|
542
|
+
args.limit,
|
|
543
|
+
after?.lastActivityAt ?? null,
|
|
544
|
+
after?.id ?? null
|
|
545
|
+
];
|
|
546
|
+
if (args.capabilities !== null) params.push(args.capabilities);
|
|
547
|
+
const { rows } = await this.pool.query(
|
|
548
|
+
`SELECT * FROM jobs
|
|
549
|
+
WHERE redacted_at IS NULL
|
|
550
|
+
AND status IN ('completed', 'failed', 'cancelled')
|
|
551
|
+
AND last_activity_at < $1
|
|
552
|
+
AND ($3::bigint IS NULL OR (last_activity_at, id) > ($3::bigint, $4::text))
|
|
553
|
+
${capabilityClause}
|
|
554
|
+
ORDER BY last_activity_at, id
|
|
555
|
+
LIMIT $2`,
|
|
556
|
+
params
|
|
557
|
+
);
|
|
558
|
+
return rows.map((row) => rowToRecord(row));
|
|
559
|
+
}
|
|
560
|
+
async redactJob(args) {
|
|
561
|
+
const client = await this.pool.connect();
|
|
562
|
+
try {
|
|
563
|
+
await client.query("BEGIN");
|
|
564
|
+
const { rowCount } = await client.query(
|
|
565
|
+
`UPDATE jobs SET
|
|
566
|
+
input = '',
|
|
567
|
+
params = '{}'::jsonb,
|
|
568
|
+
summary = NULL,
|
|
569
|
+
messages = '[]'::jsonb,
|
|
570
|
+
received_proofs = '[]'::jsonb,
|
|
571
|
+
pending_payment_msats = NULL,
|
|
572
|
+
pending_mpp_challenge_ids = NULL,
|
|
573
|
+
pending_x402_nonce = NULL,
|
|
574
|
+
pending_x402_amount_usdc_micro = NULL,
|
|
575
|
+
pending_payment_fiat_micro = NULL,
|
|
576
|
+
pending_payment_fiat_currency = NULL,
|
|
577
|
+
step_cache = '[]'::jsonb,
|
|
578
|
+
state = '{}'::jsonb,
|
|
579
|
+
requester_token = NULL,
|
|
580
|
+
request_fingerprint = NULL,
|
|
581
|
+
auth_request_path = NULL,
|
|
582
|
+
redacted_at = $3
|
|
583
|
+
WHERE id = $1
|
|
584
|
+
AND last_activity_at = $2
|
|
585
|
+
AND redacted_at IS NULL
|
|
586
|
+
AND status IN ('completed', 'failed', 'cancelled')`,
|
|
587
|
+
[args.jobId, args.expectedLastActivityAt, args.redactedAt]
|
|
588
|
+
);
|
|
589
|
+
if (rowCount === 1) {
|
|
590
|
+
await client.query("DELETE FROM job_messages WHERE job_id = $1", [args.jobId]);
|
|
591
|
+
}
|
|
592
|
+
await client.query("COMMIT");
|
|
593
|
+
return rowCount === 1;
|
|
594
|
+
} catch (err) {
|
|
595
|
+
try {
|
|
596
|
+
await client.query("ROLLBACK");
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
throw err;
|
|
600
|
+
} finally {
|
|
601
|
+
client.release();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
532
604
|
async claimReceiptSeq(jobId) {
|
|
533
605
|
const client = await this.pool.connect();
|
|
534
606
|
try {
|
|
@@ -577,7 +649,9 @@ var PostgresJobStore = class {
|
|
|
577
649
|
try {
|
|
578
650
|
await client.query("BEGIN");
|
|
579
651
|
const { rows } = await client.query(
|
|
580
|
-
|
|
652
|
+
`UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
|
|
653
|
+
WHERE id = $1 AND redacted_at IS NULL
|
|
654
|
+
RETURNING (next_seq - 1) AS seq`,
|
|
581
655
|
[jobId, Date.now()]
|
|
582
656
|
);
|
|
583
657
|
if (rows.length === 0) {
|
|
@@ -667,7 +741,9 @@ var PostgresJobStore = class {
|
|
|
667
741
|
try {
|
|
668
742
|
await client.query("BEGIN");
|
|
669
743
|
const { rows } = await client.query(
|
|
670
|
-
|
|
744
|
+
`UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
|
|
745
|
+
WHERE id = $1 AND redacted_at IS NULL
|
|
746
|
+
RETURNING (next_seq - 1) AS seq`,
|
|
671
747
|
[jobId, Date.now()]
|
|
672
748
|
);
|
|
673
749
|
if (rows.length === 0) {
|
|
@@ -1146,7 +1222,8 @@ function rowToRecord(row) {
|
|
|
1146
1222
|
stepCache: row.step_cache,
|
|
1147
1223
|
state: row.state,
|
|
1148
1224
|
createdAt: Number(row.created_at),
|
|
1149
|
-
lastActivityAt: Number(row.last_activity_at)
|
|
1225
|
+
lastActivityAt: Number(row.last_activity_at),
|
|
1226
|
+
redactedAt: row.redacted_at == null ? void 0 : Number(row.redacted_at)
|
|
1150
1227
|
};
|
|
1151
1228
|
}
|
|
1152
1229
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
applyRetryToPool
|
|
3
|
-
} from "./chunk-C3MTFLC6.js";
|
|
4
1
|
import {
|
|
5
2
|
withSdkInitLock
|
|
6
3
|
} from "./chunk-S3XAHZQY.js";
|
|
4
|
+
import {
|
|
5
|
+
applyRetryToPool
|
|
6
|
+
} from "./chunk-C3MTFLC6.js";
|
|
7
7
|
|
|
8
8
|
// src/sdk/server/credit-ledger.ts
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
@@ -2736,6 +2736,14 @@ var CreditLedger = class {
|
|
|
2736
2736
|
);
|
|
2737
2737
|
return rows.length === 1 ? drawRecordFromRow(rows[0], rows[0]) : void 0;
|
|
2738
2738
|
}
|
|
2739
|
+
/** Read every draw linked to a job, including already-resolved rows. */
|
|
2740
|
+
async listDrawsByJobId(jobId) {
|
|
2741
|
+
const { rows } = await this.pool.query(
|
|
2742
|
+
`${DRAW_WITH_BASIS_SELECT} WHERE d.job_id = $1 ORDER BY d.created_at, d.credit_id, d.ledger_seq`,
|
|
2743
|
+
[jobId]
|
|
2744
|
+
);
|
|
2745
|
+
return rows.map((row) => drawRecordFromRow(row, row));
|
|
2746
|
+
}
|
|
2739
2747
|
/**
|
|
2740
2748
|
* All pending holds on a credit, in `ledger_seq` order (no lock). Read
|
|
2741
2749
|
* surface for the internal-review sweeper wiring — a worker that dies mid-job
|
|
@@ -12,6 +12,25 @@ var ENDPOINTS = {
|
|
|
12
12
|
payout: "/_internal/payout",
|
|
13
13
|
expiry_release: "/_internal/credit-expiry-release"
|
|
14
14
|
};
|
|
15
|
+
var PENDING_DVM_REPORTS_TABLE = "pending_dvm_reports";
|
|
16
|
+
var LEGACY_PENDING_REPORTS_TABLE = "pending_revenue_reports";
|
|
17
|
+
var SDK_REPORT_TABLE_BASE_COLUMNS = [
|
|
18
|
+
"id",
|
|
19
|
+
"payload",
|
|
20
|
+
"retry_count",
|
|
21
|
+
"next_retry_at",
|
|
22
|
+
"stale",
|
|
23
|
+
"created_at"
|
|
24
|
+
];
|
|
25
|
+
var PLATFORM_REPORT_TABLE_COLUMNS = [
|
|
26
|
+
"id",
|
|
27
|
+
"payload",
|
|
28
|
+
"source",
|
|
29
|
+
"retry_count",
|
|
30
|
+
"next_retry_at",
|
|
31
|
+
"last_error",
|
|
32
|
+
"created_at"
|
|
33
|
+
];
|
|
15
34
|
var RevenueReporter = class _RevenueReporter {
|
|
16
35
|
// 24h of failed retries
|
|
17
36
|
constructor(db, platformUrl, platformToken) {
|
|
@@ -27,14 +46,14 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
27
46
|
static MAX_BACKOFF_MS = 3e5;
|
|
28
47
|
// 5 min
|
|
29
48
|
static ABANDON_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
30
|
-
/** Create the pending-reports table (idempotent). */
|
|
49
|
+
/** Create the pending-reports table and migrate the legacy SDK outbox (idempotent). */
|
|
31
50
|
async init() {
|
|
32
51
|
await withSdkInitLock(this.db, () => this.createTables());
|
|
33
52
|
}
|
|
34
53
|
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
35
54
|
async createTables() {
|
|
36
55
|
await this.db.query(`
|
|
37
|
-
CREATE TABLE IF NOT EXISTS
|
|
56
|
+
CREATE TABLE IF NOT EXISTS ${PENDING_DVM_REPORTS_TABLE} (
|
|
38
57
|
id TEXT PRIMARY KEY,
|
|
39
58
|
payload JSONB NOT NULL,
|
|
40
59
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
@@ -45,12 +64,66 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
45
64
|
kind TEXT NOT NULL DEFAULT 'revenue'
|
|
46
65
|
)
|
|
47
66
|
`);
|
|
67
|
+
await this.assertSdkTableShape(PENDING_DVM_REPORTS_TABLE);
|
|
48
68
|
await this.db.query(
|
|
49
|
-
|
|
69
|
+
`ALTER TABLE ${PENDING_DVM_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS stale_reason TEXT`
|
|
50
70
|
);
|
|
51
71
|
await this.db.query(
|
|
52
|
-
|
|
72
|
+
`ALTER TABLE ${PENDING_DVM_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'revenue'`
|
|
53
73
|
);
|
|
74
|
+
await this.migrateLegacySdkOutbox();
|
|
75
|
+
}
|
|
76
|
+
/** Copy rows from the old SDK-owned table without touching the platform's table of that name. */
|
|
77
|
+
async migrateLegacySdkOutbox() {
|
|
78
|
+
const columns = await this.tableColumns(LEGACY_PENDING_REPORTS_TABLE);
|
|
79
|
+
if (columns.size === 0) return;
|
|
80
|
+
if (PLATFORM_REPORT_TABLE_COLUMNS.every((column) => columns.has(column))) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.assertSdkColumns(LEGACY_PENDING_REPORTS_TABLE, columns);
|
|
84
|
+
await this.db.query(
|
|
85
|
+
`ALTER TABLE ${LEGACY_PENDING_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS stale_reason TEXT`
|
|
86
|
+
);
|
|
87
|
+
await this.db.query(
|
|
88
|
+
`ALTER TABLE ${LEGACY_PENDING_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'revenue'`
|
|
89
|
+
);
|
|
90
|
+
await this.db.query(`
|
|
91
|
+
WITH copied AS (
|
|
92
|
+
INSERT INTO ${PENDING_DVM_REPORTS_TABLE}
|
|
93
|
+
(id, payload, retry_count, next_retry_at, stale, stale_reason, created_at, kind)
|
|
94
|
+
SELECT id, payload, retry_count, next_retry_at, stale, stale_reason, created_at, kind
|
|
95
|
+
FROM ${LEGACY_PENDING_REPORTS_TABLE}
|
|
96
|
+
ON CONFLICT (id) DO NOTHING
|
|
97
|
+
RETURNING id
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
DELETE FROM ${LEGACY_PENDING_REPORTS_TABLE} AS legacy
|
|
101
|
+
WHERE EXISTS (SELECT 1 FROM copied WHERE copied.id = legacy.id)
|
|
102
|
+
OR EXISTS (
|
|
103
|
+
SELECT 1 FROM ${PENDING_DVM_REPORTS_TABLE} AS migrated
|
|
104
|
+
WHERE migrated.id = legacy.id
|
|
105
|
+
)
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
/** Refuse a pre-existing relation whose columns cannot safely serve as the SDK outbox. */
|
|
109
|
+
async assertSdkTableShape(tableName) {
|
|
110
|
+
this.assertSdkColumns(tableName, await this.tableColumns(tableName));
|
|
111
|
+
}
|
|
112
|
+
assertSdkColumns(tableName, columns) {
|
|
113
|
+
const matchesSdk = SDK_REPORT_TABLE_BASE_COLUMNS.every((column) => columns.has(column)) && !columns.has("source");
|
|
114
|
+
if (matchesSdk) return;
|
|
115
|
+
throw new Error(
|
|
116
|
+
`RevenueReporter.init(): table "${tableName}" is not the SDK revenue outbox shape. The SDK owns "${PENDING_DVM_REPORTS_TABLE}"; the platform owns "${LEGACY_PENDING_REPORTS_TABLE}". Refusing to boot before credit funding can reach a mismatched outbox. Found columns: ${[...columns].sort().join(", ") || "none"}.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
async tableColumns(tableName) {
|
|
120
|
+
const { rows } = await this.db.query(
|
|
121
|
+
`SELECT column_name
|
|
122
|
+
FROM information_schema.columns
|
|
123
|
+
WHERE table_schema = current_schema() AND table_name = $1`,
|
|
124
|
+
[tableName]
|
|
125
|
+
);
|
|
126
|
+
return new Set(rows.map((row) => row.column_name));
|
|
54
127
|
}
|
|
55
128
|
/** Persist a revenue report and attempt immediate delivery. */
|
|
56
129
|
async report(payload) {
|
|
@@ -367,7 +440,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
367
440
|
async getQueueStats() {
|
|
368
441
|
const { rows } = await this.db.query(
|
|
369
442
|
`SELECT stale, stale_reason, COUNT(*)::int AS count
|
|
370
|
-
FROM
|
|
443
|
+
FROM ${PENDING_DVM_REPORTS_TABLE}
|
|
371
444
|
GROUP BY stale, stale_reason`
|
|
372
445
|
);
|
|
373
446
|
let liveRetry = 0;
|
|
@@ -394,7 +467,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
394
467
|
async drainPending() {
|
|
395
468
|
try {
|
|
396
469
|
const { rows } = await this.db.query(
|
|
397
|
-
`SELECT id, payload, retry_count, created_at, kind FROM
|
|
470
|
+
`SELECT id, payload, retry_count, created_at, kind FROM ${PENDING_DVM_REPORTS_TABLE}
|
|
398
471
|
WHERE stale = false AND next_retry_at <= $1
|
|
399
472
|
ORDER BY created_at LIMIT 10`,
|
|
400
473
|
[Date.now()]
|
|
@@ -428,7 +501,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
428
501
|
_RevenueReporter.MAX_BACKOFF_MS
|
|
429
502
|
);
|
|
430
503
|
await this.db.query(
|
|
431
|
-
`UPDATE
|
|
504
|
+
`UPDATE ${PENDING_DVM_REPORTS_TABLE}
|
|
432
505
|
SET retry_count = $1, next_retry_at = $2
|
|
433
506
|
WHERE id = $3`,
|
|
434
507
|
[retryCount, Date.now() + backoff, row.id]
|
|
@@ -449,7 +522,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
449
522
|
async insertPending(q, kind, payload) {
|
|
450
523
|
const id = randomUUID();
|
|
451
524
|
await q.query(
|
|
452
|
-
`INSERT INTO
|
|
525
|
+
`INSERT INTO ${PENDING_DVM_REPORTS_TABLE} (id, payload, created_at, kind)
|
|
453
526
|
VALUES ($1, $2, $3, $4)`,
|
|
454
527
|
[id, JSON.stringify(payload), Date.now(), kind]
|
|
455
528
|
);
|
|
@@ -476,7 +549,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
476
549
|
throw err;
|
|
477
550
|
}
|
|
478
551
|
if (res.ok) {
|
|
479
|
-
await this.db.query(
|
|
552
|
+
await this.db.query(`DELETE FROM ${PENDING_DVM_REPORTS_TABLE} WHERE id = $1`, [id]);
|
|
480
553
|
console.log(`[revenue-reporter] reported ${kind} ${label}`);
|
|
481
554
|
return;
|
|
482
555
|
}
|
|
@@ -500,7 +573,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
500
573
|
/** Mark a pending row terminal (stale) with a classification reason. */
|
|
501
574
|
async markStale(id, reason) {
|
|
502
575
|
await this.db.query(
|
|
503
|
-
|
|
576
|
+
`UPDATE ${PENDING_DVM_REPORTS_TABLE} SET stale = true, stale_reason = $1 WHERE id = $2`,
|
|
504
577
|
[reason, id]
|
|
505
578
|
);
|
|
506
579
|
}
|