@dvmkit/sdk 0.0.0 → 0.1.0-rc.1
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/NOTICE +2 -0
- package/README.md +38 -2
- package/dist/chunk-27V2ILSR.js +291 -0
- package/dist/chunk-5GFED3GJ.js +955 -0
- package/dist/chunk-6JZIX5WW.js +1155 -0
- package/dist/chunk-7IH5SG2A.js +1038 -0
- package/dist/chunk-AT6V3SY7.js +102 -0
- package/dist/chunk-DCNT4PJS.js +733 -0
- package/dist/chunk-DMNLFNTW.js +135 -0
- package/dist/chunk-FROTD5XQ.js +70 -0
- package/dist/chunk-H25M54MI.js +149 -0
- package/dist/chunk-KQAJVVZT.js +712 -0
- package/dist/chunk-KXWROQGK.js +74 -0
- package/dist/chunk-L4OYF4DQ.js +67 -0
- package/dist/chunk-NTK5DJ6R.js +1256 -0
- package/dist/chunk-RPXHKMYE.js +3808 -0
- package/dist/chunk-S3XAHZQY.js +63 -0
- package/dist/chunk-YG7G4DPZ.js +25 -0
- package/dist/credit-ledger-EDMEZSA2.js +28 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +303 -0
- package/dist/job-store-C5n6bhap.d.ts +5090 -0
- package/dist/memory-credit-ledger-7TTZDSRS.js +9 -0
- package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
- package/dist/mpp-setup-MOBWGTWJ.js +30 -0
- package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
- package/dist/postgres-job-store-J5F4GUWU.js +7 -0
- package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
- package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
- package/dist/pricing-4CEB34RM.js +48 -0
- package/dist/processed-payment-store-HAA4SFNK.js +11 -0
- package/dist/revenue-reporter-M35KP6V7.js +435 -0
- package/dist/server/index.d.ts +4108 -0
- package/dist/server/index.js +22538 -0
- package/dist/ssrf-BdHsrrIb.d.ts +325 -0
- package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
- package/dist/tempo-session-store-FTEEGZXA.js +467 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +151 -0
- package/dist/x402-35VLYFKZ.js +1272 -0
- package/package.json +89 -6
|
@@ -0,0 +1,712 @@
|
|
|
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
|
+
|
|
14
|
+
// src/sdk/env.ts
|
|
15
|
+
function envFlag(value) {
|
|
16
|
+
return value === "1" || value === "true";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/lib/cast-auth/canonical-json.ts
|
|
20
|
+
function canonicalize(value) {
|
|
21
|
+
return JSON.stringify(sortDeep(value));
|
|
22
|
+
}
|
|
23
|
+
function canonicaliseForSigning(value) {
|
|
24
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
25
|
+
}
|
|
26
|
+
function sortDeep(value) {
|
|
27
|
+
if (value === null) return null;
|
|
28
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
if (!Number.isFinite(value)) {
|
|
31
|
+
throw new TypeError(`canonicalize: non-finite number is not JSON-serialisable`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
36
|
+
if (typeof value === "object") {
|
|
37
|
+
const sorted = {};
|
|
38
|
+
for (const key of Object.keys(value).sort()) {
|
|
39
|
+
const v = value[key];
|
|
40
|
+
if (v === void 0) continue;
|
|
41
|
+
sorted[key] = sortDeep(v);
|
|
42
|
+
}
|
|
43
|
+
return sorted;
|
|
44
|
+
}
|
|
45
|
+
throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/lib/cast-auth/verify.ts
|
|
49
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
50
|
+
|
|
51
|
+
// src/lib/signed-request-statement.ts
|
|
52
|
+
var SIGNED_REQUEST_AUTH_ID = "secp256k1-schnorr-v2";
|
|
53
|
+
var SIGNED_REQUEST_STATEMENT_VERSION = 2;
|
|
54
|
+
var TEST_DOMAIN_SENTINEL = "__dvmkit_test_domain__";
|
|
55
|
+
function normalizeSignedRequestDomain(domain) {
|
|
56
|
+
return {
|
|
57
|
+
audience: {
|
|
58
|
+
dvm_id: domain.audience.dvm_id,
|
|
59
|
+
builder_pubkey: domain.audience.builder_pubkey.trim().toLowerCase()
|
|
60
|
+
},
|
|
61
|
+
method: domain.method.trim().toUpperCase(),
|
|
62
|
+
path: canonicalRequestPath(domain.path),
|
|
63
|
+
capability: domain.capability
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function signedRequestStatementHeader(domain) {
|
|
67
|
+
const normalized = normalizeSignedRequestDomain(domain);
|
|
68
|
+
return {
|
|
69
|
+
v: SIGNED_REQUEST_STATEMENT_VERSION,
|
|
70
|
+
protocol: "dvmkit",
|
|
71
|
+
protocol_version: 1,
|
|
72
|
+
auth: SIGNED_REQUEST_AUTH_ID,
|
|
73
|
+
audience: normalized.audience,
|
|
74
|
+
method: normalized.method,
|
|
75
|
+
path: normalized.path,
|
|
76
|
+
capability: normalized.capability
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function canonicalSignedRequestStatement(header, input) {
|
|
80
|
+
return canonicaliseForSigning({
|
|
81
|
+
...header,
|
|
82
|
+
input
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function signedRequestHeaderMatchesDomain(header, domain) {
|
|
86
|
+
if (!header || typeof header !== "object" || Array.isArray(header)) return false;
|
|
87
|
+
const actual = header;
|
|
88
|
+
const expected = signedRequestStatementHeader(domain);
|
|
89
|
+
const audience = actual.audience;
|
|
90
|
+
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) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
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;
|
|
94
|
+
}
|
|
95
|
+
function testOnlySignedRequestDomain() {
|
|
96
|
+
if (process.env.NODE_ENV !== "test") return void 0;
|
|
97
|
+
return {
|
|
98
|
+
audience: { dvm_id: TEST_DOMAIN_SENTINEL, builder_pubkey: "00".repeat(32) },
|
|
99
|
+
method: TEST_DOMAIN_SENTINEL,
|
|
100
|
+
path: TEST_DOMAIN_SENTINEL,
|
|
101
|
+
capability: TEST_DOMAIN_SENTINEL
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function canonicalRequestPath(pathOrUrl) {
|
|
105
|
+
const parsed = new URL(pathOrUrl, "https://signed-request.invalid");
|
|
106
|
+
return parsed.pathname.startsWith("/") ? parsed.pathname : `/${parsed.pathname}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/sdk/server/signed-request.ts
|
|
110
|
+
import { schnorr as schnorr2 } from "@noble/curves/secp256k1.js";
|
|
111
|
+
|
|
112
|
+
// src/sdk/server/zod-error.ts
|
|
113
|
+
function humanizeZodError(err, opts) {
|
|
114
|
+
return humanizeZodIssues(err.issues, opts);
|
|
115
|
+
}
|
|
116
|
+
function humanizeZodIssues(issues, opts) {
|
|
117
|
+
return { message: summariseIssues(issues), hint: pickHint(issues, opts.schemaUrl) };
|
|
118
|
+
}
|
|
119
|
+
function zodIssuesOf(error) {
|
|
120
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
121
|
+
const issues = error.issues;
|
|
122
|
+
return Array.isArray(issues) ? issues : void 0;
|
|
123
|
+
}
|
|
124
|
+
var MAX_REPORTED_CLAUSES = 3;
|
|
125
|
+
function summariseIssues(issues) {
|
|
126
|
+
if (issues.length === 0) return "Input validation failed";
|
|
127
|
+
const line = issues.slice(0, MAX_REPORTED_CLAUSES).map(summariseIssue).join("; ");
|
|
128
|
+
const overflow = issues.length - MAX_REPORTED_CLAUSES;
|
|
129
|
+
return overflow > 0 ? `${line} (+${overflow.toString()} more)` : line;
|
|
130
|
+
}
|
|
131
|
+
function summariseIssue(issue) {
|
|
132
|
+
const path = formatPath(issue.path);
|
|
133
|
+
const reason = describeReason(issue);
|
|
134
|
+
return path ? `${path}: ${reason}` : reason;
|
|
135
|
+
}
|
|
136
|
+
function formatPath(path) {
|
|
137
|
+
if (path.length === 0) return "";
|
|
138
|
+
return path.map((segment) => typeof segment === "number" ? `[${segment.toString()}]` : String(segment)).join(".");
|
|
139
|
+
}
|
|
140
|
+
function describeReason(issue) {
|
|
141
|
+
switch (issue.code) {
|
|
142
|
+
case "too_big": {
|
|
143
|
+
const max = issue.maximum.toString();
|
|
144
|
+
if (issue.origin === "string") return `too long (max ${max} chars)`;
|
|
145
|
+
if (issue.origin === "array" || issue.origin === "set") {
|
|
146
|
+
return `too many items (max ${max})`;
|
|
147
|
+
}
|
|
148
|
+
return `value too large (max ${max})`;
|
|
149
|
+
}
|
|
150
|
+
case "too_small": {
|
|
151
|
+
const min = issue.minimum.toString();
|
|
152
|
+
if (issue.origin === "string") {
|
|
153
|
+
return min === "1" ? "required" : `too short (min ${min} chars)`;
|
|
154
|
+
}
|
|
155
|
+
if (issue.origin === "array" || issue.origin === "set") {
|
|
156
|
+
return min === "1" ? "required (must be non-empty)" : `too few items (min ${min})`;
|
|
157
|
+
}
|
|
158
|
+
return `value too small (min ${min})`;
|
|
159
|
+
}
|
|
160
|
+
case "invalid_type":
|
|
161
|
+
return `expected ${issue.expected}`;
|
|
162
|
+
case "invalid_value":
|
|
163
|
+
return `must be one of: ${issue.values.map(String).join(", ")}`;
|
|
164
|
+
case "invalid_format":
|
|
165
|
+
return `invalid ${issue.format}`;
|
|
166
|
+
case "unrecognized_keys":
|
|
167
|
+
return `unrecognized field(s): ${issue.keys.join(", ")}`;
|
|
168
|
+
case "not_multiple_of":
|
|
169
|
+
return `must be a multiple of ${issue.divisor.toString()}`;
|
|
170
|
+
case "invalid_union":
|
|
171
|
+
return "no matching variant";
|
|
172
|
+
case "custom":
|
|
173
|
+
default:
|
|
174
|
+
return issue.message || "invalid";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function pickHint(issues, schemaUrl) {
|
|
178
|
+
const tooBigString = issues.find(
|
|
179
|
+
(i) => i.code === "too_big" && i.origin === "string"
|
|
180
|
+
);
|
|
181
|
+
if (tooBigString) {
|
|
182
|
+
const path = formatPath(tooBigString.path);
|
|
183
|
+
const field = path ? `\`${path}\` ` : "";
|
|
184
|
+
return `Input field ${field}exceeds the ${tooBigString.maximum.toString()}-character limit. Split into smaller chunks and submit separately.`;
|
|
185
|
+
}
|
|
186
|
+
const tooSmallString = issues.find(
|
|
187
|
+
(i) => i.code === "too_small" && i.origin === "string" && i.minimum === 1
|
|
188
|
+
);
|
|
189
|
+
if (tooSmallString) {
|
|
190
|
+
const path = formatPath(tooSmallString.path);
|
|
191
|
+
const field = path ? `\`${path}\`` : "the required string field";
|
|
192
|
+
return `Provide non-empty text in ${field}.`;
|
|
193
|
+
}
|
|
194
|
+
return `Inspect ${schemaUrl} for the expected input schema.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/sdk/server/signed-request.ts
|
|
198
|
+
var SIGNED_ENVELOPE_TYPES = {
|
|
199
|
+
pubkey: "string",
|
|
200
|
+
signature: "string",
|
|
201
|
+
timestamp: "number",
|
|
202
|
+
nonce: "string",
|
|
203
|
+
auth_statement: "object"
|
|
204
|
+
};
|
|
205
|
+
var SIGNED_ENVELOPE_FIELDS = Object.keys(
|
|
206
|
+
SIGNED_ENVELOPE_TYPES
|
|
207
|
+
);
|
|
208
|
+
var SignedRequestError = class extends Error {
|
|
209
|
+
constructor(message, sub_reason, issues) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.sub_reason = sub_reason;
|
|
212
|
+
this.issues = issues;
|
|
213
|
+
this.name = "SignedRequestError";
|
|
214
|
+
}
|
|
215
|
+
sub_reason;
|
|
216
|
+
issues;
|
|
217
|
+
code = "auth_error";
|
|
218
|
+
};
|
|
219
|
+
var DEFAULT_DRIFT_SECONDS = 5 * 60;
|
|
220
|
+
var DEFAULT_ENVELOPE_FIELDS = ["signature"];
|
|
221
|
+
var DEFAULT_REPLAY_WINDOW_SECONDS = 10 * 60;
|
|
222
|
+
var DEFAULT_REPLAY_CAP = 1e5;
|
|
223
|
+
function createSignedRequestVerifier(schema, opts = {}) {
|
|
224
|
+
const driftSeconds = opts.driftSeconds ?? DEFAULT_DRIFT_SECONDS;
|
|
225
|
+
const stripFields = /* @__PURE__ */ new Set([...DEFAULT_ENVELOPE_FIELDS, ...opts.envelopeFields ?? []]);
|
|
226
|
+
const schemaIgnoreFields = /* @__PURE__ */ new Set([
|
|
227
|
+
"auth_statement",
|
|
228
|
+
...opts.schemaIgnoreFields ?? []
|
|
229
|
+
]);
|
|
230
|
+
const replayStore = opts.replayStore === null ? null : opts.replayStore ?? createDefaultReplayStore();
|
|
231
|
+
const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
232
|
+
function requiredDomain(domain) {
|
|
233
|
+
const resolved = domain ?? opts.domain ?? testOnlySignedRequestDomain();
|
|
234
|
+
if (!resolved) {
|
|
235
|
+
throw new SignedRequestError("signed-request domain is required", "signature_invalid");
|
|
236
|
+
}
|
|
237
|
+
return resolved;
|
|
238
|
+
}
|
|
239
|
+
function parse(input) {
|
|
240
|
+
if (!isZodSchema(schema)) {
|
|
241
|
+
throw new SignedRequestError("signed-request schema must be a Zod schema", "schema_invalid");
|
|
242
|
+
}
|
|
243
|
+
const parsed = schema.safeParse(withoutIgnoredFields(input, schemaIgnoreFields));
|
|
244
|
+
if (!parsed.success) {
|
|
245
|
+
throw new SignedRequestError(
|
|
246
|
+
"input did not satisfy schema",
|
|
247
|
+
"schema_invalid",
|
|
248
|
+
zodIssuesOf(parsed.error)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const envelope = {
|
|
252
|
+
...parsed.data,
|
|
253
|
+
auth_statement: statementHeader(input)
|
|
254
|
+
};
|
|
255
|
+
const mistyped = Object.entries(SIGNED_ENVELOPE_TYPES).some(
|
|
256
|
+
([field, expected]) => typeof envelope[field] !== expected
|
|
257
|
+
);
|
|
258
|
+
if (mistyped) {
|
|
259
|
+
throw new SignedRequestError(
|
|
260
|
+
`schema must include ${SIGNED_ENVELOPE_FIELDS.join("/")} envelope fields`,
|
|
261
|
+
"schema_invalid"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return envelope;
|
|
265
|
+
}
|
|
266
|
+
function canonicalBytes(input, domain) {
|
|
267
|
+
const expected = requiredDomain(domain);
|
|
268
|
+
const stripped = stripEnvelope(input, stripFields);
|
|
269
|
+
const header = statementHeader(input);
|
|
270
|
+
if (!signedRequestHeaderMatchesDomain(header, expected)) {
|
|
271
|
+
throw new SignedRequestError("signed-request domain does not match", "signature_invalid");
|
|
272
|
+
}
|
|
273
|
+
return canonicalSignedRequestStatement(header, stripped);
|
|
274
|
+
}
|
|
275
|
+
function checkAuth(input, domain) {
|
|
276
|
+
const envelope = parse(input);
|
|
277
|
+
const t = now();
|
|
278
|
+
if (Math.abs(t - envelope.timestamp) > driftSeconds) {
|
|
279
|
+
throw new SignedRequestError(
|
|
280
|
+
"timestamp drift exceeds the permitted window",
|
|
281
|
+
"timestamp_drift"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (!verifyBytes(canonicalBytes(input, domain), envelope.signature, envelope.pubkey)) {
|
|
285
|
+
throw new SignedRequestError("signature verification failed", "signature_invalid");
|
|
286
|
+
}
|
|
287
|
+
return envelope;
|
|
288
|
+
}
|
|
289
|
+
function verifySignature(input, domain) {
|
|
290
|
+
const envelope = parse(input);
|
|
291
|
+
if (!verifyBytes(canonicalBytes(input, domain), envelope.signature, envelope.pubkey)) {
|
|
292
|
+
throw new SignedRequestError("signature verification failed", "signature_invalid");
|
|
293
|
+
}
|
|
294
|
+
return envelope;
|
|
295
|
+
}
|
|
296
|
+
async function recordReplay(envelope) {
|
|
297
|
+
if (!replayStore) return;
|
|
298
|
+
const t = now();
|
|
299
|
+
if (await replayStore.checkAndRecord(envelope.pubkey, envelope.timestamp, envelope.nonce, t)) {
|
|
300
|
+
throw new SignedRequestError(
|
|
301
|
+
"duplicate (pubkey, timestamp, nonce) in replay window",
|
|
302
|
+
"replay_detected"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function signRequest(privkey, body, signOpts, domain) {
|
|
307
|
+
const pubkey = bytesToHex(schnorr2.getPublicKey(privkey));
|
|
308
|
+
const timestamp = signOpts?.timestamp ?? now();
|
|
309
|
+
const nonce = signOpts?.nonce ?? randomNonce();
|
|
310
|
+
const unsigned = {
|
|
311
|
+
pubkey,
|
|
312
|
+
timestamp,
|
|
313
|
+
nonce,
|
|
314
|
+
...body
|
|
315
|
+
};
|
|
316
|
+
const authStatement = signedRequestStatementHeader(requiredDomain(domain));
|
|
317
|
+
const signature = bytesToHex(
|
|
318
|
+
schnorr2.sign(canonicalBytes({ ...unsigned, auth_statement: authStatement }, domain), privkey)
|
|
319
|
+
);
|
|
320
|
+
return { ...unsigned, auth_statement: authStatement, signature };
|
|
321
|
+
}
|
|
322
|
+
return { checkAuth, verifySignature, recordReplay, canonicalBytes, signRequest };
|
|
323
|
+
}
|
|
324
|
+
function signedRequestInput(body) {
|
|
325
|
+
if (body.data !== void 0) return body.data;
|
|
326
|
+
if (typeof body.input === "string" && body.input.length > 0) {
|
|
327
|
+
try {
|
|
328
|
+
return JSON.parse(body.input);
|
|
329
|
+
} catch {
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
function withoutIgnoredFields(input, ignore) {
|
|
336
|
+
if (ignore.size === 0) return input;
|
|
337
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return input;
|
|
338
|
+
const entries = Object.entries(input);
|
|
339
|
+
if (!entries.some(([key]) => ignore.has(key))) return input;
|
|
340
|
+
return Object.fromEntries(entries.filter(([key]) => !ignore.has(key)));
|
|
341
|
+
}
|
|
342
|
+
function stripEnvelope(input, stripFields) {
|
|
343
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
344
|
+
return input;
|
|
345
|
+
}
|
|
346
|
+
const out = {};
|
|
347
|
+
for (const [k, v] of Object.entries(input)) {
|
|
348
|
+
if (stripFields.has(k) || k === "auth_statement") continue;
|
|
349
|
+
if (v === void 0) continue;
|
|
350
|
+
out[k] = v;
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
function statementHeader(input) {
|
|
355
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
|
|
356
|
+
return input.auth_statement;
|
|
357
|
+
}
|
|
358
|
+
function verifyBytes(bytes, signatureHex, pubkeyHex) {
|
|
359
|
+
try {
|
|
360
|
+
return schnorr2.verify(hexToBytes(signatureHex), bytes, hexToBytes(pubkeyHex));
|
|
361
|
+
} catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function hexToBytes(hex) {
|
|
366
|
+
const out = new Uint8Array(hex.length / 2);
|
|
367
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
function bytesToHex(bytes) {
|
|
371
|
+
let s = "";
|
|
372
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
373
|
+
return s;
|
|
374
|
+
}
|
|
375
|
+
function randomNonce() {
|
|
376
|
+
const bytes = new Uint8Array(12);
|
|
377
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
378
|
+
let s = "";
|
|
379
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
380
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
381
|
+
}
|
|
382
|
+
function createDefaultReplayStore() {
|
|
383
|
+
const entries = /* @__PURE__ */ new Map();
|
|
384
|
+
let lastSaturationWarnAt = 0;
|
|
385
|
+
return {
|
|
386
|
+
checkAndRecord(pubkey, timestamp, nonce, t) {
|
|
387
|
+
const key = `${pubkey}|${timestamp.toString()}|${nonce}`;
|
|
388
|
+
const cutoff = t - DEFAULT_REPLAY_WINDOW_SECONDS;
|
|
389
|
+
for (const [k, recordedAt] of entries) {
|
|
390
|
+
if (recordedAt >= cutoff) break;
|
|
391
|
+
entries.delete(k);
|
|
392
|
+
}
|
|
393
|
+
if (entries.has(key)) return Promise.resolve(true);
|
|
394
|
+
if (entries.size >= DEFAULT_REPLAY_CAP) {
|
|
395
|
+
if (t - lastSaturationWarnAt >= SATURATION_WARN_INTERVAL_SECONDS) {
|
|
396
|
+
lastSaturationWarnAt = t;
|
|
397
|
+
console.warn(
|
|
398
|
+
JSON.stringify({
|
|
399
|
+
level: "warn",
|
|
400
|
+
event: "signed_request_replay_store_saturated",
|
|
401
|
+
message: "in-memory signed-request replay store at cap \u2014 rejecting fresh inserts to avoid widening the replay window; move to PostgresReplayStore",
|
|
402
|
+
cap: DEFAULT_REPLAY_CAP,
|
|
403
|
+
window_seconds: DEFAULT_REPLAY_WINDOW_SECONDS
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return Promise.resolve(true);
|
|
408
|
+
}
|
|
409
|
+
entries.set(key, t);
|
|
410
|
+
return Promise.resolve(false);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
var SATURATION_WARN_INTERVAL_SECONDS = 60;
|
|
415
|
+
|
|
416
|
+
// src/sdk/server/ssrf.ts
|
|
417
|
+
import { lookup } from "dns/promises";
|
|
418
|
+
import { isIPv4, isIPv6 } from "net";
|
|
419
|
+
import { Agent } from "undici";
|
|
420
|
+
var SSRFError = class extends Error {
|
|
421
|
+
code = "ssrf_blocked";
|
|
422
|
+
reason;
|
|
423
|
+
constructor(reason, message) {
|
|
424
|
+
super(message);
|
|
425
|
+
this.reason = reason;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
async function assertSafeUrl(url, opts) {
|
|
429
|
+
await resolveAndValidate(url, opts);
|
|
430
|
+
}
|
|
431
|
+
async function createPinnedFetch(url, opts) {
|
|
432
|
+
const validated = await resolveAndValidate(url, opts);
|
|
433
|
+
const baseFetch = opts?.baseFetch ?? fetch;
|
|
434
|
+
const initialUrl = new URL(url);
|
|
435
|
+
const agents = [];
|
|
436
|
+
const targets = /* @__PURE__ */ new Map();
|
|
437
|
+
const createTarget = (next) => {
|
|
438
|
+
if (next.kind === "literal") return {};
|
|
439
|
+
const agent = new Agent({
|
|
440
|
+
connect: { lookup: buildPinnedLookup(next.addresses) }
|
|
441
|
+
});
|
|
442
|
+
agents.push(agent);
|
|
443
|
+
return { agent };
|
|
444
|
+
};
|
|
445
|
+
targets.set(initialUrl.origin, createTarget(validated));
|
|
446
|
+
const targetFor = async (requestUrl) => {
|
|
447
|
+
const parsed = new URL(requestUrl);
|
|
448
|
+
const existing = targets.get(parsed.origin);
|
|
449
|
+
if (existing) return existing;
|
|
450
|
+
const target = createTarget(await resolveAndValidate(parsed.href, opts));
|
|
451
|
+
targets.set(parsed.origin, target);
|
|
452
|
+
return target;
|
|
453
|
+
};
|
|
454
|
+
const pinnedFetch = async (input, init) => {
|
|
455
|
+
let request = new Request(input, init);
|
|
456
|
+
let redirects = 0;
|
|
457
|
+
for (; ; ) {
|
|
458
|
+
const target = await targetFor(request.url);
|
|
459
|
+
const response = await baseFetch(
|
|
460
|
+
request,
|
|
461
|
+
target.agent ? { redirect: "manual", dispatcher: target.agent } : { redirect: "manual" }
|
|
462
|
+
);
|
|
463
|
+
if (!isRedirect(response.status) || request.redirect === "manual") return response;
|
|
464
|
+
const location = response.headers.get("location");
|
|
465
|
+
if (location === null) return response;
|
|
466
|
+
await discardResponse(response);
|
|
467
|
+
if (request.redirect === "error") {
|
|
468
|
+
throw new TypeError("fetch failed: redirect mode is set to error");
|
|
469
|
+
}
|
|
470
|
+
if (redirects >= MAX_PINNED_REDIRECTS) {
|
|
471
|
+
throw new TypeError("fetch failed: maximum redirect count exceeded");
|
|
472
|
+
}
|
|
473
|
+
request = requestForRedirect(request, new URL(location, request.url), response.status);
|
|
474
|
+
redirects += 1;
|
|
475
|
+
opts?.onRedirectFollowed?.();
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
return {
|
|
479
|
+
fetch: pinnedFetch,
|
|
480
|
+
address: validated.address,
|
|
481
|
+
family: validated.family,
|
|
482
|
+
close: async () => {
|
|
483
|
+
await Promise.all(agents.map((agent) => agent.close()));
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
var MAX_PINNED_REDIRECTS = 20;
|
|
488
|
+
function isRedirect(status) {
|
|
489
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
490
|
+
}
|
|
491
|
+
async function discardResponse(response) {
|
|
492
|
+
await response.body?.cancel().catch(() => void 0);
|
|
493
|
+
}
|
|
494
|
+
function requestForRedirect(request, target, status) {
|
|
495
|
+
const headers = new Headers(request.headers);
|
|
496
|
+
if (new URL(request.url).origin !== target.origin) {
|
|
497
|
+
headers.delete("authorization");
|
|
498
|
+
headers.delete("cookie");
|
|
499
|
+
headers.delete("proxy-authorization");
|
|
500
|
+
headers.delete("host");
|
|
501
|
+
}
|
|
502
|
+
const method = request.method.toUpperCase();
|
|
503
|
+
const switchToGet = status === 303 && method !== "HEAD" || (status === 301 || status === 302) && method === "POST";
|
|
504
|
+
if (switchToGet) {
|
|
505
|
+
headers.delete("content-length");
|
|
506
|
+
headers.delete("content-type");
|
|
507
|
+
return redirectRequest(target, request, headers, "GET");
|
|
508
|
+
}
|
|
509
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
510
|
+
throw new TypeError("fetch failed: cannot safely replay a request body across this redirect");
|
|
511
|
+
}
|
|
512
|
+
return redirectRequest(target, request, headers, method);
|
|
513
|
+
}
|
|
514
|
+
function redirectRequest(target, request, headers, method) {
|
|
515
|
+
return new Request(target, {
|
|
516
|
+
method,
|
|
517
|
+
headers,
|
|
518
|
+
signal: request.signal,
|
|
519
|
+
redirect: request.redirect,
|
|
520
|
+
cache: request.cache,
|
|
521
|
+
credentials: request.credentials,
|
|
522
|
+
integrity: request.integrity,
|
|
523
|
+
keepalive: request.keepalive,
|
|
524
|
+
mode: request.mode,
|
|
525
|
+
referrer: request.referrer,
|
|
526
|
+
referrerPolicy: request.referrerPolicy
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function buildPinnedLookup(addresses) {
|
|
530
|
+
return ((_hostname, options, cb) => {
|
|
531
|
+
if (options.all === false) {
|
|
532
|
+
const first = addresses[0];
|
|
533
|
+
cb(
|
|
534
|
+
null,
|
|
535
|
+
first.address,
|
|
536
|
+
first.family
|
|
537
|
+
);
|
|
538
|
+
} else {
|
|
539
|
+
cb(
|
|
540
|
+
null,
|
|
541
|
+
addresses
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async function resolveAndValidate(url, opts) {
|
|
547
|
+
const resolver = opts?.resolver ?? defaultResolver;
|
|
548
|
+
let parsed;
|
|
549
|
+
try {
|
|
550
|
+
parsed = new URL(url);
|
|
551
|
+
} catch {
|
|
552
|
+
throw new SSRFError("invalid_url", `url is not a valid URL: ${url}`);
|
|
553
|
+
}
|
|
554
|
+
if (parsed.protocol !== "https:") {
|
|
555
|
+
throw new SSRFError("insecure_scheme", `url must be https; got ${parsed.protocol}`);
|
|
556
|
+
}
|
|
557
|
+
const host = parsed.hostname.toLowerCase();
|
|
558
|
+
if (host === "") {
|
|
559
|
+
throw new SSRFError("invalid_url", "url has no host");
|
|
560
|
+
}
|
|
561
|
+
if (HOSTNAME_DENYLIST_EXACT.has(host) || matchesExtraExact(host, opts?.extraHostnamesExact)) {
|
|
562
|
+
throw new SSRFError("private_address", `url host "${host}" is on the SSRF denylist`);
|
|
563
|
+
}
|
|
564
|
+
for (const suffix of HOSTNAME_DENYLIST_SUFFIX) {
|
|
565
|
+
if (host === suffix.slice(1) || host.endsWith(suffix)) {
|
|
566
|
+
throw new SSRFError(
|
|
567
|
+
"private_address",
|
|
568
|
+
`url host "${host}" matches denied suffix "${suffix}"`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (opts?.extraHostnameSuffixes) {
|
|
573
|
+
for (const suffix of opts.extraHostnameSuffixes) {
|
|
574
|
+
const s = suffix.toLowerCase();
|
|
575
|
+
if (host === s.slice(1) || host.endsWith(s)) {
|
|
576
|
+
throw new SSRFError("private_address", `url host "${host}" matches denied suffix "${s}"`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const stripped = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
581
|
+
if (isIPv4(stripped) || isIPv6(stripped)) {
|
|
582
|
+
if (isUnsafeAddress(stripped, opts)) {
|
|
583
|
+
throw new SSRFError(
|
|
584
|
+
"private_address",
|
|
585
|
+
`url host "${stripped}" is in a blocked address range`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return { kind: "literal", address: stripped, family: isIPv4(stripped) ? 4 : 6 };
|
|
589
|
+
}
|
|
590
|
+
let addresses;
|
|
591
|
+
try {
|
|
592
|
+
addresses = await resolver(host);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
throw new SSRFError(
|
|
595
|
+
"dns_failed",
|
|
596
|
+
`url host "${host}" failed DNS resolution: ${err instanceof Error ? err.message : String(err)}`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
if (addresses.length === 0) {
|
|
600
|
+
throw new SSRFError("dns_failed", `url host "${host}" resolved to no addresses`);
|
|
601
|
+
}
|
|
602
|
+
for (const { address } of addresses) {
|
|
603
|
+
if (isUnsafeAddress(address, opts)) {
|
|
604
|
+
throw new SSRFError(
|
|
605
|
+
"private_address",
|
|
606
|
+
`url host "${host}" resolves to blocked address "${address}"`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const validated = addresses.map((a) => ({ address: a.address, family: a.family }));
|
|
611
|
+
const first = validated[0];
|
|
612
|
+
return { kind: "resolved", address: first.address, family: first.family, addresses: validated };
|
|
613
|
+
}
|
|
614
|
+
var HOSTNAME_DENYLIST_EXACT = /* @__PURE__ */ new Set([
|
|
615
|
+
"localhost",
|
|
616
|
+
"ip6-localhost",
|
|
617
|
+
"ip6-loopback",
|
|
618
|
+
"metadata.google.internal",
|
|
619
|
+
"metadata"
|
|
620
|
+
]);
|
|
621
|
+
var HOSTNAME_DENYLIST_SUFFIX = [".internal", ".local", ".localhost", ".lan"];
|
|
622
|
+
async function defaultResolver(host) {
|
|
623
|
+
return lookup(host, { all: true });
|
|
624
|
+
}
|
|
625
|
+
function matchesExtraExact(host, extras) {
|
|
626
|
+
if (!extras) return false;
|
|
627
|
+
for (const entry of extras) {
|
|
628
|
+
if (entry.toLowerCase() === host) return true;
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
function isUnsafeAddress(address, opts) {
|
|
633
|
+
if (isIPv4(address)) {
|
|
634
|
+
if (isUnsafeIPv4(address)) return true;
|
|
635
|
+
return opts?.extraUnsafeIPv4?.(address) === true;
|
|
636
|
+
}
|
|
637
|
+
if (isIPv6(address)) {
|
|
638
|
+
if (isUnsafeIPv6(address)) return true;
|
|
639
|
+
return opts?.extraUnsafeIPv6?.(address) === true;
|
|
640
|
+
}
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
function isUnsafeIPv4(address) {
|
|
644
|
+
const parts = address.split(".").map(Number);
|
|
645
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isFinite(p) || p < 0 || p > 255)) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
const [a, b, c] = parts;
|
|
649
|
+
if (a === 0) return true;
|
|
650
|
+
if (a === 10) return true;
|
|
651
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
652
|
+
if (a === 127) return true;
|
|
653
|
+
if (a === 169 && b === 254) return true;
|
|
654
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
655
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2)) return true;
|
|
656
|
+
if (a === 192 && b === 168) return true;
|
|
657
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
658
|
+
if (a >= 224 && a <= 239) return true;
|
|
659
|
+
if (a >= 240) return true;
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
function isUnsafeIPv6(address) {
|
|
663
|
+
const lower = address.toLowerCase();
|
|
664
|
+
if (lower === "::1") return true;
|
|
665
|
+
if (lower === "::") return true;
|
|
666
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb")) {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
670
|
+
if (lower.startsWith("::ffff:")) {
|
|
671
|
+
const tail = lower.slice("::ffff:".length);
|
|
672
|
+
if (isIPv4(tail)) return isUnsafeIPv4(tail);
|
|
673
|
+
const m = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(tail);
|
|
674
|
+
if (m) {
|
|
675
|
+
const high = parseInt(m[1], 16);
|
|
676
|
+
const low = parseInt(m[2], 16);
|
|
677
|
+
const a = high >> 8 & 255;
|
|
678
|
+
const b = high & 255;
|
|
679
|
+
const c = low >> 8 & 255;
|
|
680
|
+
const d = low & 255;
|
|
681
|
+
return isUnsafeIPv4(`${a.toString()}.${b.toString()}.${c.toString()}.${d.toString()}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (lower.startsWith("ff")) return true;
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export {
|
|
689
|
+
isZodSchema,
|
|
690
|
+
RAIL_REFUNDABLE,
|
|
691
|
+
DEFAULT_CREDIT_MIN,
|
|
692
|
+
DEFAULT_CREDIT_MAX,
|
|
693
|
+
DEFAULT_CREDIT_TTL_SECONDS,
|
|
694
|
+
envFlag,
|
|
695
|
+
canonicalize,
|
|
696
|
+
canonicaliseForSigning,
|
|
697
|
+
SIGNED_REQUEST_AUTH_ID,
|
|
698
|
+
SIGNED_REQUEST_STATEMENT_VERSION,
|
|
699
|
+
signedRequestStatementHeader,
|
|
700
|
+
canonicalRequestPath,
|
|
701
|
+
humanizeZodError,
|
|
702
|
+
humanizeZodIssues,
|
|
703
|
+
zodIssuesOf,
|
|
704
|
+
SIGNED_ENVELOPE_FIELDS,
|
|
705
|
+
SignedRequestError,
|
|
706
|
+
createSignedRequestVerifier,
|
|
707
|
+
signedRequestInput,
|
|
708
|
+
createDefaultReplayStore,
|
|
709
|
+
SSRFError,
|
|
710
|
+
assertSafeUrl,
|
|
711
|
+
createPinnedFetch
|
|
712
|
+
};
|