@thecryptodonkey/toll-booth 1.0.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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/adapters/express.d.ts +51 -0
  4. package/dist/adapters/express.d.ts.map +1 -0
  5. package/dist/adapters/express.js +237 -0
  6. package/dist/adapters/express.js.map +1 -0
  7. package/dist/adapters/proxy-headers.d.ts +7 -0
  8. package/dist/adapters/proxy-headers.d.ts.map +1 -0
  9. package/dist/adapters/proxy-headers.js +58 -0
  10. package/dist/adapters/proxy-headers.js.map +1 -0
  11. package/dist/adapters/web-standard.d.ts +60 -0
  12. package/dist/adapters/web-standard.d.ts.map +1 -0
  13. package/dist/adapters/web-standard.js +252 -0
  14. package/dist/adapters/web-standard.js.map +1 -0
  15. package/dist/backends/alby.d.ts +25 -0
  16. package/dist/backends/alby.d.ts.map +1 -0
  17. package/dist/backends/alby.js +137 -0
  18. package/dist/backends/alby.js.map +1 -0
  19. package/dist/backends/cln.d.ts +22 -0
  20. package/dist/backends/cln.d.ts.map +1 -0
  21. package/dist/backends/cln.js +55 -0
  22. package/dist/backends/cln.js.map +1 -0
  23. package/dist/backends/lnbits.d.ts +23 -0
  24. package/dist/backends/lnbits.d.ts.map +1 -0
  25. package/dist/backends/lnbits.js +58 -0
  26. package/dist/backends/lnbits.js.map +1 -0
  27. package/dist/backends/lnd.d.ts +21 -0
  28. package/dist/backends/lnd.d.ts.map +1 -0
  29. package/dist/backends/lnd.js +59 -0
  30. package/dist/backends/lnd.js.map +1 -0
  31. package/dist/backends/phoenixd.d.ts +19 -0
  32. package/dist/backends/phoenixd.d.ts.map +1 -0
  33. package/dist/backends/phoenixd.js +59 -0
  34. package/dist/backends/phoenixd.js.map +1 -0
  35. package/dist/booth.d.ts +54 -0
  36. package/dist/booth.d.ts.map +1 -0
  37. package/dist/booth.js +200 -0
  38. package/dist/booth.js.map +1 -0
  39. package/dist/core/cashu-redeem.d.ts +9 -0
  40. package/dist/core/cashu-redeem.d.ts.map +1 -0
  41. package/dist/core/cashu-redeem.js +85 -0
  42. package/dist/core/cashu-redeem.js.map +1 -0
  43. package/dist/core/create-invoice.d.ts +19 -0
  44. package/dist/core/create-invoice.d.ts.map +1 -0
  45. package/dist/core/create-invoice.js +66 -0
  46. package/dist/core/create-invoice.js.map +1 -0
  47. package/dist/core/invoice-status.d.ts +24 -0
  48. package/dist/core/invoice-status.d.ts.map +1 -0
  49. package/dist/core/invoice-status.js +74 -0
  50. package/dist/core/invoice-status.js.map +1 -0
  51. package/dist/core/nwc-pay.d.ts +8 -0
  52. package/dist/core/nwc-pay.d.ts.map +1 -0
  53. package/dist/core/nwc-pay.js +23 -0
  54. package/dist/core/nwc-pay.js.map +1 -0
  55. package/dist/core/toll-booth.d.ts +9 -0
  56. package/dist/core/toll-booth.d.ts.map +1 -0
  57. package/dist/core/toll-booth.js +172 -0
  58. package/dist/core/toll-booth.js.map +1 -0
  59. package/dist/core/types.d.ts +101 -0
  60. package/dist/core/types.d.ts.map +1 -0
  61. package/dist/core/types.js +3 -0
  62. package/dist/core/types.js.map +1 -0
  63. package/dist/free-tier.d.ts +14 -0
  64. package/dist/free-tier.d.ts.map +1 -0
  65. package/dist/free-tier.js +41 -0
  66. package/dist/free-tier.js.map +1 -0
  67. package/dist/index.d.ts +38 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +27 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/macaroon.d.ts +39 -0
  72. package/dist/macaroon.d.ts.map +1 -0
  73. package/dist/macaroon.js +111 -0
  74. package/dist/macaroon.js.map +1 -0
  75. package/dist/payment-page.d.ts +18 -0
  76. package/dist/payment-page.d.ts.map +1 -0
  77. package/dist/payment-page.js +391 -0
  78. package/dist/payment-page.js.map +1 -0
  79. package/dist/stats.d.ts +63 -0
  80. package/dist/stats.d.ts.map +1 -0
  81. package/dist/stats.js +75 -0
  82. package/dist/stats.js.map +1 -0
  83. package/dist/storage/interface.d.ts +58 -0
  84. package/dist/storage/interface.d.ts.map +1 -0
  85. package/dist/storage/interface.js +3 -0
  86. package/dist/storage/interface.js.map +1 -0
  87. package/dist/storage/memory.d.ts +3 -0
  88. package/dist/storage/memory.d.ts.map +1 -0
  89. package/dist/storage/memory.js +139 -0
  90. package/dist/storage/memory.js.map +1 -0
  91. package/dist/storage/sqlite.d.ts +6 -0
  92. package/dist/storage/sqlite.d.ts.map +1 -0
  93. package/dist/storage/sqlite.js +264 -0
  94. package/dist/storage/sqlite.js.map +1 -0
  95. package/dist/types.d.ts +198 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +8 -0
  98. package/dist/types.js.map +1 -0
  99. package/llms.txt +91 -0
  100. package/package.json +100 -0
@@ -0,0 +1,38 @@
1
+ export { Booth } from './booth.js';
2
+ export type { AdapterType, BoothOptions } from './booth.js';
3
+ export { createTollBooth } from './core/toll-booth.js';
4
+ export type { TollBoothEngine } from './core/toll-booth.js';
5
+ export type { TollBoothRequest, TollBoothResult, TollBoothCoreConfig } from './core/types.js';
6
+ export { handleCreateInvoice } from './core/create-invoice.js';
7
+ export type { CreateInvoiceDeps } from './core/create-invoice.js';
8
+ export { handleInvoiceStatus, renderInvoiceStatusHtml } from './core/invoice-status.js';
9
+ export type { InvoiceStatusDeps } from './core/invoice-status.js';
10
+ export { handleNwcPay } from './core/nwc-pay.js';
11
+ export type { NwcPayDeps } from './core/nwc-pay.js';
12
+ export { handleCashuRedeem, REDEEM_LEASE_MS } from './core/cashu-redeem.js';
13
+ export type { CashuRedeemDeps } from './core/cashu-redeem.js';
14
+ export type { StorageBackend, StoredInvoice, DebitResult } from './storage/interface.js';
15
+ export { sqliteStorage } from './storage/sqlite.js';
16
+ export type { SqliteStorageConfig } from './storage/sqlite.js';
17
+ export { memoryStorage } from './storage/memory.js';
18
+ export { createExpressMiddleware, createExpressInvoiceStatusHandler, createExpressCreateInvoiceHandler, createExpressNwcHandler, createExpressCashuHandler } from './adapters/express.js';
19
+ export type { ExpressMiddlewareConfig } from './adapters/express.js';
20
+ export { createWebStandardMiddleware, createWebStandardInvoiceStatusHandler, createWebStandardCreateInvoiceHandler, createWebStandardNwcHandler, createWebStandardCashuHandler } from './adapters/web-standard.js';
21
+ export type { WebStandardHandler, WebStandardMiddlewareConfig } from './adapters/web-standard.js';
22
+ export { mintMacaroon, verifyMacaroon, parseCaveats } from './macaroon.js';
23
+ export { FreeTier } from './free-tier.js';
24
+ export { StatsCollector } from './stats.js';
25
+ export { renderPaymentPage, renderErrorPage } from './payment-page.js';
26
+ export { phoenixdBackend } from './backends/phoenixd.js';
27
+ export type { PhoenixdConfig } from './backends/phoenixd.js';
28
+ export { lndBackend } from './backends/lnd.js';
29
+ export type { LndConfig } from './backends/lnd.js';
30
+ export { clnBackend } from './backends/cln.js';
31
+ export type { ClnConfig } from './backends/cln.js';
32
+ export { lnbitsBackend } from './backends/lnbits.js';
33
+ export type { LNbitsConfig } from './backends/lnbits.js';
34
+ export { albyBackend } from './backends/alby.js';
35
+ export type { AlbyConfig } from './backends/alby.js';
36
+ export type { LightningBackend, Invoice, InvoiceStatus, PricingTable, BoothConfig, CreditTier, PaymentEvent, RequestEvent, ChallengeEvent, EventHandler, } from './types.js';
37
+ export type { BoothStats } from './stats.js';
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG3D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAG7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AACvF,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC3E,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAG7D,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACxF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnD,OAAO,EAAE,uBAAuB,EAAE,iCAAiC,EAAE,iCAAiC,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AACzL,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,2BAA2B,EAAE,qCAAqC,EAAE,qCAAqC,EAAE,2BAA2B,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAClN,YAAY,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAGjG,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAGtE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAGpD,YAAY,EACV,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EACnE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,GACrE,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ // src/index.ts
2
+ // Booth class (main API)
3
+ export { Booth } from './booth.js';
4
+ // Core engine (power users)
5
+ export { createTollBooth } from './core/toll-booth.js';
6
+ // Core handlers
7
+ export { handleCreateInvoice } from './core/create-invoice.js';
8
+ export { handleInvoiceStatus, renderInvoiceStatusHtml } from './core/invoice-status.js';
9
+ export { handleNwcPay } from './core/nwc-pay.js';
10
+ export { handleCashuRedeem, REDEEM_LEASE_MS } from './core/cashu-redeem.js';
11
+ export { sqliteStorage } from './storage/sqlite.js';
12
+ export { memoryStorage } from './storage/memory.js';
13
+ // Adapters
14
+ export { createExpressMiddleware, createExpressInvoiceStatusHandler, createExpressCreateInvoiceHandler, createExpressNwcHandler, createExpressCashuHandler } from './adapters/express.js';
15
+ export { createWebStandardMiddleware, createWebStandardInvoiceStatusHandler, createWebStandardCreateInvoiceHandler, createWebStandardNwcHandler, createWebStandardCashuHandler } from './adapters/web-standard.js';
16
+ // Utilities
17
+ export { mintMacaroon, verifyMacaroon, parseCaveats } from './macaroon.js';
18
+ export { FreeTier } from './free-tier.js';
19
+ export { StatsCollector } from './stats.js';
20
+ export { renderPaymentPage, renderErrorPage } from './payment-page.js';
21
+ // Backends (re-exported for convenience — prefer subpath imports for tree-shaking)
22
+ export { phoenixdBackend } from './backends/phoenixd.js';
23
+ export { lndBackend } from './backends/lnd.js';
24
+ export { clnBackend } from './backends/cln.js';
25
+ export { lnbitsBackend } from './backends/lnbits.js';
26
+ export { albyBackend } from './backends/alby.js';
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AAEf,yBAAyB;AACzB,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAGlC,4BAA4B;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAItD,gBAAgB;AAChB,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAEvF,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAK3E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,WAAW;AACX,OAAO,EAAE,uBAAuB,EAAE,iCAAiC,EAAE,iCAAiC,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAEzL,OAAO,EAAE,2BAA2B,EAAE,qCAAqC,EAAE,qCAAqC,EAAE,2BAA2B,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAGlN,YAAY;AACZ,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEtE,mFAAmF;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Mints a new macaroon encoding a payment hash and credit balance.
3
+ *
4
+ * @param rootKey - Hex-encoded 32-byte root key.
5
+ * @param paymentHash - The Lightning payment hash (hex string).
6
+ * @param creditBalanceSats - The credit balance in satoshis.
7
+ * @returns Base64-encoded binary macaroon.
8
+ */
9
+ export declare function mintMacaroon(rootKey: string, paymentHash: string, creditBalanceSats: number): string;
10
+ /**
11
+ * Result of macaroon verification.
12
+ */
13
+ export interface VerifyResult {
14
+ /** Whether the macaroon signature and caveats are valid. */
15
+ valid: boolean;
16
+ /** The payment hash extracted from the macaroon, if valid. */
17
+ paymentHash?: string;
18
+ /** The credit balance in satoshis extracted from the macaroon, if valid. */
19
+ creditBalance?: number;
20
+ }
21
+ /**
22
+ * Verifies a macaroon's cryptographic signature and extracts its caveats.
23
+ *
24
+ * @param rootKey - Hex-encoded 32-byte root key used to mint the macaroon.
25
+ * @param macaroonBase64 - Base64-encoded binary macaroon.
26
+ * @returns A VerifyResult indicating validity and parsed caveat values.
27
+ */
28
+ export declare function verifyMacaroon(rootKey: string, macaroonBase64: string): VerifyResult;
29
+ /**
30
+ * Parses first-party caveats from a macaroon into a key/value map.
31
+ *
32
+ * Caveats must follow the `key = value` format. Caveats that do not
33
+ * match this pattern are silently ignored.
34
+ *
35
+ * @param macaroonBase64 - Base64-encoded binary macaroon.
36
+ * @returns A record of caveat keys to their string values.
37
+ */
38
+ export declare function parseCaveats(macaroonBase64: string): Record<string, string>;
39
+ //# sourceMappingURL=macaroon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"macaroon.d.ts","sourceRoot":"","sources":["../src/macaroon.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAWpG;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,YAAY,CAuCpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB3E"}
@@ -0,0 +1,111 @@
1
+ import { newMacaroon, importMacaroon } from 'macaroon';
2
+ const LOCATION = 'toll-booth';
3
+ const KNOWN_CAVEATS = new Set(['payment_hash', 'credit_balance']);
4
+ /**
5
+ * Mints a new macaroon encoding a payment hash and credit balance.
6
+ *
7
+ * @param rootKey - Hex-encoded 32-byte root key.
8
+ * @param paymentHash - The Lightning payment hash (hex string).
9
+ * @param creditBalanceSats - The credit balance in satoshis.
10
+ * @returns Base64-encoded binary macaroon.
11
+ */
12
+ export function mintMacaroon(rootKey, paymentHash, creditBalanceSats) {
13
+ const keyBytes = hexToBytes(rootKey);
14
+ const m = newMacaroon({
15
+ identifier: paymentHash,
16
+ location: LOCATION,
17
+ rootKey: keyBytes,
18
+ version: 2,
19
+ });
20
+ m.addFirstPartyCaveat(`payment_hash = ${paymentHash}`);
21
+ m.addFirstPartyCaveat(`credit_balance = ${creditBalanceSats}`);
22
+ return uint8ToBase64(m.exportBinary());
23
+ }
24
+ /**
25
+ * Verifies a macaroon's cryptographic signature and extracts its caveats.
26
+ *
27
+ * @param rootKey - Hex-encoded 32-byte root key used to mint the macaroon.
28
+ * @param macaroonBase64 - Base64-encoded binary macaroon.
29
+ * @returns A VerifyResult indicating validity and parsed caveat values.
30
+ */
31
+ export function verifyMacaroon(rootKey, macaroonBase64) {
32
+ try {
33
+ const keyBytes = hexToBytes(rootKey);
34
+ const m = importMacaroon(base64ToUint8(macaroonBase64));
35
+ // Track caveat counts to reject duplicates (prevents attacker-appended overrides)
36
+ const seen = new Map();
37
+ m.verify(keyBytes, (condition) => {
38
+ const eqIdx = condition.indexOf(' = ');
39
+ if (eqIdx === -1)
40
+ return 'malformed caveat';
41
+ const key = condition.slice(0, eqIdx);
42
+ if (!KNOWN_CAVEATS.has(key))
43
+ return 'unknown caveat';
44
+ seen.set(key, (seen.get(key) ?? 0) + 1);
45
+ if (seen.get(key) > 1)
46
+ return 'duplicate caveat';
47
+ return null;
48
+ }, []);
49
+ // Use the immutable identifier as the authoritative payment hash —
50
+ // it is set at mint time and covered by the root signature.
51
+ const json = m.exportJSON();
52
+ const identifier = json.i;
53
+ const caveats = parseCaveats(macaroonBase64);
54
+ // Cross-check: the payment_hash caveat must match the identifier
55
+ if (caveats.payment_hash && caveats.payment_hash !== identifier) {
56
+ return { valid: false };
57
+ }
58
+ return {
59
+ valid: true,
60
+ paymentHash: identifier,
61
+ creditBalance: caveats.credit_balance !== undefined
62
+ ? parseInt(caveats.credit_balance, 10)
63
+ : undefined,
64
+ };
65
+ }
66
+ catch {
67
+ return { valid: false };
68
+ }
69
+ }
70
+ /**
71
+ * Parses first-party caveats from a macaroon into a key/value map.
72
+ *
73
+ * Caveats must follow the `key = value` format. Caveats that do not
74
+ * match this pattern are silently ignored.
75
+ *
76
+ * @param macaroonBase64 - Base64-encoded binary macaroon.
77
+ * @returns A record of caveat keys to their string values.
78
+ */
79
+ export function parseCaveats(macaroonBase64) {
80
+ const m = importMacaroon(base64ToUint8(macaroonBase64));
81
+ const result = {};
82
+ // caveats is an array of objects with an identifier field (Uint8Array)
83
+ const caveats = m.caveats;
84
+ for (const c of caveats) {
85
+ const raw = new TextDecoder().decode(c.identifier);
86
+ const eqIdx = raw.indexOf(' = ');
87
+ if (eqIdx !== -1) {
88
+ const key = raw.slice(0, eqIdx).trim();
89
+ // First-occurrence-wins: server-set caveats come first in the chain,
90
+ // so any attacker-appended duplicates are ignored.
91
+ if (!Object.hasOwn(result, key)) {
92
+ result[key] = raw.slice(eqIdx + 3).trim();
93
+ }
94
+ }
95
+ }
96
+ return result;
97
+ }
98
+ function hexToBytes(hex) {
99
+ const bytes = new Uint8Array(hex.length / 2);
100
+ for (let i = 0; i < hex.length; i += 2) {
101
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
102
+ }
103
+ return bytes;
104
+ }
105
+ function uint8ToBase64(bytes) {
106
+ return Buffer.from(bytes).toString('base64');
107
+ }
108
+ function base64ToUint8(b64) {
109
+ return new Uint8Array(Buffer.from(b64, 'base64'));
110
+ }
111
+ //# sourceMappingURL=macaroon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"macaroon.js","sourceRoot":"","sources":["../src/macaroon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { CreditTier } from './types.js';
2
+ import type { StoredInvoice } from './storage/interface.js';
3
+ export interface PaymentPageData {
4
+ invoice: StoredInvoice;
5
+ paid: boolean;
6
+ preimage?: string;
7
+ tokenSuffix?: string;
8
+ tiers: CreditTier[];
9
+ nwcEnabled: boolean;
10
+ cashuEnabled: boolean;
11
+ }
12
+ export interface PaymentPageErrorData {
13
+ paymentHash: string;
14
+ message: string;
15
+ }
16
+ export declare function renderPaymentPage(data: PaymentPageData): Promise<string>;
17
+ export declare function renderErrorPage(data: PaymentPageErrorData): string;
18
+ //# sourceMappingURL=payment-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-page.d.ts","sourceRoot":"","sources":["../src/payment-page.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAE3D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAsE9E;AAwSD,wBAAgB,eAAe,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAwBlE"}
@@ -0,0 +1,391 @@
1
+ // src/payment-page.ts
2
+ import QRCode from 'qrcode';
3
+ export async function renderPaymentPage(data) {
4
+ const { invoice, paid, preimage, tokenSuffix, tiers, nwcEnabled, cashuEnabled } = data;
5
+ const qrSvg = await QRCode.toString(`lightning:${invoice.bolt11}`.toUpperCase(), { type: 'svg', margin: 2 });
6
+ return `<!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11
+ <title>Payment${paid ? ' Complete' : ' Required'} — toll-booth</title>
12
+ <style>
13
+ *{margin:0;padding:0;box-sizing:border-box}
14
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a0f;color:#e0e0e0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
15
+ .card{background:#161622;border:1px solid #2a2a3a;border-radius:16px;padding:2rem;max-width:480px;width:100%;box-shadow:0 8px 32px rgba(0,0,0,.4)}
16
+ h1{font-size:1.4rem;text-align:center;margin-bottom:1.5rem;color:#fff}
17
+ .qr-wrap{background:#fff;border-radius:12px;padding:1rem;display:flex;align-items:center;justify-content:center;margin:0 auto 1.5rem;max-width:280px}
18
+ .qr-wrap svg{width:100%;height:auto}
19
+ .invoice-str{font-family:monospace;font-size:.7rem;word-break:break-all;background:#0d0d15;border:1px solid #2a2a3a;border-radius:8px;padding:.75rem;margin-bottom:1rem;max-height:80px;overflow-y:auto;color:#a0a0b0}
20
+ .btn{display:block;width:100%;padding:.75rem;border:none;border-radius:8px;font-size:.95rem;font-weight:600;cursor:pointer;margin-bottom:.75rem;transition:all .15s ease}
21
+ .btn-primary{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff}
22
+ .btn-primary:hover{opacity:.9}
23
+ .btn-secondary{background:#1e1e30;border:1px solid #3a3a50;color:#c0c0d0}
24
+ .btn-secondary:hover{background:#252540}
25
+ .btn-success{background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff}
26
+ .status{text-align:center;padding:.5rem;border-radius:8px;margin-bottom:1rem;font-size:.85rem}
27
+ .status-polling{background:#1e1e30;color:#a0a0b0}
28
+ .status-paid{background:#052e16;color:#4ade80;border:1px solid #16a34a}
29
+ .tiers{display:grid;gap:.5rem;margin-bottom:1rem}
30
+ .tier{background:#1e1e30;border:2px solid #2a2a3a;border-radius:8px;padding:.75rem;cursor:pointer;transition:all .15s ease;text-align:center}
31
+ .tier:hover,.tier.selected{border-color:#6366f1;background:#1e1e35}
32
+ .tier-label{font-weight:600;color:#fff;font-size:.95rem}
33
+ .tier-price{color:#a0a0b0;font-size:.8rem;margin-top:.25rem}
34
+ .tier-bonus{color:#6366f1;font-size:.75rem;font-weight:500}
35
+ .wallets{margin-bottom:1rem}
36
+ .wallet-label{font-size:.8rem;color:#808090;margin-bottom:.5rem}
37
+ .success-icon{font-size:3rem;text-align:center;margin:1rem 0}
38
+ .token-box{font-family:monospace;font-size:.65rem;word-break:break-all;background:#0d0d15;border:1px solid #2a2a3a;border-radius:8px;padding:.75rem;margin:.75rem 0}
39
+ .token-label{font-size:.75rem;color:#808090;margin-bottom:.25rem}
40
+ .info{font-size:.8rem;color:#808090;text-align:center;margin-top:.75rem}
41
+ .info a{color:#6366f1}
42
+ .noscript-refresh{display:inline-block;margin-top:.5rem;color:#6366f1;text-decoration:underline}
43
+ .hidden{display:none}
44
+ .spinner{display:inline-block;width:14px;height:14px;border:2px solid #404060;border-top-color:#6366f1;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:.5rem}
45
+ @keyframes spin{to{transform:rotate(360deg)}}
46
+ .credit-bal{font-size:1.1rem;text-align:center;color:#fff;margin:.5rem 0}
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="card" id="card"
51
+ data-payment-hash="${esc(invoice.paymentHash)}"
52
+ data-macaroon="${esc(invoice.macaroon)}"
53
+ data-paid="${paid}"
54
+ data-nwc="${nwcEnabled}"
55
+ data-cashu="${cashuEnabled}"
56
+ >
57
+
58
+ ${paid ? renderPaidState(invoice, preimage, tokenSuffix) : renderAwaitingState(invoice, qrSvg, tiers, nwcEnabled, cashuEnabled)}
59
+
60
+ <div class="info">Powered by <strong>toll-booth</strong> &middot; L402</div>
61
+ </div>
62
+
63
+ <script>
64
+ ${paid ? '' : clientScript()}
65
+ </script>
66
+
67
+ <noscript>
68
+ ${paid ? '' : `<div style="text-align:center;margin-top:1rem;color:#a0a0b0;font-size:.85rem">JavaScript is disabled. <a class="noscript-refresh" href="">Refresh to check payment status</a>.</div>`}
69
+ </noscript>
70
+ </body>
71
+ </html>`;
72
+ }
73
+ function renderAwaitingState(invoice, qrSvg, tiers, nwcEnabled, cashuEnabled) {
74
+ const tiersHtml = tiers.length > 0 ? `
75
+ <div class="tiers" id="tiers">
76
+ ${tiers.map((t, i) => {
77
+ const bonus = t.creditSats > t.amountSats
78
+ ? `<div class="tier-bonus">+${Math.round(((t.creditSats - t.amountSats) / t.amountSats) * 100)}% bonus</div>`
79
+ : '';
80
+ return `<div class="tier${i === 0 ? ' selected' : ''}" data-amount="${t.amountSats}" data-credit="${t.creditSats}" onclick="selectTier(this)">
81
+ <div class="tier-label">${esc(t.label)}</div>
82
+ <div class="tier-price">${formatSats(t.amountSats)} sats &rarr; ${formatSats(t.creditSats)} credits</div>
83
+ ${bonus}
84
+ </div>`;
85
+ }).join('\n ')}
86
+ </div>` : '';
87
+ const walletButtons = [];
88
+ walletButtons.push(`<button class="btn btn-primary" id="btn-copy" onclick="copyInvoice()">Copy Invoice</button>`);
89
+ walletButtons.push(`<button class="btn btn-secondary hidden" id="btn-webln" onclick="payWebLN()">Pay with WebLN</button>`);
90
+ if (nwcEnabled) {
91
+ walletButtons.push(`<button class="btn btn-secondary" id="btn-nwc" onclick="showNwc()">Pay with Nostr Wallet Connect</button>`);
92
+ }
93
+ if (cashuEnabled) {
94
+ walletButtons.push(`<button class="btn btn-secondary" id="btn-cashu" onclick="showCashu()">Redeem Cashu Token</button>`);
95
+ }
96
+ return `
97
+ <h1>Payment Required</h1>
98
+ <div class="status status-polling" id="status">
99
+ <span class="spinner"></span> Waiting for payment&hellip;
100
+ </div>
101
+
102
+ <div class="qr-wrap" id="qr-wrap">${qrSvg}</div>
103
+
104
+ <div class="invoice-str" id="invoice-str">${esc(invoice.bolt11)}</div>
105
+
106
+ ${tiersHtml}
107
+
108
+ <div class="wallets">
109
+ ${walletButtons.join('\n ')}
110
+ </div>
111
+
112
+ <div class="hidden" id="nwc-form">
113
+ <input type="text" placeholder="nostr+walletconnect://..." id="nwc-uri"
114
+ style="width:100%;padding:.5rem;border-radius:8px;border:1px solid #3a3a50;background:#0d0d15;color:#e0e0e0;font-size:.85rem;margin-bottom:.5rem">
115
+ <button class="btn btn-primary" onclick="payNwc()">Pay via NWC</button>
116
+ </div>
117
+
118
+ <div class="hidden" id="cashu-form">
119
+ <textarea placeholder="cashuA..." id="cashu-token"
120
+ style="width:100%;padding:.5rem;border-radius:8px;border:1px solid #3a3a50;background:#0d0d15;color:#e0e0e0;font-size:.85rem;height:80px;resize:vertical;margin-bottom:.5rem"></textarea>
121
+ <button class="btn btn-primary" onclick="redeemCashu()">Redeem Token</button>
122
+ </div>`;
123
+ }
124
+ function renderPaidState(invoice, preimage, tokenSuffix) {
125
+ const effectiveSuffix = preimage ?? tokenSuffix;
126
+ const tokenLabel = preimage
127
+ ? 'L402 Token (macaroon:preimage)'
128
+ : 'L402 Token';
129
+ const tokenValue = effectiveSuffix
130
+ ? `${esc(invoice.macaroon)}:${esc(effectiveSuffix)}`
131
+ : undefined;
132
+ const preimageHtml = preimage
133
+ ? `
134
+ <div>
135
+ <div class="token-label">Payment preimage</div>
136
+ <div class="token-box" id="preimage">${esc(preimage)}</div>
137
+ </div>
138
+ `
139
+ : '';
140
+ return `
141
+ <h1>Payment Complete</h1>
142
+ <div class="status status-paid" id="status">Invoice paid successfully</div>
143
+ <div class="success-icon">&#9889;</div>
144
+ <div class="credit-bal" id="credit-bal">${formatSats(invoice.amountSats)} sats credited</div>
145
+
146
+ ${preimageHtml}
147
+
148
+ <div>
149
+ <div class="token-label">${tokenLabel}</div>
150
+ <div class="token-box" id="l402-token">${tokenValue ?? 'Unavailable'}</div>
151
+ </div>
152
+
153
+ ${tokenValue ? '<button class="btn btn-success" onclick="copyToken()">Copy L402 Token</button>' : ''}`;
154
+ }
155
+ function clientScript() {
156
+ return `
157
+ (function(){
158
+ var card = document.getElementById('card');
159
+ var hash = card.dataset.paymentHash;
160
+ var statusUrl = window.location.pathname + window.location.search;
161
+ var statusToken = new URLSearchParams(window.location.search).get('token') || '';
162
+ // Derive the mount base path so API calls work when toll-booth is mounted on a subpath.
163
+ // The payment page is always served at <basePath>/invoice-status/<hash>, so strip from
164
+ // '/invoice-status/' onwards to recover the base path (e.g. '' or '/pay').
165
+ var _pathParts = window.location.pathname.split('/invoice-status/');
166
+ var basePath = _pathParts.length > 1 ? _pathParts[0] : '';
167
+ if (card.dataset.paid === 'true') return;
168
+
169
+ // Detect WebLN
170
+ if (typeof window.webln !== 'undefined') {
171
+ document.getElementById('btn-webln').classList.remove('hidden');
172
+ }
173
+
174
+ // Poll for payment
175
+ var pollInterval = setInterval(function(){
176
+ fetch(statusUrl, {headers:{'Accept':'application/json'}})
177
+ .then(function(r){return r.json()})
178
+ .then(function(d){
179
+ if(d.paid){
180
+ clearInterval(pollInterval);
181
+ showPaid(d.preimage, 0, null, d.token_suffix);
182
+ }
183
+ })
184
+ .catch(function(){});
185
+ }, 3000);
186
+
187
+ window.copyInvoice = function(){
188
+ var str = document.getElementById('invoice-str').textContent;
189
+ navigator.clipboard.writeText(str).then(function(){
190
+ var btn = document.getElementById('btn-copy');
191
+ btn.textContent = 'Copied!';
192
+ setTimeout(function(){btn.textContent='Copy Invoice'},2000);
193
+ });
194
+ };
195
+
196
+ window.copyToken = function(){
197
+ var str = document.getElementById('l402-token').textContent;
198
+ navigator.clipboard.writeText(str).then(function(){
199
+ var btn = document.querySelector('.btn-success');
200
+ btn.textContent = 'Copied!';
201
+ setTimeout(function(){btn.textContent='Copy L402 Token'},2000);
202
+ });
203
+ };
204
+
205
+ window.payWebLN = async function(){
206
+ try {
207
+ await window.webln.enable();
208
+ var invoice = document.getElementById('invoice-str').textContent;
209
+ await window.webln.sendPayment(invoice);
210
+ } catch(e) {
211
+ console.error('WebLN payment failed:', e);
212
+ }
213
+ };
214
+
215
+ window.selectTier = function(el){
216
+ document.querySelectorAll('.tier').forEach(function(t){t.classList.remove('selected')});
217
+ el.classList.add('selected');
218
+ // Create a new invoice for this tier, then update the page in-place
219
+ fetch(basePath + '/create-invoice', {
220
+ method: 'POST',
221
+ headers: {'Content-Type': 'application/json'},
222
+ body: JSON.stringify({amountSats: parseInt(el.dataset.amount)})
223
+ })
224
+ .then(function(r){return r.json()})
225
+ .then(function(d){
226
+ if (!d.bolt11) return;
227
+ // Update QR code in-place
228
+ var qrWrap = document.getElementById('qr-wrap');
229
+ if (qrWrap && d.qr_svg) qrWrap.innerHTML = d.qr_svg;
230
+ // Update invoice string
231
+ var invStr = document.getElementById('invoice-str');
232
+ if (invStr) invStr.textContent = d.bolt11;
233
+ // Update payment hash for polling
234
+ hash = d.payment_hash;
235
+ card.dataset.paymentHash = d.payment_hash;
236
+ if (d.macaroon) card.dataset.macaroon = d.macaroon;
237
+ // Update browser URL without reload
238
+ if (d.payment_url) {
239
+ statusUrl = d.payment_url;
240
+ var parsedUrl = new URL(d.payment_url, window.location.origin);
241
+ statusToken = parsedUrl.searchParams.get('token') || '';
242
+ history.replaceState(null, '', d.payment_url);
243
+ }
244
+ // Restart polling with new hash
245
+ clearInterval(pollInterval);
246
+ pollInterval = setInterval(function(){
247
+ fetch(statusUrl, {headers:{'Accept':'application/json'}})
248
+ .then(function(r){return r.json()})
249
+ .then(function(d){
250
+ if(d.paid){
251
+ clearInterval(pollInterval);
252
+ showPaid(d.preimage, 0, null, d.token_suffix);
253
+ }
254
+ })
255
+ .catch(function(){});
256
+ }, 3000);
257
+ })
258
+ .catch(function(e){ console.error('Tier selection failed:', e) });
259
+ };
260
+
261
+ window.showNwc = function(){
262
+ document.getElementById('nwc-form').classList.toggle('hidden');
263
+ };
264
+
265
+ window.showCashu = function(){
266
+ document.getElementById('cashu-form').classList.toggle('hidden');
267
+ };
268
+
269
+ window.payNwc = function(){
270
+ var uri = document.getElementById('nwc-uri').value.trim();
271
+ if (!uri) return;
272
+ var invoice = document.getElementById('invoice-str').textContent;
273
+ fetch(basePath + '/nwc-pay', {
274
+ method: 'POST',
275
+ headers: {'Content-Type': 'application/json'},
276
+ body: JSON.stringify({nwcUri: uri, bolt11: invoice, paymentHash: hash, statusToken: statusToken})
277
+ })
278
+ .then(function(r){return r.json()})
279
+ .then(function(d){
280
+ if (d.preimage) showPaid(d.preimage, 0, null, d.token_suffix);
281
+ else if (d.error) alert(d.error);
282
+ })
283
+ .catch(function(e){ alert('NWC payment failed: ' + e.message) });
284
+ };
285
+
286
+ window.redeemCashu = function(){
287
+ var token = document.getElementById('cashu-token').value.trim();
288
+ if (!token) return;
289
+ fetch(basePath + '/cashu-redeem', {
290
+ method: 'POST',
291
+ headers: {'Content-Type': 'application/json'},
292
+ body: JSON.stringify({token: token, paymentHash: hash, statusToken: statusToken})
293
+ })
294
+ .then(function(r){return r.json()})
295
+ .then(function(d){
296
+ if (d.error) { alert(d.error); return; }
297
+ var mac = d.macaroon || card.dataset.macaroon;
298
+ showPaid(null, d.credited, mac, d.token_suffix);
299
+ })
300
+ .catch(function(e){ alert('Cashu redemption failed: ' + e.message) });
301
+ };
302
+
303
+ function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
304
+ function fmtSats(n){return Number(n).toLocaleString('en-GB')}
305
+
306
+ function showPaid(preimage, creditedAmount, cashuMacaroon, tokenSuffix){
307
+ clearInterval(pollInterval);
308
+ var status = document.getElementById('status');
309
+ status.className = 'status status-paid';
310
+ status.textContent = 'Invoice paid successfully';
311
+
312
+ // Replace card content with success state
313
+ var h1 = document.querySelector('h1');
314
+ h1.textContent = 'Payment Complete';
315
+ document.title = 'Payment Complete \\u2014 toll-booth';
316
+
317
+ // Hide payment UI, show success
318
+ var qr = document.getElementById('qr-wrap');
319
+ if(qr) qr.style.display = 'none';
320
+ var inv = document.getElementById('invoice-str');
321
+ if(inv) inv.style.display = 'none';
322
+ var tiers = document.getElementById('tiers');
323
+ if(tiers) tiers.style.display = 'none';
324
+ var wallets = document.querySelector('.wallets');
325
+ if(wallets) wallets.style.display = 'none';
326
+ var nwcForm = document.getElementById('nwc-form');
327
+ if(nwcForm) nwcForm.style.display = 'none';
328
+ var cashuForm = document.getElementById('cashu-form');
329
+ if(cashuForm) cashuForm.style.display = 'none';
330
+
331
+ // Add success content after status
332
+ var successHtml = '<div class="success-icon">&#9889;</div>';
333
+ if (creditedAmount) {
334
+ successHtml += '<div class="credit-bal">' + fmtSats(creditedAmount) + ' sats credited</div>';
335
+ }
336
+
337
+ // Determine the macaroon and auth token format
338
+ var macStr = cashuMacaroon ? escHtml(cashuMacaroon) : (card.dataset.macaroon ? escHtml(card.dataset.macaroon) : '');
339
+
340
+ if (preimage) {
341
+ var safePreimage = escHtml(preimage);
342
+ successHtml += '<div><div class="token-label">Payment preimage</div><div class="token-box" id="preimage">' + safePreimage + '</div></div>';
343
+ if (macStr) {
344
+ successHtml += '<div><div class="token-label">L402 Token (macaroon:preimage)</div><div class="token-box" id="l402-token">' + macStr + ':' + safePreimage + '</div></div>';
345
+ successHtml += '<button class="btn btn-success" onclick="copyToken()">Copy L402 Token</button>';
346
+ }
347
+ } else if (macStr && tokenSuffix) {
348
+ successHtml += '<div><div class="token-label">L402 Token</div><div class="token-box" id="l402-token">' + macStr + ':' + escHtml(tokenSuffix) + '</div></div>';
349
+ successHtml += '<button class="btn btn-success" onclick="copyToken()">Copy L402 Token</button>';
350
+ }
351
+ status.insertAdjacentHTML('afterend', successHtml);
352
+ }
353
+ })();`;
354
+ }
355
+ export function renderErrorPage(data) {
356
+ return `<!DOCTYPE html>
357
+ <html lang="en">
358
+ <head>
359
+ <meta charset="utf-8">
360
+ <meta name="viewport" content="width=device-width, initial-scale=1">
361
+ <title>Invoice Not Found — toll-booth</title>
362
+ <style>
363
+ *{margin:0;padding:0;box-sizing:border-box}
364
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a0f;color:#e0e0e0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
365
+ .card{background:#161622;border:1px solid #2a2a3a;border-radius:16px;padding:2rem;max-width:480px;width:100%;text-align:center}
366
+ h1{font-size:1.4rem;margin-bottom:1rem;color:#fff}
367
+ p{color:#a0a0b0;font-size:.9rem;line-height:1.5}
368
+ .hash{font-family:monospace;font-size:.7rem;color:#606070;word-break:break-all;margin-top:1rem}
369
+ </style>
370
+ </head>
371
+ <body>
372
+ <div class="card">
373
+ <h1>Invoice Not Found</h1>
374
+ <p>${esc(data.message)}</p>
375
+ <div class="hash">${esc(data.paymentHash)}</div>
376
+ </div>
377
+ </body>
378
+ </html>`;
379
+ }
380
+ function esc(s) {
381
+ return s
382
+ .replace(/&/g, '&amp;')
383
+ .replace(/</g, '&lt;')
384
+ .replace(/>/g, '&gt;')
385
+ .replace(/"/g, '&quot;')
386
+ .replace(/'/g, '&#39;');
387
+ }
388
+ function formatSats(sats) {
389
+ return sats.toLocaleString('en-GB');
390
+ }
391
+ //# sourceMappingURL=payment-page.js.map