@nodana/phoenixd-mpp-sdk 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server/index.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ ChargeError: () => ChargeError,
24
+ Mppx: () => import_server2.Mppx,
25
+ charge: () => charge2,
26
+ memoryStore: () => memoryStore,
27
+ phoenixd: () => methods_exports,
28
+ redisStore: () => redisStore
29
+ });
30
+ module.exports = __toCommonJS(server_exports);
31
+ var import_server2 = require("mppx/server");
32
+
33
+ // src/server/methods.ts
34
+ var methods_exports = {};
35
+ __export(methods_exports, {
36
+ charge: () => charge2
37
+ });
38
+
39
+ // src/server/charge.ts
40
+ var import_mppx3 = require("mppx");
41
+ var import_server = require("mppx/server");
42
+
43
+ // src/Methods.ts
44
+ var import_mppx = require("mppx");
45
+ var charge = import_mppx.Method.from({
46
+ name: "lightning",
47
+ intent: "charge",
48
+ schema: {
49
+ credential: {
50
+ payload: import_mppx.z.object({
51
+ preimage: import_mppx.z.string().check(import_mppx.z.regex(/^[0-9a-f]{64}$/))
52
+ })
53
+ },
54
+ request: import_mppx.z.object({
55
+ amount: import_mppx.z.string().check(import_mppx.z.regex(/^[1-9][0-9]*$/)),
56
+ currency: import_mppx.z.literal("sat"),
57
+ description: import_mppx.z.optional(import_mppx.z.string()),
58
+ methodDetails: import_mppx.z.object({
59
+ invoice: import_mppx.z.string(),
60
+ paymentHash: import_mppx.z.optional(import_mppx.z.string().check(import_mppx.z.regex(/^[0-9a-f]{64}$/))),
61
+ network: import_mppx.z.optional(import_mppx.z.enum(["mainnet", "regtest", "signet"]))
62
+ })
63
+ })
64
+ }
65
+ });
66
+
67
+ // src/errors.ts
68
+ var import_mppx2 = require("mppx");
69
+ var ChargeError = class extends import_mppx2.Errors.PaymentError {
70
+ constructor(code, message) {
71
+ super(message);
72
+ this.code = code;
73
+ const problem = code === "store_unavailable" || code === "invalid_invoice" ? "internal-payment-error" : code === "invoice_expired" ? "payment-expired" : code === "invalid_preimage" || code === "already_consumed" ? "verification-failed" : "invalid-challenge";
74
+ this.type = `https://paymentauth.org/problems/${problem}`;
75
+ this.status = code === "store_unavailable" ? 503 : code === "invalid_invoice" ? 502 : 402;
76
+ }
77
+ code;
78
+ name = "ChargeError";
79
+ title = "Lightning payment rejected";
80
+ type;
81
+ status;
82
+ };
83
+
84
+ // src/shared.ts
85
+ var import_node_crypto = require("crypto");
86
+ function satoshis(amount) {
87
+ if (!/^[1-9][0-9]*$/.test(amount))
88
+ throw new TypeError("amount must be a positive integer string in satoshis");
89
+ const value = Number(amount);
90
+ if (!Number.isSafeInteger(value * 1e3))
91
+ throw new TypeError(
92
+ "amount exceeds the safely representable millisatoshi range"
93
+ );
94
+ return value;
95
+ }
96
+ function paymentHash(preimage) {
97
+ if (!/^[0-9a-f]{64}$/.test(preimage))
98
+ throw new TypeError("Invalid payment preimage");
99
+ return (0, import_node_crypto.createHash)("sha256").update(Buffer.from(preimage, "hex")).digest("hex");
100
+ }
101
+ function invoiceDetails(invoice, request, expected) {
102
+ if (request.currency !== "sat" || invoice.amount !== satoshis(request.amount) * 1e3)
103
+ throw new TypeError("Invoice amount does not match the requested satoshis");
104
+ if (!/^[0-9a-f]{64}$/.test(invoice.paymentHash))
105
+ throw new TypeError("Invoice has an invalid payment hash");
106
+ const network = invoice.chain;
107
+ if (network !== "mainnet" && network !== "regtest" && network !== "signet")
108
+ throw new TypeError("Invoice uses an unsupported Lightning network");
109
+ if (expected !== void 0 && network !== expected)
110
+ throw new TypeError(
111
+ "Invoice network does not match the configured network"
112
+ );
113
+ const expiresAt = (invoice.timestampSeconds + (invoice.expirySeconds ?? 3600)) * 1e3;
114
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())
115
+ throw new TypeError("Invoice is expired or has an invalid expiry");
116
+ return { paymentHash: invoice.paymentHash, network, expiresAt };
117
+ }
118
+ async function bodyHash(request) {
119
+ return (0, import_node_crypto.createHash)("sha256").update(Buffer.from(await request.clone().arrayBuffer())).digest("hex");
120
+ }
121
+
122
+ // src/server/store.ts
123
+ var validId = (id) => typeof id === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(id);
124
+ function assertRecord(value) {
125
+ if (typeof value !== "object" || value === null)
126
+ throw new TypeError("Invalid stored challenge");
127
+ const record = value;
128
+ if (!validId(record.id) || !/^[0-9a-f]{64}$/.test(record.paymentHash) || !/^[0-9a-f]{64}$/.test(record.bodyHash) || !Number.isSafeInteger(record.amountSat) || record.amountSat < 1 || !Number.isSafeInteger(record.expiresAt) || typeof record.serializedChallenge !== "string" || typeof record.requestUrl !== "string" || typeof record.requestMethod !== "string") {
129
+ throw new TypeError("Invalid stored challenge");
130
+ }
131
+ }
132
+ function memoryStore(options = {}) {
133
+ const maxEntries = options.maxEntries ?? 1e4;
134
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1)
135
+ throw new TypeError("maxEntries must be positive");
136
+ const entries = /* @__PURE__ */ new Map();
137
+ const hashes = /* @__PURE__ */ new Set();
138
+ const prune = () => {
139
+ let removed = 0;
140
+ for (const [id, entry] of entries)
141
+ if (entry.charge.expiresAt <= Date.now()) {
142
+ entries.delete(id);
143
+ hashes.delete(entry.charge.paymentHash);
144
+ removed++;
145
+ }
146
+ return removed;
147
+ };
148
+ return {
149
+ async issue(charge3) {
150
+ assertRecord(charge3);
151
+ if (charge3.expiresAt <= Date.now()) throw new Error("Charge has expired");
152
+ prune();
153
+ if (entries.size >= maxEntries)
154
+ throw new Error("Charge store capacity reached");
155
+ if (entries.has(charge3.id) || hashes.has(charge3.paymentHash))
156
+ throw new Error("Charge already issued");
157
+ entries.set(charge3.id, {
158
+ charge: structuredClone(charge3),
159
+ consumed: false
160
+ });
161
+ hashes.add(charge3.paymentHash);
162
+ },
163
+ async get(id) {
164
+ return entries.has(id) ? structuredClone(entries.get(id).charge) : void 0;
165
+ },
166
+ async consume(id) {
167
+ const entry = entries.get(id);
168
+ if (!entry || entry.consumed || entry.charge.expiresAt <= Date.now())
169
+ return false;
170
+ entry.consumed = true;
171
+ return true;
172
+ },
173
+ async pruneExpired() {
174
+ return prune();
175
+ }
176
+ };
177
+ }
178
+ var issueScript = `
179
+ local time = redis.call('TIME')
180
+ local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
181
+ local expires = tonumber(ARGV[2])
182
+ if expires <= now then return -1 end
183
+ if redis.call('EXISTS', KEYS[1]) == 1 or redis.call('EXISTS', KEYS[2]) == 1 then return 0 end
184
+ redis.call('HSET', KEYS[1], 'record', ARGV[1], 'expires', ARGV[2], 'consumed', '0')
185
+ redis.call('PEXPIREAT', KEYS[1], ARGV[2])
186
+ redis.call('SET', KEYS[2], ARGV[3])
187
+ redis.call('PEXPIREAT', KEYS[2], ARGV[2])
188
+ return 1
189
+ `;
190
+ var consumeScript = `
191
+ if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end
192
+ local expires = tonumber(redis.call('HGET', KEYS[1], 'expires'))
193
+ local consumed = redis.call('HGET', KEYS[1], 'consumed')
194
+ if not expires or (consumed ~= '0' and consumed ~= '1') then
195
+ return redis.error_reply('Invalid stored payment challenge')
196
+ end
197
+ local time = redis.call('TIME')
198
+ local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
199
+ if expires <= now or consumed == '1' then return 0 end
200
+ redis.call('HSET', KEYS[1], 'consumed', '1')
201
+ return 1
202
+ `;
203
+ function redisStore(client, options = {}) {
204
+ const prefix = options.keyPrefix ?? "phoenixd-mpp";
205
+ if (!/^[A-Za-z0-9:_-]{1,128}$/.test(prefix))
206
+ throw new TypeError("Invalid Redis keyPrefix");
207
+ const root = `{${prefix}}:`;
208
+ const key = (id) => `${root}challenge:${id}`;
209
+ return {
210
+ async issue(charge3) {
211
+ assertRecord(charge3);
212
+ const result = await client.sendCommand([
213
+ "EVAL",
214
+ issueScript,
215
+ "2",
216
+ key(charge3.id),
217
+ `${root}hash:${charge3.paymentHash}`,
218
+ JSON.stringify(charge3),
219
+ String(charge3.expiresAt),
220
+ charge3.id
221
+ ]);
222
+ if (result === -1) throw new Error("Charge has expired");
223
+ if (result === 0) throw new Error("Charge already issued");
224
+ if (result !== 1) throw new Error("Unexpected Redis issuance response");
225
+ },
226
+ async get(id) {
227
+ if (!validId(id)) return void 0;
228
+ const value = await client.sendCommand(["HGET", key(id), "record"]);
229
+ if (value === null) return void 0;
230
+ if (typeof value !== "string")
231
+ throw new TypeError("Invalid Redis record response");
232
+ const record = JSON.parse(value);
233
+ assertRecord(record);
234
+ if (record.id !== id)
235
+ throw new TypeError("Stored challenge ID does not match");
236
+ return record;
237
+ },
238
+ async consume(id) {
239
+ if (!validId(id)) return false;
240
+ const result = await client.sendCommand([
241
+ "EVAL",
242
+ consumeScript,
243
+ "1",
244
+ key(id)
245
+ ]);
246
+ if (result !== 0 && result !== 1)
247
+ throw new Error("Unexpected Redis consumption response");
248
+ return result === 1;
249
+ }
250
+ };
251
+ }
252
+
253
+ // src/server/charge.ts
254
+ var defaults = { currency: "sat", methodDetails: { invoice: "" } };
255
+ function charge2(options) {
256
+ const {
257
+ client,
258
+ store = memoryStore(),
259
+ network,
260
+ invoiceExpirySeconds = 3600
261
+ } = options;
262
+ if (!Number.isSafeInteger(invoiceExpirySeconds) || invoiceExpirySeconds < 1)
263
+ throw new TypeError("invoiceExpirySeconds must be a positive integer");
264
+ const byInput = /* @__PURE__ */ new WeakMap();
265
+ const byCaptured = /* @__PURE__ */ new WeakMap();
266
+ const http = import_server.Transport.http();
267
+ const transport = {
268
+ ...http,
269
+ async respondChallenge(parameters) {
270
+ const context = byInput.get(parameters.input);
271
+ if (!context)
272
+ throw new ChargeError(
273
+ "invalid_challenge",
274
+ "An HTTP payment handler is required"
275
+ );
276
+ const original = parameters.challenge;
277
+ const request = charge.schema.request.parse(original.request);
278
+ const amountSat = satoshis(request.amount);
279
+ let details;
280
+ let serialized;
281
+ try {
282
+ const invoice = await client.createInvoice({
283
+ amountSat,
284
+ description: original.description ?? request.description ?? "",
285
+ expirySeconds: invoiceExpirySeconds
286
+ });
287
+ serialized = invoice.serialized;
288
+ details = invoiceDetails(
289
+ await client.decodeInvoice({ invoice: serialized }),
290
+ request,
291
+ network
292
+ );
293
+ if (invoice.paymentHash !== details.paymentHash || invoice.amountSat !== amountSat)
294
+ throw new TypeError(
295
+ "Generated invoice does not match the node response"
296
+ );
297
+ } catch {
298
+ throw new ChargeError(
299
+ "invalid_invoice",
300
+ "Phoenixd could not provide a matching, unexpired invoice"
301
+ );
302
+ }
303
+ const requestedExpiry = original.expires === void 0 ? Date.now() + 3e5 : Date.parse(original.expires);
304
+ const expiresAt = Math.min(requestedExpiry, details.expiresAt);
305
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now())
306
+ throw new ChargeError(
307
+ "invoice_expired",
308
+ "The configured challenge expiry is in the past"
309
+ );
310
+ const issuedRequest = {
311
+ ...request,
312
+ methodDetails: {
313
+ invoice: serialized,
314
+ paymentHash: details.paymentHash,
315
+ network: details.network
316
+ }
317
+ };
318
+ const issued = import_mppx3.Challenge.from({
319
+ secretKey: context.secretKey,
320
+ realm: original.realm,
321
+ method: "lightning",
322
+ intent: "charge",
323
+ request: issuedRequest,
324
+ expires: new Date(expiresAt),
325
+ ...original.description === void 0 ? {} : { description: original.description },
326
+ ...original.opaque === void 0 ? {} : { opaque: original.opaque },
327
+ ...original.digest === void 0 ? {} : { digest: original.digest },
328
+ ...original.header === void 0 ? {} : { header: original.header }
329
+ });
330
+ Object.assign(original.request, issuedRequest);
331
+ Object.assign(original, issued, { request: original.request });
332
+ const record = {
333
+ id: original.id,
334
+ serializedChallenge: import_mppx3.Challenge.serialize(original),
335
+ paymentHash: details.paymentHash,
336
+ amountSat,
337
+ expiresAt,
338
+ requestUrl: parameters.input.url,
339
+ requestMethod: parameters.input.method,
340
+ bodyHash: context.bodyHash
341
+ };
342
+ try {
343
+ await store.issue(record);
344
+ } catch {
345
+ throw new ChargeError(
346
+ "store_unavailable",
347
+ "Could not persist the payment challenge"
348
+ );
349
+ }
350
+ return http.respondChallenge({
351
+ ...parameters,
352
+ error: parameters.error,
353
+ challenge: original
354
+ });
355
+ },
356
+ respondReceipt(parameters) {
357
+ const response = http.respondReceipt(parameters);
358
+ response.headers.set("Cache-Control", "no-store");
359
+ return response;
360
+ }
361
+ };
362
+ async function validate(parameters) {
363
+ const { credential, envelope, request } = parameters;
364
+ const context = envelope === void 0 ? void 0 : byCaptured.get(envelope.capturedRequest);
365
+ if (!context)
366
+ throw new ChargeError(
367
+ "invalid_challenge",
368
+ "Use the configured HTTP payment handler"
369
+ );
370
+ let record;
371
+ try {
372
+ record = await store.get(credential.challenge.id);
373
+ } catch {
374
+ throw new ChargeError(
375
+ "store_unavailable",
376
+ "Could not read the payment challenge"
377
+ );
378
+ }
379
+ if (!record)
380
+ throw new ChargeError(
381
+ "unknown_challenge",
382
+ "The payment challenge is unknown"
383
+ );
384
+ if (Date.now() >= record.expiresAt)
385
+ throw new ChargeError(
386
+ "invoice_expired",
387
+ "The payment challenge has expired"
388
+ );
389
+ if (record.serializedChallenge !== import_mppx3.Challenge.serialize(credential.challenge) || record.amountSat !== satoshis(request.amount) || request.currency !== "sat") {
390
+ throw new ChargeError(
391
+ "invalid_challenge",
392
+ "The credential does not match the issued payment challenge"
393
+ );
394
+ }
395
+ if (record.requestUrl !== context.input.url || record.requestMethod !== context.input.method || record.bodyHash !== context.bodyHash) {
396
+ throw new ChargeError(
397
+ "invalid_challenge",
398
+ "The credential does not match this request"
399
+ );
400
+ }
401
+ let hash;
402
+ try {
403
+ hash = paymentHash(credential.payload.preimage);
404
+ } catch {
405
+ throw new ChargeError("invalid_preimage", "The payment proof is invalid");
406
+ }
407
+ if (hash !== record.paymentHash)
408
+ throw new ChargeError("invalid_preimage", "The payment proof is invalid");
409
+ return record;
410
+ }
411
+ return import_mppx3.Method.toServer(charge, {
412
+ defaults,
413
+ transport,
414
+ async preflight({ capturedRequest, input, secretKey }) {
415
+ const context = {
416
+ input,
417
+ secretKey,
418
+ bodyHash: await bodyHash(input)
419
+ };
420
+ byInput.set(input, context);
421
+ if (capturedRequest) byCaptured.set(capturedRequest, context);
422
+ return void 0;
423
+ },
424
+ stableBinding: (request) => ({
425
+ amount: request.amount,
426
+ currency: request.currency
427
+ }),
428
+ async validate(parameters) {
429
+ await validate(parameters);
430
+ return {
431
+ challenge: parameters.credential.challenge,
432
+ credential: parameters.credential,
433
+ details: {},
434
+ intent: "charge",
435
+ method: "lightning",
436
+ request: parameters.credential.challenge.request
437
+ };
438
+ },
439
+ async broadcast(parameters) {
440
+ const record = await validate(parameters);
441
+ let consumed;
442
+ try {
443
+ consumed = await store.consume(record.id);
444
+ } catch {
445
+ throw new ChargeError(
446
+ "store_unavailable",
447
+ "Could not consume the payment challenge"
448
+ );
449
+ }
450
+ if (!consumed)
451
+ throw new ChargeError(
452
+ "already_consumed",
453
+ "The payment challenge has already been used or expired"
454
+ );
455
+ return import_mppx3.Receipt.from({
456
+ method: "lightning",
457
+ reference: record.paymentHash,
458
+ status: "success",
459
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
460
+ });
461
+ }
462
+ });
463
+ }
464
+ // Annotate the CommonJS export names for ESM import in node:
465
+ 0 && (module.exports = {
466
+ ChargeError,
467
+ Mppx,
468
+ charge,
469
+ memoryStore,
470
+ phoenixd,
471
+ redisStore
472
+ });
473
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/index.ts","../../src/server/methods.ts","../../src/server/charge.ts","../../src/Methods.ts","../../src/errors.ts","../../src/shared.ts","../../src/server/store.ts"],"sourcesContent":["export { Mppx } from \"mppx/server\";\nexport * as phoenixd from \"./methods\";\nexport { charge } from \"./charge\";\nexport type { ChargeOptions, ServerChargeMethod } from \"./charge\";\nexport { redisStore, memoryStore } from \"./store\";\nexport type {\n ChargeStore,\n ManagedChargeStore,\n StoredCharge,\n RedisClient,\n RedisStoreOptions,\n} from \"./store\";\nexport { ChargeError } from \"../errors\";\n","export { charge } from \"./charge\";\n","import { Challenge, Method, Receipt } from \"mppx\";\nimport { Transport } from \"mppx/server\";\nimport type { PhoenixdClient } from \"@nodana/phoenixd-ts\";\nimport * as Methods from \"../Methods\";\nimport { ChargeError } from \"../errors\";\nimport { bodyHash, invoiceDetails, paymentHash, satoshis } from \"../shared\";\nimport { memoryStore } from \"./store\";\nimport type { ChargeStore, StoredCharge } from \"./store\";\n\nexport interface ChargeOptions {\n client: Pick<PhoenixdClient, \"createInvoice\" | \"decodeInvoice\">;\n /** Use a shared durable store for deployed services. Defaults to process-local memory. */\n store?: ChargeStore;\n /** Invoice lifetime in seconds. Defaults to 3600; challenge expiry is capped to it. */\n invoiceExpirySeconds?: number;\n network?: Methods.LightningNetwork;\n}\n\nconst defaults = { currency: \"sat\", methodDetails: { invoice: \"\" } } as const;\n\nexport type ServerChargeMethod = Method.Server<\n typeof Methods.charge,\n typeof defaults,\n Transport.Http\n>;\n\ninterface HttpContext {\n input: Request;\n secretKey: string;\n bodyHash: string;\n}\n\nexport function charge(options: ChargeOptions): ServerChargeMethod {\n const {\n client,\n store = memoryStore(),\n network,\n invoiceExpirySeconds = 3600,\n } = options;\n\n if (!Number.isSafeInteger(invoiceExpirySeconds) || invoiceExpirySeconds < 1)\n throw new TypeError(\"invoiceExpirySeconds must be a positive integer\");\n\n const byInput = new WeakMap<Request, HttpContext>();\n const byCaptured = new WeakMap<Method.CapturedRequest, HttpContext>();\n const http = Transport.http();\n\n const transport: Transport.Http = {\n ...http,\n\n async respondChallenge(parameters) {\n const context = byInput.get(parameters.input);\n\n if (!context)\n throw new ChargeError(\n \"invalid_challenge\",\n \"An HTTP payment handler is required\",\n );\n\n const original = parameters.challenge;\n const request = Methods.charge.schema.request.parse(original.request);\n const amountSat = satoshis(request.amount);\n let details: ReturnType<typeof invoiceDetails>;\n let serialized: string;\n\n try {\n const invoice = await client.createInvoice({\n amountSat,\n description: original.description ?? request.description ?? \"\",\n expirySeconds: invoiceExpirySeconds,\n });\n\n serialized = invoice.serialized;\n details = invoiceDetails(\n await client.decodeInvoice({ invoice: serialized }),\n request,\n network,\n );\n\n if (\n invoice.paymentHash !== details.paymentHash ||\n invoice.amountSat !== amountSat\n )\n throw new TypeError(\n \"Generated invoice does not match the node response\",\n );\n } catch {\n throw new ChargeError(\n \"invalid_invoice\",\n \"Phoenixd could not provide a matching, unexpired invoice\",\n );\n }\n\n const requestedExpiry =\n original.expires === undefined\n ? Date.now() + 300_000\n : Date.parse(original.expires);\n const expiresAt = Math.min(requestedExpiry, details.expiresAt);\n\n if (!Number.isFinite(expiresAt) || expiresAt <= Date.now())\n throw new ChargeError(\n \"invoice_expired\",\n \"The configured challenge expiry is in the past\",\n );\n\n const issuedRequest = {\n ...request,\n methodDetails: {\n invoice: serialized,\n paymentHash: details.paymentHash,\n network: details.network,\n },\n };\n const issued = Challenge.from({\n secretKey: context.secretKey,\n realm: original.realm,\n method: \"lightning\",\n intent: \"charge\",\n request: issuedRequest,\n expires: new Date(expiresAt),\n ...(original.description === undefined\n ? {}\n : { description: original.description }),\n ...(original.opaque === undefined ? {} : { opaque: original.opaque }),\n ...(original.digest === undefined ? {} : { digest: original.digest }),\n ...(original.header === undefined ? {} : { header: original.header }),\n });\n\n // Enrich the framework's challenge in place so its lifecycle event sees\n // the same issued ID/request as the HTTP response. No invoice is created\n // on a successful credential retry; only challenge responses need one.\n Object.assign(original.request, issuedRequest);\n Object.assign(original, issued, { request: original.request });\n\n const record: StoredCharge = {\n id: original.id,\n serializedChallenge: Challenge.serialize(original),\n paymentHash: details.paymentHash,\n amountSat,\n expiresAt,\n requestUrl: parameters.input.url,\n requestMethod: parameters.input.method,\n bodyHash: context.bodyHash,\n };\n\n try {\n await store.issue(record);\n } catch {\n throw new ChargeError(\n \"store_unavailable\",\n \"Could not persist the payment challenge\",\n );\n }\n\n // Rejected credentials always receive a fresh invoice/challenge. Neither\n // malformed payloads nor a consumed preimage can trigger reuse.\n return http.respondChallenge({\n ...parameters,\n error: parameters.error,\n challenge: original,\n });\n },\n\n respondReceipt(parameters) {\n const response = http.respondReceipt(parameters);\n\n response.headers.set(\"Cache-Control\", \"no-store\");\n\n return response;\n },\n };\n\n async function validate(\n parameters: Method.VerifyContext<typeof Methods.charge>,\n ): Promise<StoredCharge> {\n const { credential, envelope, request } = parameters;\n const context =\n envelope === undefined\n ? undefined\n : byCaptured.get(envelope.capturedRequest);\n\n if (!context)\n throw new ChargeError(\n \"invalid_challenge\",\n \"Use the configured HTTP payment handler\",\n );\n\n let record: StoredCharge | undefined;\n\n try {\n record = await store.get(credential.challenge.id);\n } catch {\n throw new ChargeError(\n \"store_unavailable\",\n \"Could not read the payment challenge\",\n );\n }\n\n if (!record)\n throw new ChargeError(\n \"unknown_challenge\",\n \"The payment challenge is unknown\",\n );\n\n if (Date.now() >= record.expiresAt)\n throw new ChargeError(\n \"invoice_expired\",\n \"The payment challenge has expired\",\n );\n\n if (\n record.serializedChallenge !==\n Challenge.serialize(credential.challenge) ||\n record.amountSat !== satoshis(request.amount) ||\n request.currency !== \"sat\"\n ) {\n throw new ChargeError(\n \"invalid_challenge\",\n \"The credential does not match the issued payment challenge\",\n );\n }\n\n if (\n record.requestUrl !== context.input.url ||\n record.requestMethod !== context.input.method ||\n record.bodyHash !== context.bodyHash\n ) {\n throw new ChargeError(\n \"invalid_challenge\",\n \"The credential does not match this request\",\n );\n }\n\n let hash: string;\n\n try {\n hash = paymentHash(credential.payload.preimage);\n } catch {\n throw new ChargeError(\"invalid_preimage\", \"The payment proof is invalid\");\n }\n\n if (hash !== record.paymentHash)\n throw new ChargeError(\"invalid_preimage\", \"The payment proof is invalid\");\n\n return record;\n }\n\n return Method.toServer(Methods.charge, {\n defaults,\n transport,\n\n async preflight({ capturedRequest, input, secretKey }) {\n const context: HttpContext = {\n input,\n secretKey,\n bodyHash: await bodyHash(input),\n };\n\n byInput.set(input, context);\n\n if (capturedRequest) byCaptured.set(capturedRequest, context);\n\n return undefined;\n },\n\n stableBinding: (request) => ({\n amount: request.amount,\n currency: request.currency,\n }),\n\n async validate(parameters) {\n await validate(parameters);\n\n return {\n challenge: parameters.credential.challenge,\n credential: parameters.credential,\n details: {},\n intent: \"charge\",\n method: \"lightning\",\n request: parameters.credential.challenge.request,\n };\n },\n\n async broadcast(parameters) {\n const record = await validate(parameters);\n let consumed: boolean;\n\n try {\n consumed = await store.consume(record.id);\n } catch {\n throw new ChargeError(\n \"store_unavailable\",\n \"Could not consume the payment challenge\",\n );\n }\n\n if (!consumed)\n throw new ChargeError(\n \"already_consumed\",\n \"The payment challenge has already been used or expired\",\n );\n\n return Receipt.from({\n method: \"lightning\",\n reference: record.paymentHash,\n status: \"success\",\n timestamp: new Date().toISOString(),\n });\n },\n });\n}\n","// Lightning wire schema adapted from buildonspark/lightning-mpp-sdk (MIT).\n// Copyright (c) 2026 Lightspark Group, Inc. See NOTICE.\nimport { Method, z } from \"mppx\";\n\nexport const charge = Method.from({\n name: \"lightning\",\n intent: \"charge\",\n schema: {\n credential: {\n payload: z.object({\n preimage: z.string().check(z.regex(/^[0-9a-f]{64}$/)),\n }),\n },\n request: z.object({\n amount: z.string().check(z.regex(/^[1-9][0-9]*$/)),\n currency: z.literal(\"sat\"),\n description: z.optional(z.string()),\n methodDetails: z.object({\n invoice: z.string(),\n paymentHash: z.optional(z.string().check(z.regex(/^[0-9a-f]{64}$/))),\n network: z.optional(z.enum([\"mainnet\", \"regtest\", \"signet\"])),\n }),\n }),\n },\n});\n\nexport type ChargeRequest = z.output<typeof charge.schema.request>;\n\nexport type LightningNetwork = NonNullable<\n ChargeRequest[\"methodDetails\"][\"network\"]\n>;\n","import { Errors } from \"mppx\";\n\nexport type ChargeErrorCode =\n | \"unknown_challenge\"\n | \"invalid_challenge\"\n | \"invalid_preimage\"\n | \"invoice_expired\"\n | \"already_consumed\"\n | \"invalid_invoice\"\n | \"store_unavailable\";\n\nexport class ChargeError extends Errors.PaymentError {\n readonly name = \"ChargeError\";\n readonly title = \"Lightning payment rejected\";\n readonly type: string;\n override readonly status: number;\n\n constructor(\n public readonly code: ChargeErrorCode,\n message: string,\n ) {\n super(message);\n\n const problem =\n code === \"store_unavailable\" || code === \"invalid_invoice\"\n ? \"internal-payment-error\"\n : code === \"invoice_expired\"\n ? \"payment-expired\"\n : code === \"invalid_preimage\" || code === \"already_consumed\"\n ? \"verification-failed\"\n : \"invalid-challenge\";\n\n this.type = `https://paymentauth.org/problems/${problem}`;\n this.status =\n code === \"store_unavailable\"\n ? 503\n : code === \"invalid_invoice\"\n ? 502\n : 402;\n }\n}\n\nexport type ClientErrorCode =\n \"invalid_invoice\" | \"amount_limit\" | \"payment_failed\" | \"payment_unknown\";\n\nexport class PhoenixdPaymentError extends Error {\n readonly name = \"PhoenixdPaymentError\";\n\n constructor(\n public readonly code: ClientErrorCode,\n message: string,\n ) {\n super(message);\n }\n}\n","import { createHash } from \"node:crypto\";\nimport type { DecodedInvoice } from \"@nodana/phoenixd-ts\";\nimport type { ChargeRequest, LightningNetwork } from \"./Methods\";\n\nexport function satoshis(amount: string): number {\n if (!/^[1-9][0-9]*$/.test(amount))\n throw new TypeError(\"amount must be a positive integer string in satoshis\");\n\n const value = Number(amount);\n\n if (!Number.isSafeInteger(value * 1000))\n throw new TypeError(\n \"amount exceeds the safely representable millisatoshi range\",\n );\n\n return value;\n}\n\nexport function paymentHash(preimage: string): string {\n if (!/^[0-9a-f]{64}$/.test(preimage))\n throw new TypeError(\"Invalid payment preimage\");\n\n return createHash(\"sha256\")\n .update(Buffer.from(preimage, \"hex\"))\n .digest(\"hex\");\n}\n\nexport function invoiceDetails(\n invoice: DecodedInvoice,\n request: Pick<ChargeRequest, \"amount\" | \"currency\">,\n expected?: LightningNetwork,\n): { paymentHash: string; network: LightningNetwork; expiresAt: number } {\n if (\n request.currency !== \"sat\" ||\n invoice.amount !== satoshis(request.amount) * 1000\n )\n throw new TypeError(\"Invoice amount does not match the requested satoshis\");\n\n if (!/^[0-9a-f]{64}$/.test(invoice.paymentHash))\n throw new TypeError(\"Invoice has an invalid payment hash\");\n\n const network = invoice.chain;\n\n if (network !== \"mainnet\" && network !== \"regtest\" && network !== \"signet\")\n throw new TypeError(\"Invoice uses an unsupported Lightning network\");\n\n if (expected !== undefined && network !== expected)\n throw new TypeError(\n \"Invoice network does not match the configured network\",\n );\n\n const expiresAt =\n (invoice.timestampSeconds + (invoice.expirySeconds ?? 3600)) * 1000;\n\n if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())\n throw new TypeError(\"Invoice is expired or has an invalid expiry\");\n\n return { paymentHash: invoice.paymentHash, network, expiresAt };\n}\n\nexport async function bodyHash(request: Request): Promise<string> {\n // Clone preserves the body for the application handler.\n return createHash(\"sha256\")\n .update(Buffer.from(await request.clone().arrayBuffer()))\n .digest(\"hex\");\n}\n","/** Stored values contain no payment preimages or node credentials. */\nexport interface StoredCharge {\n readonly id: string;\n readonly serializedChallenge: string;\n readonly paymentHash: string;\n readonly amountSat: number;\n readonly expiresAt: number;\n readonly requestUrl: string;\n readonly requestMethod: string;\n readonly bodyHash: string;\n}\n\nexport interface ChargeStore {\n /** Persist before responding with a challenge. Reject reused IDs/payment hashes. */\n issue(charge: StoredCharge): Promise<void>;\n get(id: string): Promise<StoredCharge | undefined>;\n /** Atomically succeeds once, and only while the stored challenge is unexpired. */\n consume(id: string): Promise<boolean>;\n}\n\nexport interface ManagedChargeStore extends ChargeStore {\n pruneExpired(): Promise<number>;\n}\n\nconst validId = (id: string) =>\n typeof id === \"string\" && /^[A-Za-z0-9_-]{1,128}$/.test(id);\n\nfunction assertRecord(value: unknown): asserts value is StoredCharge {\n if (typeof value !== \"object\" || value === null)\n throw new TypeError(\"Invalid stored challenge\");\n\n const record = value as StoredCharge;\n\n if (\n !validId(record.id) ||\n !/^[0-9a-f]{64}$/.test(record.paymentHash) ||\n !/^[0-9a-f]{64}$/.test(record.bodyHash) ||\n !Number.isSafeInteger(record.amountSat) ||\n record.amountSat < 1 ||\n !Number.isSafeInteger(record.expiresAt) ||\n typeof record.serializedChallenge !== \"string\" ||\n typeof record.requestUrl !== \"string\" ||\n typeof record.requestMethod !== \"string\"\n ) {\n throw new TypeError(\"Invalid stored challenge\");\n }\n}\n\nexport function memoryStore(\n options: { maxEntries?: number } = {},\n): ManagedChargeStore {\n const maxEntries = options.maxEntries ?? 10_000;\n\n if (!Number.isSafeInteger(maxEntries) || maxEntries < 1)\n throw new TypeError(\"maxEntries must be positive\");\n\n const entries = new Map<\n string,\n { charge: StoredCharge; consumed: boolean }\n >();\n const hashes = new Set<string>();\n\n const prune = () => {\n let removed = 0;\n\n for (const [id, entry] of entries)\n if (entry.charge.expiresAt <= Date.now()) {\n entries.delete(id);\n hashes.delete(entry.charge.paymentHash);\n removed++;\n }\n\n return removed;\n };\n\n return {\n async issue(charge) {\n assertRecord(charge);\n\n if (charge.expiresAt <= Date.now()) throw new Error(\"Charge has expired\");\n\n prune();\n\n if (entries.size >= maxEntries)\n throw new Error(\"Charge store capacity reached\");\n\n if (entries.has(charge.id) || hashes.has(charge.paymentHash))\n throw new Error(\"Charge already issued\");\n\n entries.set(charge.id, {\n charge: structuredClone(charge),\n consumed: false,\n });\n hashes.add(charge.paymentHash);\n },\n\n async get(id) {\n return entries.has(id)\n ? structuredClone(entries.get(id)!.charge)\n : undefined;\n },\n\n async consume(id) {\n const entry = entries.get(id);\n\n if (!entry || entry.consumed || entry.charge.expiresAt <= Date.now())\n return false;\n\n entry.consumed = true;\n\n return true;\n },\n\n async pruneExpired() {\n return prune();\n },\n };\n}\n\n/** Compatible with a connected node-redis client using its default reply types. */\nexport interface RedisClient {\n sendCommand(args: string[]): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n /** Namespace shared by instances of this service. Defaults to phoenixd-mpp. */\n keyPrefix?: string;\n}\n\n// Both keys share a hash tag. All checks and writes run in one Redis operation.\nconst issueScript = `\nlocal time = redis.call('TIME')\nlocal now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)\nlocal expires = tonumber(ARGV[2])\nif expires <= now then return -1 end\nif redis.call('EXISTS', KEYS[1]) == 1 or redis.call('EXISTS', KEYS[2]) == 1 then return 0 end\nredis.call('HSET', KEYS[1], 'record', ARGV[1], 'expires', ARGV[2], 'consumed', '0')\nredis.call('PEXPIREAT', KEYS[1], ARGV[2])\nredis.call('SET', KEYS[2], ARGV[3])\nredis.call('PEXPIREAT', KEYS[2], ARGV[2])\nreturn 1\n`;\n\nconst consumeScript = `\nif redis.call('EXISTS', KEYS[1]) == 0 then return 0 end\nlocal expires = tonumber(redis.call('HGET', KEYS[1], 'expires'))\nlocal consumed = redis.call('HGET', KEYS[1], 'consumed')\nif not expires or (consumed ~= '0' and consumed ~= '1') then\n return redis.error_reply('Invalid stored payment challenge')\nend\nlocal time = redis.call('TIME')\nlocal now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)\nif expires <= now or consumed == '1' then return 0 end\nredis.call('HSET', KEYS[1], 'consumed', '1')\nreturn 1\n`;\n\n/** Shared payment state; Redis expires records automatically. Does not own the connection. */\nexport function redisStore(\n client: RedisClient,\n options: RedisStoreOptions = {},\n): ChargeStore {\n const prefix = options.keyPrefix ?? \"phoenixd-mpp\";\n\n if (!/^[A-Za-z0-9:_-]{1,128}$/.test(prefix))\n throw new TypeError(\"Invalid Redis keyPrefix\");\n\n const root = `{${prefix}}:`;\n\n const key = (id: string) => `${root}challenge:${id}`;\n\n return {\n async issue(charge) {\n assertRecord(charge);\n\n // Serialize before awaiting so caller mutations cannot change issued state.\n const result = await client.sendCommand([\n \"EVAL\",\n issueScript,\n \"2\",\n key(charge.id),\n `${root}hash:${charge.paymentHash}`,\n JSON.stringify(charge),\n String(charge.expiresAt),\n charge.id,\n ]);\n\n if (result === -1) throw new Error(\"Charge has expired\");\n\n if (result === 0) throw new Error(\"Charge already issued\");\n\n if (result !== 1) throw new Error(\"Unexpected Redis issuance response\");\n },\n\n async get(id) {\n if (!validId(id)) return undefined;\n\n const value = await client.sendCommand([\"HGET\", key(id), \"record\"]);\n\n if (value === null) return undefined;\n\n if (typeof value !== \"string\")\n throw new TypeError(\"Invalid Redis record response\");\n\n const record: unknown = JSON.parse(value);\n\n assertRecord(record);\n\n if (record.id !== id)\n throw new TypeError(\"Stored challenge ID does not match\");\n\n return record;\n },\n\n async consume(id) {\n if (!validId(id)) return false;\n\n const result = await client.sendCommand([\n \"EVAL\",\n consumeScript,\n \"1\",\n key(id),\n ]);\n\n if (result !== 0 && result !== 1)\n throw new Error(\"Unexpected Redis consumption response\");\n\n return result === 1;\n },\n };\n}\n"],"mappings":"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","names":["charge","import_server","charge","import_mppx","import_mppx","charge","charge"]}
@@ -0,0 +1,65 @@
1
+ import { Transport } from 'mppx/server';
2
+ export { Mppx } from 'mppx/server';
3
+ import { Method } from 'mppx';
4
+ import { PhoenixdClient } from '@nodana/phoenixd-ts';
5
+ import { L as LightningNetwork, c as charge$1 } from '../index-3V4vmG3I.cjs';
6
+ export { C as ChargeError } from '../index-3V4vmG3I.cjs';
7
+
8
+ /** Stored values contain no payment preimages or node credentials. */
9
+ interface StoredCharge {
10
+ readonly id: string;
11
+ readonly serializedChallenge: string;
12
+ readonly paymentHash: string;
13
+ readonly amountSat: number;
14
+ readonly expiresAt: number;
15
+ readonly requestUrl: string;
16
+ readonly requestMethod: string;
17
+ readonly bodyHash: string;
18
+ }
19
+ interface ChargeStore {
20
+ /** Persist before responding with a challenge. Reject reused IDs/payment hashes. */
21
+ issue(charge: StoredCharge): Promise<void>;
22
+ get(id: string): Promise<StoredCharge | undefined>;
23
+ /** Atomically succeeds once, and only while the stored challenge is unexpired. */
24
+ consume(id: string): Promise<boolean>;
25
+ }
26
+ interface ManagedChargeStore extends ChargeStore {
27
+ pruneExpired(): Promise<number>;
28
+ }
29
+ declare function memoryStore(options?: {
30
+ maxEntries?: number;
31
+ }): ManagedChargeStore;
32
+ /** Compatible with a connected node-redis client using its default reply types. */
33
+ interface RedisClient {
34
+ sendCommand(args: string[]): Promise<unknown>;
35
+ }
36
+ interface RedisStoreOptions {
37
+ /** Namespace shared by instances of this service. Defaults to phoenixd-mpp. */
38
+ keyPrefix?: string;
39
+ }
40
+ /** Shared payment state; Redis expires records automatically. Does not own the connection. */
41
+ declare function redisStore(client: RedisClient, options?: RedisStoreOptions): ChargeStore;
42
+
43
+ interface ChargeOptions {
44
+ client: Pick<PhoenixdClient, "createInvoice" | "decodeInvoice">;
45
+ /** Use a shared durable store for deployed services. Defaults to process-local memory. */
46
+ store?: ChargeStore;
47
+ /** Invoice lifetime in seconds. Defaults to 3600; challenge expiry is capped to it. */
48
+ invoiceExpirySeconds?: number;
49
+ network?: LightningNetwork;
50
+ }
51
+ declare const defaults: {
52
+ readonly currency: "sat";
53
+ readonly methodDetails: {
54
+ readonly invoice: "";
55
+ };
56
+ };
57
+ type ServerChargeMethod = Method.Server<typeof charge$1, typeof defaults, Transport.Http>;
58
+ declare function charge(options: ChargeOptions): ServerChargeMethod;
59
+
60
+ declare const methods_charge: typeof charge;
61
+ declare namespace methods {
62
+ export { methods_charge as charge };
63
+ }
64
+
65
+ export { type ChargeOptions, type ChargeStore, type ManagedChargeStore, type RedisClient, type RedisStoreOptions, type ServerChargeMethod, type StoredCharge, charge, memoryStore, methods as phoenixd, redisStore };
@@ -0,0 +1,65 @@
1
+ import { Transport } from 'mppx/server';
2
+ export { Mppx } from 'mppx/server';
3
+ import { Method } from 'mppx';
4
+ import { PhoenixdClient } from '@nodana/phoenixd-ts';
5
+ import { L as LightningNetwork, c as charge$1 } from '../index-3V4vmG3I.js';
6
+ export { C as ChargeError } from '../index-3V4vmG3I.js';
7
+
8
+ /** Stored values contain no payment preimages or node credentials. */
9
+ interface StoredCharge {
10
+ readonly id: string;
11
+ readonly serializedChallenge: string;
12
+ readonly paymentHash: string;
13
+ readonly amountSat: number;
14
+ readonly expiresAt: number;
15
+ readonly requestUrl: string;
16
+ readonly requestMethod: string;
17
+ readonly bodyHash: string;
18
+ }
19
+ interface ChargeStore {
20
+ /** Persist before responding with a challenge. Reject reused IDs/payment hashes. */
21
+ issue(charge: StoredCharge): Promise<void>;
22
+ get(id: string): Promise<StoredCharge | undefined>;
23
+ /** Atomically succeeds once, and only while the stored challenge is unexpired. */
24
+ consume(id: string): Promise<boolean>;
25
+ }
26
+ interface ManagedChargeStore extends ChargeStore {
27
+ pruneExpired(): Promise<number>;
28
+ }
29
+ declare function memoryStore(options?: {
30
+ maxEntries?: number;
31
+ }): ManagedChargeStore;
32
+ /** Compatible with a connected node-redis client using its default reply types. */
33
+ interface RedisClient {
34
+ sendCommand(args: string[]): Promise<unknown>;
35
+ }
36
+ interface RedisStoreOptions {
37
+ /** Namespace shared by instances of this service. Defaults to phoenixd-mpp. */
38
+ keyPrefix?: string;
39
+ }
40
+ /** Shared payment state; Redis expires records automatically. Does not own the connection. */
41
+ declare function redisStore(client: RedisClient, options?: RedisStoreOptions): ChargeStore;
42
+
43
+ interface ChargeOptions {
44
+ client: Pick<PhoenixdClient, "createInvoice" | "decodeInvoice">;
45
+ /** Use a shared durable store for deployed services. Defaults to process-local memory. */
46
+ store?: ChargeStore;
47
+ /** Invoice lifetime in seconds. Defaults to 3600; challenge expiry is capped to it. */
48
+ invoiceExpirySeconds?: number;
49
+ network?: LightningNetwork;
50
+ }
51
+ declare const defaults: {
52
+ readonly currency: "sat";
53
+ readonly methodDetails: {
54
+ readonly invoice: "";
55
+ };
56
+ };
57
+ type ServerChargeMethod = Method.Server<typeof charge$1, typeof defaults, Transport.Http>;
58
+ declare function charge(options: ChargeOptions): ServerChargeMethod;
59
+
60
+ declare const methods_charge: typeof charge;
61
+ declare namespace methods {
62
+ export { methods_charge as charge };
63
+ }
64
+
65
+ export { type ChargeOptions, type ChargeStore, type ManagedChargeStore, type RedisClient, type RedisStoreOptions, type ServerChargeMethod, type StoredCharge, charge, memoryStore, methods as phoenixd, redisStore };