diff --git a/.gitignore b/.gitignore index 7e1ac32f3..3467785fb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,21 +17,29 @@ src/* !src/cli.h !src/cli_main.c !src/constants.h +!src/crypto.c !src/datadir.h !src/errors.c !src/errors.h !src/message.c !src/message.h +!src/memory.h !src/noise.h +!src/p2p.c !src/peer.c !src/peer.h +!src/protocol.c !src/protocol.h +!src/random.h !src/scoring.c !src/scoring.h !src/secure_memory.c !src/secure_memory.h !src/secure_random.c !src/secure_random.h +!src/security.c +!src/sha2.c +!src/sha2.h !src/taproot.c !src/taproot.h !src/threads.c diff --git a/examples/http.js b/examples/http.js index fa5c4e1a3..79da24ec1 100644 --- a/examples/http.js +++ b/examples/http.js @@ -1,4 +1,7 @@ // # Exposing ARCs with HTTP +// Downstream apps set `path` to their `assets/` (or similar). Files there are served first; unhandled +// paths fall through to the packaged `@fabric/http` `assets/` (e.g. Fomantic / semantic) — no extra +// settings required. See `FabricHTTPServer` in `@fabric/http` `types/server.js` static middleware order. // Fabric makes it easy to publish applications to the Web, // giving downstream users access to a hosted instance of the // application. diff --git a/functions/bolt12Semantics.js b/functions/bolt12Semantics.js new file mode 100644 index 000000000..36c842b9c --- /dev/null +++ b/functions/bolt12Semantics.js @@ -0,0 +1,161 @@ +'use strict'; + +/** + * BOLT #12 semantic helpers for interpreting **`Lightning#decodeLightning`** (`decode` RPC) JSON from Core Lightning. + * + * **BIP-340 signatures (BOLT #12 § Signature calculation):** + * - **Offers** (`lno1…`) do **not** carry a signature TLV. + * - **`invoice_request`** and **`invoice`** streams include **signature** (TLV type **240**…1000), keyed with + * `SIG("lightning" ‖ stream ‖ field, Merkle_root, key)` per the spec ([BOLT #12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md)). + * - This module does **not** verify Schnorr signatures; use **`lightningd`** / **`decode`** and payment flows for that. + * + * **Recurrence:** TLVs such as `offer_recurrence_optional` / `_compulsory`, `invreq_recurrence_*`, `invoice_recurrence_basetime` + * are summarized from flat **`decode`** objects when present. + * + * @module functions/bolt12Semantics + * @see docs/LIGHTNING_COMPAT.md + */ + +/** + * TLV type numbers for BOLT #12 and Core Lightning `decode` fields (offer / invoice_request / invoice). + * @readonly + */ +const CLN_BOLT12_TLV = Object.freeze({ + INVREQ_METADATA: 0, + OFFER_CHAINS: 2, + OFFER_METADATA: 4, + OFFER_CURRENCY: 6, + OFFER_AMOUNT: 8, + OFFER_DESCRIPTION: 10, + OFFER_FEATURES: 12, + OFFER_ABSOLUTE_EXPIRY: 14, + OFFER_PATHS: 16, + OFFER_ISSUER: 18, + OFFER_QUANTITY_MAX: 20, + OFFER_ISSUER_ID: 22, + OFFER_RECURRENCE_COMPULSORY: 24, + OFFER_RECURRENCE_OPTIONAL: 25, + OFFER_RECURRENCE_BASE: 26, + OFFER_RECURRENCE_PAYWINDOW: 27, + OFFER_RECURRENCE_LIMIT: 29, + INVREQ_CHAIN: 80, + INVREQ_AMOUNT: 82, + INVREQ_FEATURES: 84, + INVREQ_QUANTITY: 86, + INVREQ_PAYER_ID: 88, + INVREQ_PAYER_NOTE: 89, + INVREQ_PATHS: 90, + INVREQ_BIP353_NAME: 91, + INVREQ_RECURRENCE_COUNTER: 92, + INVREQ_RECURRENCE_START: 93, + INVREQ_RECURRENCE_CANCEL: 94, + SIGNATURE_MIN: 240, + SIGNATURE_MAX: 1000, + INVOICE_PATHS: 160, + INVOICE_BLINDEDPAY: 162, + INVOICE_CREATED_AT: 164, + INVOICE_RELATIVE_EXPIRY: 166, + INVOICE_PAYMENT_HASH: 168, + INVOICE_AMOUNT: 170, + INVOICE_FALLBACKS: 172, + INVOICE_FEATURES: 174, + INVOICE_NODE_ID: 176, + INVOICE_RECURRENCE_BASETIME: 177 +}); + +/** @deprecated Use {@link CLN_BOLT12_TLV} — name kept for existing imports. */ +const BOLT12_TLV = CLN_BOLT12_TLV; + +/** + * Normalized kind for a value returned by **`decode`** / **`decodeLightning`**. + * @readonly + * @enum {string} + */ +const Bolt12StreamKind = Object.freeze({ + bolt12_offer: 'bolt12_offer', + bolt12_invoice_request: 'bolt12_invoice_request', + bolt12_invoice: 'bolt12_invoice', + bolt11_invoice: 'bolt11_invoice', + unknown: 'unknown' +}); + +/** + * Classify **`decode`** result from Core Lightning using its **`type`** field (wording varies slightly by version). + * @param {Object|null|undefined} decoded + * @returns {typeof Bolt12StreamKind[keyof typeof Bolt12StreamKind]} + */ +function classifyDecodedBolt12 (decoded) { + if (!decoded || typeof decoded !== 'object') return Bolt12StreamKind.unknown; + const t = decoded.type; + if (typeof t !== 'string') return Bolt12StreamKind.unknown; + const s = t.toLowerCase(); + if (s.includes('bolt12') && s.includes('offer') && !s.includes('invoice')) { + return Bolt12StreamKind.bolt12_offer; + } + if (s.includes('invoice_request') || (s.includes('bolt12') && s.includes('invoice') && s.includes('request'))) { + return Bolt12StreamKind.bolt12_invoice_request; + } + if (s.includes('bolt12') && s.includes('invoice') && !s.includes('request')) { + return Bolt12StreamKind.bolt12_invoice; + } + if (s.includes('bolt11') || /^bitcoin\s*invoice/i.test(t)) { + return Bolt12StreamKind.bolt11_invoice; + } + return Bolt12StreamKind.unknown; +} + +/** + * Whether BOLT #12 Merkle **BIP-340** signatures apply to this stream (not offers). + * @param {typeof Bolt12StreamKind[keyof typeof Bolt12StreamKind]} kind + * @returns {boolean} + */ +function bip340SignatureApplies (kind) { + return kind === Bolt12StreamKind.bolt12_invoice_request || kind === Bolt12StreamKind.bolt12_invoice; +} + +/** + * Collect recurrence-related fields from a flat **`decode`** object (snake_case keys as CLN tends to emit). + * @param {Object|null|undefined} decoded + * @returns {Object|null} Plain object copy, or `null` if nothing recurrence-related. + */ +function summarizeBolt12RecurrenceFromDecode (decoded) { + if (!decoded || typeof decoded !== 'object') return null; + /** @type {Record} */ + const out = {}; + + const offerKeys = [ + 'offer_recurrence', + 'offer_recurrence_compulsory', + 'offer_recurrence_optional', + 'offer_recurrence_base', + 'offer_recurrence_paywindow', + 'offer_recurrence_limit' + ]; + for (const k of offerKeys) { + if (decoded[k] != null) out[k] = decoded[k]; + } + + const invreqKeys = [ + 'invreq_recurrence_counter', + 'invreq_recurrence_start', + 'invreq_recurrence_cancel' + ]; + for (const k of invreqKeys) { + if (decoded[k] != null) out[k] = decoded[k]; + } + + if (decoded.invoice_recurrence_basetime != null) { + out.invoice_recurrence_basetime = decoded.invoice_recurrence_basetime; + } + + return Object.keys(out).length ? out : null; +} + +module.exports = { + BOLT12_TLV, + CLN_BOLT12_TLV, + Bolt12StreamKind, + bip340SignatureApplies, + classifyDecodedBolt12, + summarizeBolt12RecurrenceFromDecode +}; diff --git a/functions/fabricDocumentOfferEnvelope.js b/functions/fabricDocumentOfferEnvelope.js new file mode 100644 index 000000000..88f643f1f --- /dev/null +++ b/functions/fabricDocumentOfferEnvelope.js @@ -0,0 +1,57 @@ +'use strict'; + +/** + * Fabric document-offer envelope types (JSON `type` on generic payloads). Conceptually aligned with + * Lightning BOLT12 *offers* (buyer-facing request → seller-facing response); wire remains + * `P2P_INVENTORY_REQUEST` / `P2P_INVENTORY_RESPONSE` opcodes. See `docs/FABRIC_DOCUMENT_OFFER.md`. + */ + +/** Canonical JSON `type`: buyer/originator asks for catalog / settlement terms. */ +const FABRIC_DOCUMENT_OFFER = 'FABRIC_DOCUMENT_OFFER'; +/** Accepted synonym for `FABRIC_DOCUMENT_OFFER`. */ +const FABRIC_DOCUMENT_OFFER_REQUEST = 'FABRIC_DOCUMENT_OFFER_REQUEST'; + +/** Canonical JSON `type`: seller/router replies (items, L1 `contentHash`, optional HTLC hooks). */ +const FABRIC_DOCUMENT_OFFER_RESPONSE = 'FABRIC_DOCUMENT_OFFER_RESPONSE'; +/** Accepted synonym for `FABRIC_DOCUMENT_OFFER_RESPONSE`. */ +const FABRIC_DOCUMENT_OFFER_REPLY = 'FABRIC_DOCUMENT_OFFER_REPLY'; + +const TO_LEGACY_INVENTORY = Object.freeze({ + [FABRIC_DOCUMENT_OFFER]: 'INVENTORY_REQUEST', + [FABRIC_DOCUMENT_OFFER_REQUEST]: 'INVENTORY_REQUEST', + [FABRIC_DOCUMENT_OFFER_RESPONSE]: 'INVENTORY_RESPONSE', + [FABRIC_DOCUMENT_OFFER_REPLY]: 'INVENTORY_RESPONSE' +}); + +/** + * Map Fabric document-offer envelope `type` strings to legacy handler types (`INVENTORY_*`). + * @param {unknown} type + * @returns {string|null} + */ +function fabricDocumentOfferEnvelopeToLegacy (type) { + if (typeof type !== 'string') return null; + const t = type.trim(); + return TO_LEGACY_INVENTORY[t] || null; +} + +/** + * Shallow-clone `message` with `type` set to `INVENTORY_REQUEST` / `INVENTORY_RESPONSE` when a Fabric alias is used. + * @param {object} message + * @returns {object} + */ +function normalizeFabricDocumentOfferEnvelopeForHandlers (message) { + if (!message || typeof message !== 'object') return message; + const legacy = fabricDocumentOfferEnvelopeToLegacy(message.type); + if (!legacy) return message; + return Object.assign({}, message, { type: legacy }); +} + +module.exports = { + FABRIC_DOCUMENT_OFFER, + FABRIC_DOCUMENT_OFFER_REQUEST, + FABRIC_DOCUMENT_OFFER_RESPONSE, + FABRIC_DOCUMENT_OFFER_REPLY, + TO_LEGACY_INVENTORY, + fabricDocumentOfferEnvelopeToLegacy, + normalizeFabricDocumentOfferEnvelopeForHandlers +}; diff --git a/functions/fabricPaymentBech32.js b/functions/fabricPaymentBech32.js new file mode 100644 index 000000000..ddea2a379 --- /dev/null +++ b/functions/fabricPaymentBech32.js @@ -0,0 +1,130 @@ +'use strict'; + +const bech32 = require('./bech32'); +const lb = require('./lightningBolt12'); + +const FABRIC_ROUTED_PAYMENT_HRP = 'fa'; + +/** All such strings use the `1` separator after the HRP, e.g. `fa1…`. */ +const FABRIC_ROUTED_PAYMENT_PREFIX = `${FABRIC_ROUTED_PAYMENT_HRP}1`; + +/** Payload format byte (first byte after decode). */ +const FABRIC_PAYMENT_VERSION = 0; + +/** + * Route / binding type (second byte). `0` = 32-byte document **content hash** (see L1 document exchange). + * @readonly + * @enum {number} + */ +const FabricRouteType = Object.freeze({ + /** SHA256 hash as used with `purchaseContentHashHex` / canonical publish preimage chain. */ + DOCUMENT_CONTENT_HASH: 0 +}); + +/** + * @param {Array|unknown} arr + * @returns {Buffer} + */ +function hash32ByteArrayToBuffer (arr) { + if (!Array.isArray(arr) || arr.length !== 32) { + throw new TypeError('hash32 array must have exactly 32 elements'); + } + const out = Buffer.alloc(32); + for (let i = 0; i < 32; i++) { + const x = arr[i]; + if (!Number.isInteger(x) || x < 0 || x > 255) { + throw new TypeError('hash32 array elements must be integers in 0..255'); + } + out[i] = x; + } + return out; +} + +/** + * @param {string|null|undefined} s + * @returns {boolean} + */ +function isFabricRoutedPaymentString (s) { + return String(s || '').trim().toLowerCase().startsWith(FABRIC_ROUTED_PAYMENT_PREFIX); +} + +/** + * Encode a v0 Fabric-routed payment reference: version + route type + 32-byte id (e.g. content hash). + * @param {Object} o + * @param {Buffer|Uint8Array|number[]} o.hash32 Exactly 32 bytes (e.g. document content hash). + * @param {number} [o.routeType=0] {@link FabricRouteType} + * @returns {string} bech32m string (`fa1…`) + */ +function encodeFabricRoutedPaymentV0 (o) { + if (!o || !o.hash32) throw new TypeError('encodeFabricRoutedPaymentV0 requires hash32'); + let buf; + if (Buffer.isBuffer(o.hash32)) buf = o.hash32; + else if (o.hash32 instanceof Uint8Array) buf = Buffer.from(o.hash32); + else if (Array.isArray(o.hash32)) buf = hash32ByteArrayToBuffer(o.hash32); + else throw new TypeError('hash32 must be Buffer, Uint8Array, or byte array'); + if (buf.length !== 32) throw new TypeError('hash32 must be exactly 32 bytes'); + const routeType = o.routeType != null ? Number(o.routeType) : FabricRouteType.DOCUMENT_CONTENT_HASH; + if (!Number.isInteger(routeType) || routeType < 0 || routeType > 255) { + throw new TypeError('routeType must be an integer 0..255'); + } + const payload = Buffer.concat([Buffer.from([FABRIC_PAYMENT_VERSION, routeType]), buf]); + const words = bech32.toWords(payload); + return bech32.encode(FABRIC_ROUTED_PAYMENT_HRP, words, 'bech32m'); +} + +/** + * Decode an `fa1…` string. Returns `null` if HRP/spec mismatch or payload shape is wrong. + * @param {string} str + * @returns {{ version: number, routeType: number, hash32: Buffer, hrp: string, spec: string }|null} + */ +function decodeFabricRoutedPayment (str) { + if (typeof str !== 'string' || !str.trim()) return null; + let d; + try { + d = bech32.decode(str.trim()); + } catch { + return null; + } + if (d.hrp !== FABRIC_ROUTED_PAYMENT_HRP || d.spec !== 'bech32m') return null; + let raw; + try { + raw = bech32.fromWords(d.words); + } catch { + return null; + } + if (raw.length !== 34) return null; + const version = raw[0]; + const routeType = raw[1]; + const hash32 = Buffer.from(raw.slice(2, 34)); + if (version !== FABRIC_PAYMENT_VERSION) return null; + return { + version, + routeType, + hash32, + hrp: d.hrp, + spec: d.spec + }; +} + +/** + * Classify a payment-related encoded string: Fabric `fa1…`, then Lightning `ln…` (see {@link ./lightningBolt12}). + * @param {string|null|undefined} s + * @returns {string} + */ +function classifyPaymentEncodingString (s) { + const t = String(s || '').trim(); + if (!t) return 'empty'; + if (isFabricRoutedPaymentString(t)) return 'fabric_routed_payment'; + return lb.classifyLightningEncodedString(t); +} + +module.exports = { + FABRIC_ROUTED_PAYMENT_HRP, + FABRIC_ROUTED_PAYMENT_PREFIX, + FABRIC_PAYMENT_VERSION, + FabricRouteType, + classifyPaymentEncodingString, + decodeFabricRoutedPayment, + encodeFabricRoutedPaymentV0, + isFabricRoutedPaymentString +}; diff --git a/functions/lightningBolt12.js b/functions/lightningBolt12.js new file mode 100644 index 000000000..c5767861c --- /dev/null +++ b/functions/lightningBolt12.js @@ -0,0 +1,101 @@ +'use strict'; + +/** BOLT12 offer encoding uses HRP `lno` → strings begin with `lno1` (bech32m). */ +const BOLT12_OFFER_PREFIX = 'lno1'; + +/** Invoice request (`invoice_request` in spec) — Core Lightning exposes `lnr1…` strings. */ +const BOLT12_INVOICE_REQUEST_PREFIX = 'lnr1'; + +/** + * Common BOLT11 invoice HRPs (first segment before `1`). Not exhaustive; unknown `ln…1` may still be BOLT11. + * @type {ReadonlyArray} + */ +const BOLT11_INVOICE_HRPS = Object.freeze([ + 'lnbc', 'lntb', 'lnbcrt', 'lntbs', 'lnbs', 'lntbsc', 'lnsb', 'lnbsc', 'lntb4', 'lnbc4' +]); + +/** + * @typedef {'bolt12_offer'|'bolt12_invoice_request'|'bolt11_invoice'|'unknown'} Bolt12Class + */ + +function normalizeBolt12ScanString (s) { + let t = String(s ?? '').trim(); + if (!t) return ''; + t = t.replace(/\+\s*/g, ''); + return t; +} + +/** + * @param {string|null|undefined} s + * @returns {boolean} + */ +function isBolt12OfferString (s) { + const low = normalizeBolt12ScanString(s).toLowerCase(); + return low.startsWith(BOLT12_OFFER_PREFIX); +} + +/** + * @param {string|null|undefined} s + * @returns {boolean} + */ +function isBolt12InvoiceRequestString (s) { + const low = normalizeBolt12ScanString(s).toLowerCase(); + return low.startsWith(BOLT12_INVOICE_REQUEST_PREFIX); +} + +/** + * @param {string|null|undefined} s + * @returns {boolean} + */ +function isLikelyBolt11InvoiceString (s) { + const t = String(s || '').trim().toLowerCase(); + if (!t.includes('1')) return false; + return BOLT11_INVOICE_HRPS.some((hrp) => t.startsWith(`${hrp}1`)); +} + +/** + * Classify a Lightning-encoded string for routing to `fetchInvoice`, `sendInvoice`, `pay`, etc. + * @param {string|null|undefined} s + * @returns {Bolt12Class|'empty'} + */ +function classifyLightningEncodedString (s) { + const t = normalizeBolt12ScanString(s); + if (!t) return 'empty'; + const low = t.toLowerCase(); + if (low.startsWith(BOLT12_OFFER_PREFIX)) return 'bolt12_offer'; + if (low.startsWith(BOLT12_INVOICE_REQUEST_PREFIX)) return 'bolt12_invoice_request'; + if (isLikelyBolt11InvoiceString(low)) return 'bolt11_invoice'; + return 'unknown'; +} + +/** + * How Fabric **markets** and related layers relate to Lightning (see `docs/FABRIC_LIGHTNING_OFFERS.md`). + * @readonly + * @enum {string} + */ +const FabricLightningOfferRole = Object.freeze({ + /** Fabric `Peer` `P2P_SESSION_OFFER` / peering — not a BOLT12 string. */ + fabric_peer_handshake: 'fabric_peer_handshake', + /** Inventory `offerBtc` / document rate — market surface; may pair with a BOLT12 `bolt12` for LN pay. */ + fabric_document_commerce: 'fabric_document_commerce', + /** BOLT12 `offer` / `lno1…` — Lightning payment offer (Core Lightning `offer` RPC). */ + lightning_bolt12_offer: 'lightning_bolt12_offer', + /** BOLT12 `invoice_request` / `lnr1…` — payer-initiated flow (`invoicerequest` RPC). */ + lightning_bolt12_invoice_request: 'lightning_bolt12_invoice_request' +}); + +/** @type {typeof FabricLightningOfferRole} Preferred name; same frozen object as {@link FabricLightningOfferRole}. */ +const FabricLightningMarketRole = FabricLightningOfferRole; + +module.exports = { + BOLT12_OFFER_PREFIX, + BOLT12_INVOICE_REQUEST_PREFIX, + BOLT11_INVOICE_HRPS, + FabricLightningMarketRole, + FabricLightningOfferRole, + classifyLightningEncodedString, + isBolt12OfferString, + isBolt12InvoiceRequestString, + isLikelyBolt11InvoiceString, + normalizeBolt12ScanString +}; diff --git a/package-lock.json b/package-lock.json index e9c76aa75..475cb9e3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,16 @@ "@asciidoctor/core": "^2.0.0-rc.1" } }, + "node_modules/@asciidoctor/cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@asciidoctor/cli/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -103,6 +113,59 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/@asciidoctor/cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asciidoctor/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@asciidoctor/cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@asciidoctor/cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@asciidoctor/cli/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -133,14 +196,14 @@ } }, "node_modules/@asciidoctor/core": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.8.tgz", - "integrity": "sha512-oozXk7ZO1RAd/KLFLkKOhqTcG4GO3CV44WwOFg2gMcCsqCUTarvMT7xERIoWW2WurKbB0/ce+98r01p8xPOlBw==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.9.tgz", + "integrity": "sha512-tIPRHo1T2SFmAm+j77cDsj0RuaszP7xJxsaVTTAF5CwKyTbazw9TnIVlpIWM5yWfIWAWcAZy92RcnPgMJwny1w==", "dev": true, "license": "MIT", "dependencies": { - "asciidoctor-opal-runtime": "0.3.3", - "unxhr": "1.0.1" + "asciidoctor-opal-runtime": "0.3.4", + "unxhr": "~1.2" }, "engines": { "node": ">=8.11", @@ -169,9 +232,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -402,29 +465,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -471,91 +548,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1007,9 +999,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1034,13 +1026,16 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -1118,14 +1113,14 @@ "license": "MIT" }, "node_modules/asciidoctor": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-2.2.8.tgz", - "integrity": "sha512-G+sDYWnNo+QHRkIvN5k7ASbvrd2bHuNXHlZ83+PjVFYtl0//as5iebq+Bdf3aSwXrkM7akcEJPUpdTjjP0MgYw==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-2.2.9.tgz", + "integrity": "sha512-lzviGTZ/tnnmDLIE+fY/m8aUc6lGTGRNh5rTC1HPevlc89G0iYez+sQFT60oZ87BPzOYllP+TeK1xh0D1wt/6Q==", "dev": true, "license": "MIT", "dependencies": { "@asciidoctor/cli": "3.5.0", - "@asciidoctor/core": "2.2.8" + "@asciidoctor/core": "2.2.9" }, "bin": { "asciidoctor": "bin/asciidoctor", @@ -1138,14 +1133,14 @@ } }, "node_modules/asciidoctor-opal-runtime": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.3.tgz", - "integrity": "sha512-/CEVNiOia8E5BMO9FLooo+Kv18K4+4JBFRJp8vUy/N5dMRAg+fRNV4HA+o6aoSC79jVU/aT5XvUpxSxSsTS8FQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.4.tgz", + "integrity": "sha512-zqd6zn1LV+PZ69AP/kEbB00zuPHMIAJY3IX8+aZV+X1qOwatYvKGjsMmdMc5ApfhtkjZ4mYkqiTPJWnEnBiMJg==", "dev": true, "license": "MIT", "dependencies": { - "glob": "7.1.3", - "unxhr": "1.0.1" + "fast-glob": "~3.3", + "unxhr": "~1.2" }, "engines": { "node": ">=8.11" @@ -1528,6 +1523,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/cache-point": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz", @@ -1825,6 +1836,69 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2577,9 +2651,9 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, @@ -3375,24 +3449,27 @@ "license": "Apache-2.0" }, "node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" - } - }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3414,27 +3491,29 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/gopd": { @@ -3502,9 +3581,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -3905,9 +3984,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -4254,85 +4333,6 @@ "node": ">=14" } }, - "node_modules/js-beautify/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/js-beautify/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-beautify/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", @@ -4398,19 +4398,19 @@ } }, "node_modules/jsdoc-api": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.5.tgz", - "integrity": "sha512-TQwh1jA8xtCkIbVwm/XA3vDRAa5JjydyKx1cC413Sh3WohDFxcMdwKSvn4LOsq2xWyAmOU/VnSChTQf6EF0R8g==", + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.6.tgz", + "integrity": "sha512-8JW0532+rXVw8LoZ1LIAeKofsV8QQZhnY3chxMHV9hQdsuDTshsajwk0b4EMxCqen2vZ2op/r/qeEsqZxsEQyg==", "dev": true, "license": "MIT", "dependencies": { - "array-back": "^6.2.2", + "array-back": "^6.2.3", "cache-point": "^3.0.1", - "current-module-paths": "^1.1.2", + "current-module-paths": "^1.1.3", "file-set": "^5.3.0", - "jsdoc": "^4.0.4", + "jsdoc": "^4.0.5", "object-to-spawn-args": "^2.0.1", - "walk-back": "^5.1.1" + "walk-back": "^5.1.2" }, "engines": { "node": ">=12.17" @@ -4554,9 +4554,9 @@ } }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5579,28 +5579,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/mocha/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mocha/node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -5614,13 +5592,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/mocha/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/mocha/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -5637,23 +5608,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mocha/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6278,21 +6232,29 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", @@ -6532,6 +6494,59 @@ "npm-normalize-package-bin": "^1.0.0" } }, + "node_modules/read-package-json/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-package-json/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -6664,6 +6679,59 @@ "rimraf": "bin.js" } }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6943,12 +7011,12 @@ } }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -7145,18 +7213,21 @@ "dev": true }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -7175,7 +7246,24 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -7188,6 +7276,22 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -7202,6 +7306,16 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7312,6 +7426,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -7559,9 +7690,9 @@ } }, "node_modules/unxhr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.0.1.tgz", - "integrity": "sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz", + "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==", "dev": true, "license": "MIT", "engines": { @@ -7606,12 +7737,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-to-istanbul": { @@ -7934,18 +8069,18 @@ "license": "Apache-2.0" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -7970,6 +8105,64 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8077,6 +8270,51 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 450ff0252..ee4e7bcca 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,9 @@ }, "merkletreejs": { "@noble/hashes": "=1.7.1" + }, + "jayson": { + "uuid": "14.0.0" } }, "devDependencies": { diff --git a/reports/install.log b/reports/install.log index b1d6484fc..15a241230 100644 --- a/reports/install.log +++ b/reports/install.log @@ -5,21 +5,14 @@ $ npm i CXX(target) Release/obj.target/fabric/src/binding.o - CC(target) Release/obj.target/fabric/native/sipa/segwit_addr.o - CC(target) Release/obj.target/fabric/src/peer.o - CC(target) Release/obj.target/fabric/src/message.o - CC(target) Release/obj.target/fabric/src/errors.o - CC(target) Release/obj.target/fabric/src/threads.o - CC(target) Release/obj.target/fabric/src/scoring.o - CC(target) Release/obj.target/fabric/src/validation.o - CC(target) Release/obj.target/fabric/src/sha2.o - CC(target) Release/obj.target/fabric/src/security.o - CC(target) Release/obj.target/fabric/src/taproot.o + CC(target) Release/obj.target/fabric/src/crypto.o + CC(target) Release/obj.target/fabric/src/protocol.o + CC(target) Release/obj.target/fabric/src/p2p.o SOLINK_MODULE(target) Release/fabric.node -added 665 packages, and audited 666 packages in 33s +added 685 packages, and audited 686 packages in 29s -109 packages are looking for funding +111 packages are looking for funding run `npm fund` for details found 0 vulnerabilities diff --git a/services/lightning.js b/services/lightning.js index 8f2616595..61c27cbca 100644 --- a/services/lightning.js +++ b/services/lightning.js @@ -35,9 +35,6 @@ function shouldTreatLightningStderrAsError (line) { ); } -/** - * Node does not chunk stderr on line boundaries; buffer and emit per logical line. - */ function appendLightningStderrChunk (service, chunk) { if (!service._lightningdStderrBuf) service._lightningdStderrBuf = ''; service._lightningdStderrBuf += chunk.toString('utf8'); @@ -275,6 +272,106 @@ class Lightning extends Service { }; } + async callRpc (method, params = [], timeoutMs = 30000) { + return this._makeRPCRequest(method, params, timeoutMs); + } + + async decodeLightning (boltString) { + return this._makeRPCRequest('decode', [boltString]); + } + + async decodePay (bolt11) { + return this._makeRPCRequest('decodepay', [bolt11]); + } + + async createOffer (params) { + if (!params || typeof params !== 'object' || Array.isArray(params)) { + throw new Error('createOffer requires a params object (e.g. { amount_msat, description })'); + } + return this._makeRPCRequest('offer', [params]); + } + + async fetchInvoice (offerOrParams, invoiceParams = null) { + if (offerOrParams != null && typeof offerOrParams === 'object' && !Array.isArray(offerOrParams) && typeof offerOrParams.offer === 'string') { + if (invoiceParams != null && typeof invoiceParams === 'object' && !Array.isArray(invoiceParams)) { + return this._makeRPCRequest('fetchinvoice', [Object.assign({}, offerOrParams, invoiceParams)]); + } + return this._makeRPCRequest('fetchinvoice', [offerOrParams]); + } + if (typeof offerOrParams !== 'string') { + throw new Error('fetchInvoice requires offer string or params object with `offer`'); + } + if (invoiceParams != null && typeof invoiceParams === 'object' && !Array.isArray(invoiceParams)) { + return this._makeRPCRequest('fetchinvoice', [offerOrParams, invoiceParams]); + } + return this._makeRPCRequest('fetchinvoice', [offerOrParams]); + } + + async pay (invoiceOrParams, timeoutMs = 30000) { + return this._makeRPCRequest('pay', [invoiceOrParams], timeoutMs); + } + + async listOffers (filter = null) { + if (filter == null) return this._makeRPCRequest('listoffers', []); + if (typeof filter === 'string') return this._makeRPCRequest('listoffers', [filter]); + if (typeof filter === 'object' && !Array.isArray(filter)) { + return this._makeRPCRequest('listoffers', [filter]); + } + throw new Error('listOffers expects offer_id string, filter object, or null'); + } + + async disableOffer (offerId) { + return this._makeRPCRequest('disableoffer', [offerId]); + } + + async createInvoiceRequest (params) { + if (!params || typeof params !== 'object' || Array.isArray(params)) { + throw new Error('createInvoiceRequest requires a params object (e.g. { amount, description })'); + } + return this._makeRPCRequest('invoicerequest', [params]); + } + + async listInvoiceRequests (filter = null) { + if (filter == null) return this._makeRPCRequest('listinvoicerequests', []); + if (typeof filter === 'string') return this._makeRPCRequest('listinvoicerequests', [filter]); + if (typeof filter === 'object' && !Array.isArray(filter)) { + return this._makeRPCRequest('listinvoicerequests', [filter]); + } + throw new Error('listInvoiceRequests expects invreq_id string, filter object, or null'); + } + + async disableInvoiceRequest (invreqId) { + return this._makeRPCRequest('disableinvoicerequest', [invreqId]); + } + + async sendInvoice (params) { + if (!params || typeof params !== 'object' || Array.isArray(params)) { + throw new Error('sendInvoice requires a params object (e.g. { invreq, label })'); + } + return this._makeRPCRequest('sendinvoice', [params]); + } + + async getRoute (destinationId, amountMsat, riskfactor = 10, cltvOrRouteOptions = null) { + if (cltvOrRouteOptions != null && typeof cltvOrRouteOptions === 'object' && !Array.isArray(cltvOrRouteOptions)) { + const o = cltvOrRouteOptions; + return this._makeRPCRequest('getroute', [ + destinationId, + amountMsat, + riskfactor, + o.cltv != null ? o.cltv : null, + o.fromid != null ? o.fromid : null, + o.fuzzpercent != null ? o.fuzzpercent : null, + o.exclude != null ? o.exclude : null, + o.maxhops != null ? o.maxhops : null + ]); + } + const args = [destinationId, amountMsat, riskfactor]; + if (cltvOrRouteOptions != null) { + args.push(cltvOrRouteOptions); + } + return this._makeRPCRequest('getroute', args); + } + /** * Computes the total liquidity of the Lightning node. * @returns {Object} Liquidity in BTC. @@ -885,15 +982,41 @@ class Lightning extends Service { */ Lightning.CLN_RPC_METHODS = Object.freeze([ 'connect', + 'decode', + 'decodepay', + 'disableinvoicerequest', + 'disableoffer', + 'fetchinvoice', 'fundchannel', 'getinfo', + 'getroute', 'invoice', + 'invoicerequest', 'listchannels', 'listfunds', + 'listinvoicerequests', + 'listoffers', 'newaddr', + 'offer', + 'pay', + 'sendinvoice', 'stop' ]); +Lightning.DOCS = Object.freeze({ + boltCompatibility: 'docs/BOLT_COMPATIBILITY.md', + fabricLightningOffers: 'docs/FABRIC_LIGHTNING_OFFERS.md', + fabricLightningMarkets: 'docs/FABRIC_LIGHTNING_OFFERS.md', + fabricPaymentBech32: 'docs/FABRIC_PAYMENT_BECH32.md', + lightningCompat: 'docs/LIGHTNING_COMPAT.md' +}); + Lightning.redactSensitiveCommandArg = redactSensitiveCommandArg; +Object.assign(Lightning, { + Bolt12: require('../functions/lightningBolt12'), + FabricPayment: require('../functions/fabricPaymentBech32'), + Bolt12Semantics: require('../functions/bolt12Semantics') +}); + module.exports = Lightning; diff --git a/src/crypto.c b/src/crypto.c new file mode 100644 index 000000000..7cbc59e53 --- /dev/null +++ b/src/crypto.c @@ -0,0 +1,8 @@ +/** + * Native addon — crypto and Taproot stack (single translation unit). + * Order: SHA-256/512 → secure memory/random → Bech32/segwit_addr → BIP340/Taproot. + */ +#include "sha2.c" +#include "security.c" +#include "../native/sipa/segwit_addr.c" +#include "taproot.c" diff --git a/src/p2p.c b/src/p2p.c new file mode 100644 index 000000000..3838e1d77 --- /dev/null +++ b/src/p2p.c @@ -0,0 +1,7 @@ +/** + * Native addon — P2P stack: threading primitives, scoring, Noise peer (single translation unit). + * Named p2p.c (not peer.c) so this TU can #include peer.c without self-inclusion. + */ +#include "threads.c" +#include "scoring.c" +#include "peer.c" diff --git a/src/protocol.c b/src/protocol.c new file mode 100644 index 000000000..90eb7e221 --- /dev/null +++ b/src/protocol.c @@ -0,0 +1,6 @@ +/** + * Native addon — protocol layer: errors, validation, Message (single translation unit). + */ +#include "errors.c" +#include "validation.c" +#include "message.c" diff --git a/src/security.c b/src/security.c new file mode 100644 index 000000000..2896c79db --- /dev/null +++ b/src/security.c @@ -0,0 +1,624 @@ +#include "memory.h" +#include "random.h" + +#include +#include +#include +#include +#include + +// Platform-specific includes +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +#ifdef __linux__ +#include +#endif + +#ifdef __APPLE__ +#include +#endif + +// Internal allocation tracking for secure free operations +typedef struct allocation_record +{ + void *ptr; + size_t size; + struct allocation_record *next; +} allocation_record_t; + +static allocation_record_t *allocations = NULL; +static int memory_initialized = 0; + +// Platform-specific secure zero implementation +#ifdef _WIN32 +#define SECURE_ZERO_IMPL(ptr, len) SecureZeroMemory((ptr), (len)) +#else +// Use volatile to prevent compiler optimization +static void secure_zero_impl(volatile void *ptr, size_t len) +{ + volatile uint8_t *p = (volatile uint8_t *)ptr; + for (size_t i = 0; i < len; i++) + { + p[i] = 0; + } + // Memory barrier to prevent reordering + __asm__ __volatile__("" : : "r"(ptr) : "memory"); +} +#define SECURE_ZERO_IMPL(ptr, len) secure_zero_impl((ptr), (len)) +#endif + +FabricError fabric_secure_memory_init(void) +{ + if (memory_initialized) + { + return FABRIC_SUCCESS; + } + + allocations = NULL; + memory_initialized = 1; + return FABRIC_SUCCESS; +} + +void fabric_secure_memory_cleanup(void) +{ + if (!memory_initialized) + { + return; + } + + // Clean up any remaining allocations + allocation_record_t *current = allocations; + while (current) + { + allocation_record_t *next = current->next; + + // Securely zero the tracked memory + SECURE_ZERO_IMPL(current->ptr, current->size); + free(current->ptr); + free(current); + + current = next; + } + + allocations = NULL; + memory_initialized = 0; +} + +/** @return 0 on success, -1 if tracking metadata could not be allocated */ +static int track_allocation(void *ptr, size_t size) +{ + if (!ptr || size == 0) + return 0; + + allocation_record_t *record = malloc(sizeof(allocation_record_t)); + if (!record) + return -1; + + record->ptr = ptr; + record->size = size; + record->next = allocations; + allocations = record; + return 0; +} + +static size_t untrack_allocation(void *ptr) +{ + if (!ptr) + return 0; + + allocation_record_t **current = &allocations; + while (*current) + { + if ((*current)->ptr == ptr) + { + allocation_record_t *to_remove = *current; + size_t size = to_remove->size; + *current = to_remove->next; + free(to_remove); + return size; + } + current = &(*current)->next; + } + + return 0; // Not found +} + +void fabric_secure_zero(volatile void *ptr, size_t len) +{ + if (!ptr || len == 0) + return; + + SECURE_ZERO_IMPL(ptr, len); +} + +void *fabric_secure_malloc(size_t size) +{ + if (size == 0) + return NULL; + + if (!memory_initialized) + { + fabric_secure_memory_init(); + } + + void *ptr = malloc(size); + if (!ptr) + return NULL; + + // Zero initialize + memset(ptr, 0, size); + + // Track the allocation (fail closed: untracked secure buffers are unsafe to hand out) + if (track_allocation(ptr, size) != 0) + { + free(ptr); + return NULL; + } + + return ptr; +} + +void fabric_secure_free(void *ptr, size_t size) +{ + if (!ptr) + return; + + // If size is 0, try to get it from tracking + if (size == 0) + { + size = untrack_allocation(ptr); + } + else + { + untrack_allocation(ptr); + } + + // Securely zero the memory + if (size > 0) + { + SECURE_ZERO_IMPL(ptr, size); + } + + free(ptr); +} + +void *fabric_secure_realloc(void *ptr, size_t old_size, size_t new_size) +{ + if (new_size == 0) + { + fabric_secure_free(ptr, old_size); + return NULL; + } + + if (!ptr) + { + return fabric_secure_malloc(new_size); + } + + if (old_size == 0) + { + old_size = fabric_secure_malloc_size(ptr); + } + + void *new_ptr = fabric_secure_malloc(new_size); + if (!new_ptr) + { + return NULL; // Original memory is unchanged + } + + // Copy the minimum of old and new sizes + size_t copy_size = (old_size < new_size) ? old_size : new_size; + memcpy(new_ptr, ptr, copy_size); + + // Securely free the old memory + fabric_secure_free(ptr, old_size); + + return new_ptr; +} + +int fabric_secure_memcmp(const void *a, const void *b, size_t len) +{ + if (!a || !b) + { + return (a == b) ? 0 : 1; + } + + const uint8_t *pa = (const uint8_t *)a; + const uint8_t *pb = (const uint8_t *)b; + uint8_t result = 0; + + // Constant-time comparison + for (size_t i = 0; i < len; i++) + { + result |= pa[i] ^ pb[i]; + } + + return result; +} + +FabricError fabric_secure_memcpy(void *dest, size_t dest_size, + const void *src, size_t src_size) +{ + FABRIC_CHECK_NULL(dest); + FABRIC_CHECK_NULL(src); + FABRIC_CHECK_CONDITION(dest_size > 0, FABRIC_ERROR_INVALID_SIZE); + FABRIC_CHECK_CONDITION(src_size > 0, FABRIC_ERROR_INVALID_SIZE); + FABRIC_CHECK_CONDITION(src_size <= dest_size, FABRIC_ERROR_BUFFER_TOO_SMALL); + + memcpy(dest, src, src_size); + + // Zero remaining bytes if dest is larger + if (dest_size > src_size) + { + memset((uint8_t *)dest + src_size, 0, dest_size - src_size); + } + + return FABRIC_SUCCESS; +} + +FabricError fabric_secure_mlock(void *ptr, size_t len) +{ + FABRIC_CHECK_NULL(ptr); + FABRIC_CHECK_CONDITION(len > 0, FABRIC_ERROR_INVALID_SIZE); + +#ifdef _WIN32 + if (!VirtualLock(ptr, len)) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } +#else + if (mlock(ptr, len) != 0) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } +#endif + + return FABRIC_SUCCESS; +} + +FabricError fabric_secure_munlock(void *ptr, size_t len) +{ + FABRIC_CHECK_NULL(ptr); + FABRIC_CHECK_CONDITION(len > 0, FABRIC_ERROR_INVALID_SIZE); + +#ifdef _WIN32 + if (!VirtualUnlock(ptr, len)) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } +#else + if (munlock(ptr, len) != 0) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } +#endif + + return FABRIC_SUCCESS; +} + +char *fabric_secure_strdup(const char *str) +{ + if (!str) + return NULL; + + size_t len = strlen(str) + 1; + char *dup = fabric_secure_malloc(len); + if (!dup) + return NULL; + + memcpy(dup, str, len); + return dup; +} + +size_t fabric_secure_malloc_size(void *ptr) +{ + if (!ptr) + return 0; + + allocation_record_t *current = allocations; + while (current) + { + if (current->ptr == ptr) + { + return current->size; + } + current = current->next; + } + + return 0; // Not found +} + +// Secure random internal state +static int random_initialized = 0; +static int urandom_fd = -1; + +#ifdef _WIN32 +static HCRYPTPROV hCryptProv = 0; +#endif + +FabricError fabric_secure_random_init(void) +{ + if (random_initialized) + { + return FABRIC_SUCCESS; + } + +#ifdef _WIN32 + // Windows: Use CryptGenRandom + if (!CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) + { + return FABRIC_ERROR_CRYPTO_INIT_FAILED; + } +#else + // Unix-like systems: Open /dev/urandom as fallback + urandom_fd = open("/dev/urandom", O_RDONLY); + if (urandom_fd < 0) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } + + // Set FD_CLOEXEC to prevent descriptor leaking to child processes + int flags = fcntl(urandom_fd, F_GETFD); + if (flags >= 0) + { + fcntl(urandom_fd, F_SETFD, flags | FD_CLOEXEC); + } +#endif + + random_initialized = 1; + return FABRIC_SUCCESS; +} + +FabricError fabric_secure_random_bytes(uint8_t *buffer, size_t length) +{ + FABRIC_CHECK_NULL(buffer); + FABRIC_CHECK_CONDITION(length > 0, FABRIC_ERROR_INVALID_SIZE); + + if (!random_initialized) + { + FabricError init_result = fabric_secure_random_init(); + if (init_result != FABRIC_SUCCESS) + { + return init_result; + } + } + +#ifdef __linux__ + // Linux: Try getrandom() first (available since kernel 3.17) + { + size_t offset = 0; + while (offset < length) + { + ssize_t result; + do + { + result = getrandom(buffer + offset, length - offset, 0); + } while (result < 0 && errno == EINTR); + + if (result < 0) + { + if (errno != ENOSYS) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } + break; /* fall back to /dev/urandom */ + } + if (result == 0) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } + offset += (size_t)result; + } + if (offset == length) + { + return FABRIC_SUCCESS; + } + } +#endif + +#ifdef __APPLE__ + // macOS: Use Security framework + OSStatus status = SecRandomCopyBytes(kSecRandomDefault, length, buffer); + if (status == errSecSuccess) + { + return FABRIC_SUCCESS; + } + + // Fall back to /dev/urandom on failure +#endif + +#ifdef _WIN32 + // Windows: Use CryptGenRandom + if (CryptGenRandom(hCryptProv, (DWORD)length, buffer)) + { + return FABRIC_SUCCESS; + } + return FABRIC_ERROR_CRYPTO_INIT_FAILED; +#else + // Unix fallback: Read from /dev/urandom + if (urandom_fd < 0) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } + + size_t bytes_read = 0; + while (bytes_read < length) + { + ssize_t result = read(urandom_fd, buffer + bytes_read, length - bytes_read); + if (result < 0) + { + if (errno == EINTR) + { + continue; // Interrupted by signal, retry + } + return FABRIC_ERROR_SYSTEM_CALL_FAILED; + } + if (result == 0) + { + return FABRIC_ERROR_SYSTEM_CALL_FAILED; // Unexpected EOF + } + bytes_read += (size_t)result; + } + + return FABRIC_SUCCESS; +#endif +} + +FabricError fabric_secure_random_uint32(uint32_t *result) +{ + FABRIC_CHECK_NULL(result); + + return fabric_secure_random_bytes((uint8_t *)result, sizeof(uint32_t)); +} + +FabricError fabric_secure_random_range(uint32_t max_value, uint32_t *result) +{ + FABRIC_CHECK_NULL(result); + FABRIC_CHECK_CONDITION(max_value > 0, FABRIC_ERROR_INVALID_ARGUMENT); + + if (max_value == 1) + { + *result = 0; + return FABRIC_SUCCESS; + } + + // Use rejection sampling to avoid modulo bias + // Calculate the largest multiple of max_value that fits in uint32_t + uint32_t threshold = UINT32_MAX - (UINT32_MAX % max_value); + + uint32_t random_value; + int attempts = 0; + const int max_attempts = 100; // Prevent infinite loops + + do + { + FabricError rand_result = fabric_secure_random_uint32(&random_value); + if (rand_result != FABRIC_SUCCESS) + { + return rand_result; + } + + attempts++; + if (attempts > max_attempts) + { + return FABRIC_ERROR_OPERATION_FAILED; + } + } while (random_value >= threshold); + + *result = random_value % max_value; + return FABRIC_SUCCESS; +} + +void fabric_secure_random_cleanup(void) +{ + if (!random_initialized) + { + return; + } + +#ifdef _WIN32 + if (hCryptProv) + { + CryptReleaseContext(hCryptProv, 0); + hCryptProv = 0; + } +#else + if (urandom_fd >= 0) + { + close(urandom_fd); + urandom_fd = -1; + } +#endif + + random_initialized = 0; +} + +FabricError fabric_secure_random_test_entropy(void) +{ + enum { TEST_BYTES = 1024 }; + uint8_t test_buffer[TEST_BYTES]; + const size_t test_size = (size_t)TEST_BYTES; + + // Generate test data + FabricError result = fabric_secure_random_bytes(test_buffer, test_size); + if (result != FABRIC_SUCCESS) + { + return result; + } + + // Simple entropy tests + + // Test 1: Check for all zeros or all ones + int all_zero = 1, all_one = 1; + for (size_t i = 0; i < test_size; i++) + { + if (test_buffer[i] != 0x00) + all_zero = 0; + if (test_buffer[i] != 0xFF) + all_one = 0; + } + + if (all_zero || all_one) + { + return FABRIC_ERROR_CRYPTO_INIT_FAILED; + } + + // Test 2: Basic frequency test (should be roughly balanced) + int bit_count = 0; + for (size_t i = 0; i < test_size; i++) + { + for (int bit = 0; bit < 8; bit++) + { + if (test_buffer[i] & (1 << bit)) + { + bit_count++; + } + } + } + + int total_bits = test_size * 8; + // Allow 10% deviation from 50% + if (bit_count < (total_bits * 0.4) || bit_count > (total_bits * 0.6)) + { + return FABRIC_ERROR_CRYPTO_INIT_FAILED; + } + + // Test 3: Check for obvious patterns (consecutive identical bytes) + int consecutive_count = 0; + int max_consecutive = 0; + + for (size_t i = 1; i < test_size; i++) + { + if (test_buffer[i] == test_buffer[i - 1]) + { + consecutive_count++; + } + else + { + if (consecutive_count > max_consecutive) + { + max_consecutive = consecutive_count; + } + consecutive_count = 0; + } + } + if (consecutive_count > max_consecutive) + { + max_consecutive = consecutive_count; + } + + // Fail if more than 5% of bytes are consecutive duplicates + if (max_consecutive > (test_size * 0.05)) + { + return FABRIC_ERROR_CRYPTO_INIT_FAILED; + } + + return FABRIC_SUCCESS; +} diff --git a/src/sha2.c b/src/sha2.c new file mode 100644 index 000000000..1d810f831 --- /dev/null +++ b/src/sha2.c @@ -0,0 +1,389 @@ +#include "sha2.h" +#include "memory.h" + +#include + +typedef struct { + uint64_t bitlen; + uint32_t state[8]; + uint8_t data[64]; + size_t datalen; +} fabric_sha256_ctx; + +typedef struct { + uint64_t bitlen[2]; + uint64_t state[8]; + uint8_t data[128]; + size_t datalen; +} fabric_sha512_ctx; + +static const uint32_t K256[64] = { + 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, + 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U, + 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, + 0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U, + 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, + 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U, + 0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U, + 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U +}; + +static const uint64_t K512[80] = { + 0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, 0xe9b5dba58189dbbcULL, + 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL, + 0xd807aa98a3030242ULL, 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL, + 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, 0xc19bf174cf692694ULL, + 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL, + 0x2de92c6f592b0275ULL, 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL, + 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, 0xbf597fc7beef0ee4ULL, + 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL, + 0x27b70a8546d22ffcULL, 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL, + 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, 0x92722c851482353bULL, + 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL, + 0xd192e819d6ef5218ULL, 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL, + 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, 0x34b0bcb5e19b48a8ULL, + 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL, + 0x748f82ee5defb2fcULL, 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL, + 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, 0xc67178f2e372532bULL, + 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL, + 0x06f067aa72176fbaULL, 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL, + 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, 0x431d67c49c100d4cULL, + 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL +}; + +#define ROTRIGHT32(a, b) (((a) >> (b)) | ((a) << (32 - (b)))) +#define CH32(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ32(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EP0_32(x) (ROTRIGHT32((x), 2) ^ ROTRIGHT32((x), 13) ^ ROTRIGHT32((x), 22)) +#define EP1_32(x) (ROTRIGHT32((x), 6) ^ ROTRIGHT32((x), 11) ^ ROTRIGHT32((x), 25)) +#define SIG0_32(x) (ROTRIGHT32((x), 7) ^ ROTRIGHT32((x), 18) ^ ((x) >> 3)) +#define SIG1_32(x) (ROTRIGHT32((x), 17) ^ ROTRIGHT32((x), 19) ^ ((x) >> 10)) + +#define ROTRIGHT64(a, b) (((a) >> (b)) | ((a) << (64 - (b)))) +#define CH64(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ64(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EP0_64(x) (ROTRIGHT64((x), 28) ^ ROTRIGHT64((x), 34) ^ ROTRIGHT64((x), 39)) +#define EP1_64(x) (ROTRIGHT64((x), 14) ^ ROTRIGHT64((x), 18) ^ ROTRIGHT64((x), 41)) +#define SIG0_64(x) (ROTRIGHT64((x), 1) ^ ROTRIGHT64((x), 8) ^ ((x) >> 7)) +#define SIG1_64(x) (ROTRIGHT64((x), 19) ^ ROTRIGHT64((x), 61) ^ ((x) >> 6)) + +static void fabric_sha256_transform(fabric_sha256_ctx *ctx, const uint8_t data[]) +{ + uint32_t m[64]; + uint32_t a, b, c, d, e, f, g, h; + uint32_t t1, t2; + size_t i, j; + + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) | ((uint32_t)data[j + 2] << 8) | ((uint32_t)data[j + 3]); + } + for (; i < 64; ++i) { + m[i] = SIG1_32(m[i - 2]) + m[i - 7] + SIG0_32(m[i - 15]) + m[i - 16]; + } + + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + + for (i = 0; i < 64; ++i) { + t1 = h + EP1_32(e) + CH32(e, f, g) + K256[i] + m[i]; + t2 = EP0_32(a) + MAJ32(a, b, c); + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void fabric_sha256_init(fabric_sha256_ctx *ctx) +{ + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667U; + ctx->state[1] = 0xbb67ae85U; + ctx->state[2] = 0x3c6ef372U; + ctx->state[3] = 0xa54ff53aU; + ctx->state[4] = 0x510e527fU; + ctx->state[5] = 0x9b05688cU; + ctx->state[6] = 0x1f83d9abU; + ctx->state[7] = 0x5be0cd19U; +} + +static void fabric_sha256_update(fabric_sha256_ctx *ctx, const uint8_t *data, size_t len) +{ + size_t i; + for (i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 64) { + fabric_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void fabric_sha256_final(fabric_sha256_ctx *ctx, uint8_t hash[32]) +{ + size_t i = ctx->datalen; + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 64) ctx->data[i++] = 0x00; + fabric_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + + ctx->bitlen += ctx->datalen * 8; + ctx->data[63] = (uint8_t)(ctx->bitlen); + ctx->data[62] = (uint8_t)(ctx->bitlen >> 8); + ctx->data[61] = (uint8_t)(ctx->bitlen >> 16); + ctx->data[60] = (uint8_t)(ctx->bitlen >> 24); + ctx->data[59] = (uint8_t)(ctx->bitlen >> 32); + ctx->data[58] = (uint8_t)(ctx->bitlen >> 40); + ctx->data[57] = (uint8_t)(ctx->bitlen >> 48); + ctx->data[56] = (uint8_t)(ctx->bitlen >> 56); + fabric_sha256_transform(ctx, ctx->data); + + for (i = 0; i < 4; ++i) { + hash[i] = (uint8_t)((ctx->state[0] >> (24 - i * 8)) & 0xff); + hash[i + 4] = (uint8_t)((ctx->state[1] >> (24 - i * 8)) & 0xff); + hash[i + 8] = (uint8_t)((ctx->state[2] >> (24 - i * 8)) & 0xff); + hash[i + 12] = (uint8_t)((ctx->state[3] >> (24 - i * 8)) & 0xff); + hash[i + 16] = (uint8_t)((ctx->state[4] >> (24 - i * 8)) & 0xff); + hash[i + 20] = (uint8_t)((ctx->state[5] >> (24 - i * 8)) & 0xff); + hash[i + 24] = (uint8_t)((ctx->state[6] >> (24 - i * 8)) & 0xff); + hash[i + 28] = (uint8_t)((ctx->state[7] >> (24 - i * 8)) & 0xff); + } +} + +static void fabric_sha512_transform(fabric_sha512_ctx *ctx, const uint8_t data[]) +{ + uint64_t m[80]; + uint64_t a, b, c, d, e, f, g, h; + uint64_t t1, t2; + size_t i, j; + + for (i = 0, j = 0; i < 16; ++i, j += 8) { + m[i] = ((uint64_t)data[j] << 56) | ((uint64_t)data[j + 1] << 48) | ((uint64_t)data[j + 2] << 40) | ((uint64_t)data[j + 3] << 32) | + ((uint64_t)data[j + 4] << 24) | ((uint64_t)data[j + 5] << 16) | ((uint64_t)data[j + 6] << 8) | ((uint64_t)data[j + 7]); + } + for (; i < 80; ++i) { + m[i] = SIG1_64(m[i - 2]) + m[i - 7] + SIG0_64(m[i - 15]) + m[i - 16]; + } + + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + + for (i = 0; i < 80; ++i) { + t1 = h + EP1_64(e) + CH64(e, f, g) + K512[i] + m[i]; + t2 = EP0_64(a) + MAJ64(a, b, c); + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void fabric_sha512_init(fabric_sha512_ctx *ctx) +{ + ctx->datalen = 0; + ctx->bitlen[0] = 0; + ctx->bitlen[1] = 0; + ctx->state[0] = 0x6a09e667f3bcc908ULL; + ctx->state[1] = 0xbb67ae8584caa73bULL; + ctx->state[2] = 0x3c6ef372fe94f82bULL; + ctx->state[3] = 0xa54ff53a5f1d36f1ULL; + ctx->state[4] = 0x510e527fade682d1ULL; + ctx->state[5] = 0x9b05688c2b3e6c1fULL; + ctx->state[6] = 0x1f83d9abfb41bd6bULL; + ctx->state[7] = 0x5be0cd19137e2179ULL; +} + +static void fabric_sha512_update(fabric_sha512_ctx *ctx, const uint8_t *data, size_t len) +{ + size_t i; + for (i = 0; i < len; ++i) { + ctx->data[ctx->datalen++] = data[i]; + if (ctx->datalen == 128) { + fabric_sha512_transform(ctx, ctx->data); + if ((ctx->bitlen[0] += 1024) < 1024) ctx->bitlen[1]++; + ctx->datalen = 0; + } + } +} + +static void fabric_sha512_final(fabric_sha512_ctx *ctx, uint8_t hash[64]) +{ + size_t i = ctx->datalen; + if (i < 112) { + ctx->data[i++] = 0x80; + while (i < 112) ctx->data[i++] = 0x00; + } else { + ctx->data[i++] = 0x80; + while (i < 128) ctx->data[i++] = 0x00; + fabric_sha512_transform(ctx, ctx->data); + memset(ctx->data, 0, 112); + } + + uint64_t low = ctx->bitlen[0] + (uint64_t)(ctx->datalen * 8); + uint64_t high = ctx->bitlen[1]; + if (low < ctx->bitlen[0]) high++; + + ctx->data[127] = (uint8_t)(low); + ctx->data[126] = (uint8_t)(low >> 8); + ctx->data[125] = (uint8_t)(low >> 16); + ctx->data[124] = (uint8_t)(low >> 24); + ctx->data[123] = (uint8_t)(low >> 32); + ctx->data[122] = (uint8_t)(low >> 40); + ctx->data[121] = (uint8_t)(low >> 48); + ctx->data[120] = (uint8_t)(low >> 56); + ctx->data[119] = (uint8_t)(high); + ctx->data[118] = (uint8_t)(high >> 8); + ctx->data[117] = (uint8_t)(high >> 16); + ctx->data[116] = (uint8_t)(high >> 24); + ctx->data[115] = (uint8_t)(high >> 32); + ctx->data[114] = (uint8_t)(high >> 40); + ctx->data[113] = (uint8_t)(high >> 48); + ctx->data[112] = (uint8_t)(high >> 56); + fabric_sha512_transform(ctx, ctx->data); + + for (i = 0; i < 8; ++i) { + hash[i * 8 + 0] = (uint8_t)((ctx->state[i] >> 56) & 0xff); + hash[i * 8 + 1] = (uint8_t)((ctx->state[i] >> 48) & 0xff); + hash[i * 8 + 2] = (uint8_t)((ctx->state[i] >> 40) & 0xff); + hash[i * 8 + 3] = (uint8_t)((ctx->state[i] >> 32) & 0xff); + hash[i * 8 + 4] = (uint8_t)((ctx->state[i] >> 24) & 0xff); + hash[i * 8 + 5] = (uint8_t)((ctx->state[i] >> 16) & 0xff); + hash[i * 8 + 6] = (uint8_t)((ctx->state[i] >> 8) & 0xff); + hash[i * 8 + 7] = (uint8_t)(ctx->state[i] & 0xff); + } +} + +int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32]) +{ + if (!out32) return 0; + if (!data && len != 0) return 0; + fabric_sha256_ctx ctx; + fabric_sha256_init(&ctx); + fabric_sha256_update(&ctx, data, len); + fabric_sha256_final(&ctx, out32); + return 1; +} + +int fabric_sha512(const uint8_t *data, size_t len, uint8_t out64[64]) +{ + if (!out64) return 0; + if (!data && len != 0) return 0; + fabric_sha512_ctx ctx; + fabric_sha512_init(&ctx); + fabric_sha512_update(&ctx, data, len); + fabric_sha512_final(&ctx, out64); + return 1; +} + +int fabric_hmac_sha512(const uint8_t *key, size_t key_len, + const uint8_t *data, size_t data_len, + uint8_t out64[64]) +{ + uint8_t key_block[128]; + uint8_t key_hash[64]; + uint8_t o_key_pad[128]; + uint8_t i_key_pad[128]; + uint8_t inner[64]; + size_t i; + +#define FABRIC_HMAC_SHA512_WIPE() do { \ + fabric_secure_zero(key_block, sizeof(key_block)); \ + fabric_secure_zero(key_hash, sizeof(key_hash)); \ + fabric_secure_zero(o_key_pad, sizeof(o_key_pad)); \ + fabric_secure_zero(i_key_pad, sizeof(i_key_pad)); \ + fabric_secure_zero(inner, sizeof(inner)); \ + } while (0) + + if (!out64) return 0; + if ((key == NULL && key_len > 0) || (data == NULL && data_len > 0)) return 0; + + memset(key_block, 0, sizeof(key_block)); + if (key_len > sizeof(key_block)) { + if (!fabric_sha512(key, key_len, key_hash)) { + FABRIC_HMAC_SHA512_WIPE(); + return 0; + } + memcpy(key_block, key_hash, sizeof(key_hash)); + } else if (key_len > 0 && key != NULL) { + memcpy(key_block, key, key_len); + } + + for (i = 0; i < sizeof(key_block); i++) { + o_key_pad[i] = key_block[i] ^ 0x5c; + i_key_pad[i] = key_block[i] ^ 0x36; + } + + fabric_sha512_ctx ctx; + fabric_sha512_init(&ctx); + fabric_sha512_update(&ctx, i_key_pad, sizeof(i_key_pad)); + fabric_sha512_update(&ctx, data, data_len); + fabric_sha512_final(&ctx, inner); + + fabric_sha512_init(&ctx); + fabric_sha512_update(&ctx, o_key_pad, sizeof(o_key_pad)); + fabric_sha512_update(&ctx, inner, sizeof(inner)); + fabric_sha512_final(&ctx, out64); + FABRIC_HMAC_SHA512_WIPE(); + return 1; +} + +int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len, + const uint8_t *salt, size_t salt_len, + uint32_t iterations, + uint8_t *out, size_t out_len) +{ + uint32_t block_index = 1; + size_t generated = 0; + uint8_t u[64]; + uint8_t t[64]; + uint8_t sbuf[256]; + +#define FABRIC_PBKDF2_WIPE() do { \ + fabric_secure_zero(u, sizeof(u)); \ + fabric_secure_zero(t, sizeof(t)); \ + fabric_secure_zero(sbuf, sizeof(sbuf)); \ + } while (0) + + if (!out || iterations == 0) return 0; + if ((password == NULL && password_len > 0) || (salt == NULL && salt_len > 0)) return 0; + if (salt_len + 4 > sizeof(sbuf)) return 0; + + while (generated < out_len) { + memcpy(sbuf, salt, salt_len); + sbuf[salt_len + 0] = (uint8_t)((block_index >> 24) & 0xff); + sbuf[salt_len + 1] = (uint8_t)((block_index >> 16) & 0xff); + sbuf[salt_len + 2] = (uint8_t)((block_index >> 8) & 0xff); + sbuf[salt_len + 3] = (uint8_t)(block_index & 0xff); + + if (!fabric_hmac_sha512(password, password_len, sbuf, salt_len + 4, u)) { + FABRIC_PBKDF2_WIPE(); + return 0; + } + memcpy(t, u, sizeof(t)); + + for (uint32_t i = 1; i < iterations; i++) { + if (!fabric_hmac_sha512(password, password_len, u, sizeof(u), u)) { + FABRIC_PBKDF2_WIPE(); + return 0; + } + for (size_t j = 0; j < sizeof(t); j++) t[j] ^= u[j]; + } + + size_t to_copy = (out_len - generated > sizeof(t)) ? sizeof(t) : (out_len - generated); + memcpy(out + generated, t, to_copy); + generated += to_copy; + block_index++; + } + + FABRIC_PBKDF2_WIPE(); + return 1; +} diff --git a/src/sha2.h b/src/sha2.h new file mode 100644 index 000000000..4ddb203d2 --- /dev/null +++ b/src/sha2.h @@ -0,0 +1,25 @@ +#ifndef FABRIC_SHA2_H +#define FABRIC_SHA2_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32]); +int fabric_sha512(const uint8_t *data, size_t len, uint8_t out64[64]); +int fabric_hmac_sha512(const uint8_t *key, size_t key_len, + const uint8_t *data, size_t data_len, + uint8_t out64[64]); +int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len, + const uint8_t *salt, size_t salt_len, + uint32_t iterations, + uint8_t *out, size_t out_len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/tests/bitcoin/transaction.js b/tests/bitcoin/transaction.js index f77f2d2e1..2908e6e9d 100644 --- a/tests/bitcoin/transaction.js +++ b/tests/bitcoin/transaction.js @@ -23,9 +23,11 @@ describe('@fabric/core/types/bitcoin', function () { it('exposes identifier properties', function () { const tx = new BitcoinTransaction({ raw: 'deadbeef' }); - assert.strictEqual(tx.hash, ''); - assert.strictEqual(tx.id, ''); - assert.strictEqual(tx.txid, ''); + assert.ok(Buffer.isBuffer(tx._state.content.raw)); + assert.deepStrictEqual(tx._state.content.raw, Buffer.from('deadbeef', 'hex')); + assert.strictEqual(tx.hash, '281dd50f6f56bc6e867fe73dd614a73c55a647a479704f64804b574cafb0f5c5'); + assert.strictEqual(tx.txid, 'c5f5b0af4c574b80644f7079a447a6553ca714d63de77f866ebc566f0fd51d28'); + assert.strictEqual(tx.id, tx.txid); }); it('can sign as holder', function () { diff --git a/tests/fabric.chain.js b/tests/fabric.chain.js index 6d6eef078..ae789a2d2 100644 --- a/tests/fabric.chain.js +++ b/tests/fabric.chain.js @@ -87,6 +87,23 @@ describe('@fabric/core/types/chain', function () { assert.ok(chain); }); + it('proposeTransaction ignores duplicate ids (single mempool entry)', async function () { + const chain = new Chain(); + const block = new Block({ debug: true, input: 'Hello, world.' }); + + await chain.start(); + await chain.append(block); + + const payload = { type: 'Transaction', input: 'duplicate-proposal' }; + const tx1 = chain.proposeTransaction(payload); + const tx2 = chain.proposeTransaction(payload); + + assert.strictEqual(tx1.id, tx2.id); + assert.strictEqual(tx1, tx2, 'duplicate proposal should return the same Transaction instance'); + assert.strictEqual(chain.mempool.filter((id) => id === tx1.id).length, 1); + await chain.stop(); + }); + it('can mine a second block with transactions', async function () { const chain = new Chain(); const block = new Block({ diff --git a/tests/fabric.environment.js b/tests/fabric.environment.js index 044a98977..1c9fe8862 100644 --- a/tests/fabric.environment.js +++ b/tests/fabric.environment.js @@ -30,6 +30,71 @@ describe('@fabric/core/types/environment', function () { assert.ok(environment); }); + it('does not treat the BIP39 fixture as the default seed when NODE_ENV is not test (fresh install)', function () { + const prev = { + NODE_ENV: process.env.NODE_ENV, + FABRIC_SEED: process.env.FABRIC_SEED, + FABRIC_XPRV: process.env.FABRIC_XPRV, + FABRIC_XPUB: process.env.FABRIC_XPUB + }; + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-env-')); + const store = path.join(home, '.fabric'); + const walletPath = path.join(store, 'wallet.json'); + try { + delete process.env.FABRIC_SEED; + delete process.env.FABRIC_XPRV; + delete process.env.FABRIC_XPUB; + process.env.NODE_ENV = 'production'; + const environment = new Environment({ home, path: walletPath, store }); + environment.start(); + assert.strictEqual(environment.wallet, false, 'in-memory wallet must not be created from the test fixture in production'); + } finally { + for (const k of Object.keys(prev)) { + if (prev[k] === undefined) delete process.env[k]; + else process.env[k] = prev[k]; + } + fs.rmSync(home, { recursive: true, force: true }); + } + }); + + it('does not emit fatal error events when wallet file is empty or invalid JSON', function () { + const prev = { + NODE_ENV: process.env.NODE_ENV, + FABRIC_SEED: process.env.FABRIC_SEED, + FABRIC_XPRV: process.env.FABRIC_XPRV, + FABRIC_XPUB: process.env.FABRIC_XPUB + }; + + try { + delete process.env.FABRIC_SEED; + delete process.env.FABRIC_XPRV; + delete process.env.FABRIC_XPUB; + process.env.NODE_ENV = 'production'; + + for (const content of ['', '{']) { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-env-bad-wallet-')); + const store = path.join(home, '.fabric'); + const walletPath = path.join(store, 'wallet.json'); + fs.mkdirSync(store, { recursive: true }); + fs.writeFileSync(walletPath, content, 'utf8'); + + const environment = new Environment({ home, path: walletPath, store }); + let emittedError = null; + environment.on('error', (err) => { emittedError = err; }); + + environment.start(); + assert.strictEqual(emittedError, null, `unexpected error event for wallet content repr: ${JSON.stringify(content)}`); + assert.strictEqual(environment.wallet, false); + fs.rmSync(home, { recursive: true, force: true }); + } + } finally { + for (const k of Object.keys(prev)) { + if (prev[k] === undefined) delete process.env[k]; + else process.env[k] = prev[k]; + } + } + }); + it('can instantiate from a seed', async function () { const environment = new Environment({ xpub: FIXTURE_SEED }); await environment.start(); diff --git a/tests/fabric.peer.js b/tests/fabric.peer.js index 7c91dce75..4177f6fec 100644 --- a/tests/fabric.peer.js +++ b/tests/fabric.peer.js @@ -767,6 +767,22 @@ describe('@fabric/core/types/peer', function () { }); peer._handleGenericMessage({ type: 'INVENTORY_REQUEST', object: {}, message: {}, origin: {} }, { name: 'o' }); }); + it('normalizes FABRIC_DOCUMENT_OFFER envelope to INVENTORY_REQUEST', function (done) { + const peer = new Peer({ listen: false, peersDb: null }); + peer.once('inventory', (ev) => { + assert.strictEqual(ev.message.type, 'INVENTORY_REQUEST'); + done(); + }); + peer._handleGenericMessage({ type: 'FABRIC_DOCUMENT_OFFER', object: {}, message: {}, origin: {} }, { name: 'o' }); + }); + it('normalizes FABRIC_DOCUMENT_OFFER_RESPONSE envelope to INVENTORY_RESPONSE', function (done) { + const peer = new Peer({ listen: false, peersDb: null }); + peer.once('inventoryResponse', (ev) => { + assert.strictEqual(ev.message.type, 'INVENTORY_RESPONSE'); + done(); + }); + peer._handleGenericMessage({ type: 'FABRIC_DOCUMENT_OFFER_RESPONSE', object: { kind: 'documents', items: [] } }, { name: 'o' }); + }); it('serveLocalDocumentInventory sends INVENTORY_RESPONSE for offerBtc requests', function () { const peer = new Peer({ listen: false, peersDb: null, serveLocalDocumentInventory: true }); peer._state.content.documents = { doca: 'hello' }; @@ -783,6 +799,7 @@ describe('@fabric/core/types/peer', function () { const back = Message.fromBuffer(written); const inner = JSON.parse(back.data); assert.strictEqual(back.type, 'P2P_INVENTORY_RESPONSE'); + assert.strictEqual(inner.kind, 'documents'); assert.strictEqual(inner.items.length, 1); assert.strictEqual(inner.items[0].id, 'doca'); assert.strictEqual(inner.items[0].rateSats, 1000); @@ -819,6 +836,25 @@ describe('@fabric/core/types/peer', function () { }, { name: '127.0.0.1:13' }); assert.strictEqual(writes, 0); }); + it('serveLocalDocumentInventory responds to Hub-style kind:documents catalog requests', function () { + const peer = new Peer({ listen: false, peersDb: null, serveLocalDocumentInventory: true }); + peer._state.content.documents = { catdoc: 'hello catalog' }; + let written = null; + peer.connections['127.0.0.1:14'] = { + _writeFabric: (b) => { written = b; } + }; + peer._handleGenericMessage({ + type: 'INVENTORY_REQUEST', + object: { kind: 'documents', created: Date.now() } + }, { name: '127.0.0.1:14' }); + assert.ok(written && written.length); + const back = Message.fromBuffer(written); + const inner = JSON.parse(back.data); + assert.strictEqual(inner.kind, 'documents'); + assert.strictEqual(inner.items.length, 1); + assert.strictEqual(inner.items[0].id, 'catdoc'); + assert.strictEqual(inner.items[0].published, true); + }); it('relayInventoryRequest relays offerBtc INVENTORY_REQUEST when no local items', function () { const peer = new Peer({ listen: false, peersDb: null, serveLocalDocumentInventory: true, relayInventoryRequest: true }); peer._state.content.documents = {}; diff --git a/tests/fabricPaymentBech32.unit.js b/tests/fabricPaymentBech32.unit.js new file mode 100644 index 000000000..3bc3e0892 --- /dev/null +++ b/tests/fabricPaymentBech32.unit.js @@ -0,0 +1,108 @@ +'use strict'; + +const assert = require('assert'); +const crypto = require('crypto'); +const fp = require('../functions/fabricPaymentBech32'); + +describe('@fabric/core/functions/fabricPaymentBech32', function () { + it('encodeFabricRoutedPaymentV0 round-trips decodeFabricRoutedPayment', function () { + const hash32 = crypto.randomBytes(32); + const enc = fp.encodeFabricRoutedPaymentV0({ hash32 }); + assert.ok(enc.toLowerCase().startsWith('fa1'), enc); + const dec = fp.decodeFabricRoutedPayment(enc); + assert.ok(dec); + assert.strictEqual(dec.version, 0); + assert.strictEqual(dec.routeType, 0); + assert.ok(Buffer.isBuffer(dec.hash32)); + assert.deepStrictEqual(dec.hash32, hash32); + }); + + it('reject wrong hash length', function () { + assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: Buffer.alloc(31) }), /32 bytes/); + }); + + it('encodeFabricRoutedPaymentV0 rejects invalid inputs', function () { + assert.throws(() => fp.encodeFabricRoutedPaymentV0(), /hash32/); + assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: 'not-bytes' }), /Buffer/); + assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: Buffer.alloc(32), routeType: 1.5 }), /integer/); + assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: Buffer.alloc(32), routeType: 300 }), /255/); + }); + + it('decodeFabricRoutedPayment returns null for lnbc or garbage', function () { + assert.strictEqual(fp.decodeFabricRoutedPayment('lnbc1qqqqqqqqqqqq'), null); + assert.strictEqual(fp.decodeFabricRoutedPayment('fa1broken'), null); + }); + + it('classifyPaymentEncodingString prefers fa over ln', function () { + const h = crypto.randomBytes(32); + const fa = fp.encodeFabricRoutedPaymentV0({ hash32: h }); + assert.strictEqual(fp.classifyPaymentEncodingString(fa), 'fabric_routed_payment'); + assert.strictEqual(fp.classifyPaymentEncodingString('lno1qqqq'), 'bolt12_offer'); + assert.strictEqual(fp.classifyPaymentEncodingString('lnr1qqqq'), 'bolt12_invoice_request'); + }); + + it('classifyPaymentEncodingString empty and unknown', function () { + assert.strictEqual(fp.classifyPaymentEncodingString(''), 'empty'); + assert.strictEqual(fp.classifyPaymentEncodingString(' '), 'empty'); + assert.strictEqual(fp.classifyPaymentEncodingString('xyz'), 'unknown'); + assert.strictEqual(fp.classifyPaymentEncodingString(null), 'empty'); + }); + + it('isFabricRoutedPaymentString', function () { + assert.strictEqual(fp.isFabricRoutedPaymentString('fa1qqqq'), true); + assert.strictEqual(fp.isFabricRoutedPaymentString('FA1qqqq'), true); + assert.strictEqual(fp.isFabricRoutedPaymentString('lno1qq'), false); + assert.strictEqual(fp.isFabricRoutedPaymentString(''), false); + }); + + it('encodeFabricRoutedPaymentV0 rejects malformed byte array hash32', function () { + assert.throws( + () => fp.encodeFabricRoutedPaymentV0({ hash32: [0, 1, 2] }), + /exactly 32 elements/ + ); + assert.throws( + () => fp.encodeFabricRoutedPaymentV0({ hash32: Array(32).fill(1.5) }), + /0\.\.255/ + ); + assert.throws( + () => fp.encodeFabricRoutedPaymentV0({ hash32: Array(32).fill(256) }), + /0\.\.255/ + ); + }); + + it('encodeFabricRoutedPaymentV0 accepts validated 32-byte number array', function () { + const arr = Array.from(crypto.randomBytes(32)); + const enc = fp.encodeFabricRoutedPaymentV0({ hash32: arr }); + const dec = fp.decodeFabricRoutedPayment(enc); + assert.ok(dec); + assert.deepStrictEqual(Array.from(dec.hash32), arr); + }); + + it('encodeFabricRoutedPaymentV0 accepts Uint8Array hash32', function () { + const u8 = new Uint8Array(32); + u8[0] = 9; + const enc = fp.encodeFabricRoutedPaymentV0({ hash32: u8 }); + const dec = fp.decodeFabricRoutedPayment(enc); + assert.ok(dec); + assert.strictEqual(dec.hash32[0], 9); + }); + + it('encodeFabricRoutedPaymentV0 non-default routeType round-trips', function () { + const hash32 = crypto.randomBytes(32); + const enc = fp.encodeFabricRoutedPaymentV0({ hash32, routeType: 7 }); + const dec = fp.decodeFabricRoutedPayment(enc); + assert.ok(dec); + assert.strictEqual(dec.routeType, 7); + }); + + it('decodeFabricRoutedPayment rejects empty input', function () { + assert.strictEqual(fp.decodeFabricRoutedPayment(''), null); + assert.strictEqual(fp.decodeFabricRoutedPayment(null), null); + }); + + it('exports constants', function () { + assert.strictEqual(fp.FABRIC_ROUTED_PAYMENT_HRP, 'fa'); + assert.strictEqual(fp.FABRIC_ROUTED_PAYMENT_PREFIX, 'fa1'); + assert.strictEqual(fp.FabricRouteType.DOCUMENT_CONTENT_HASH, 0); + }); +}); diff --git a/tests/lightning/bolt12.classify.unit.js b/tests/lightning/bolt12.classify.unit.js new file mode 100644 index 000000000..6bd93f535 --- /dev/null +++ b/tests/lightning/bolt12.classify.unit.js @@ -0,0 +1,83 @@ +'use strict'; + +const assert = require('assert'); +const lb = require('../../functions/lightningBolt12'); +const fp = require('../../functions/fabricPaymentBech32'); +const Lightning = require('../../services/lightning'); + +describe('@fabric/core/functions/lightningBolt12', function () { + it('classifies lno1 as bolt12_offer', function () { + assert.strictEqual(lb.classifyLightningEncodedString('lno1qqqqqqqqqqqq'), 'bolt12_offer'); + assert.strictEqual(lb.isBolt12OfferString('LNO1TEST'), true); + }); + + it('normalizeBolt12ScanString strips + continuations per BOLT12 § Encoding', function () { + assert.strictEqual( + lb.normalizeBolt12ScanString('lno1xxxx+\nyyyyyyyyyyyy+\nzzzzzz'), + 'lno1xxxxyyyyyyyyyyyyzzzzzz' + ); + assert.strictEqual(lb.normalizeBolt12ScanString(' '), ''); + assert.strictEqual(lb.classifyLightningEncodedString('lno1aa+\nbb'), 'bolt12_offer'); + }); + + it('classifies lnr1 as bolt12_invoice_request', function () { + assert.strictEqual(lb.classifyLightningEncodedString('lnr1qqqqqqqqqqqq'), 'bolt12_invoice_request'); + assert.strictEqual(lb.isBolt12InvoiceRequestString('lnr1x'), true); + }); + + it('classifies common bolt11 HRPs', function () { + assert.strictEqual(lb.classifyLightningEncodedString('lnbc1qqqq'), 'bolt11_invoice'); + assert.strictEqual(lb.classifyLightningEncodedString('lntb1qqqq'), 'bolt11_invoice'); + assert.strictEqual(lb.classifyLightningEncodedString('lnbcrt1qqqq'), 'bolt11_invoice'); + assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnbc1'), true); + }); + + it('returns empty or unknown appropriately', function () { + assert.strictEqual(lb.classifyLightningEncodedString(''), 'empty'); + assert.strictEqual(lb.classifyLightningEncodedString(' '), 'empty'); + assert.strictEqual(lb.classifyLightningEncodedString('notlightning'), 'unknown'); + }); + + it('isBolt12OfferString and isLikelyBolt11InvoiceString edge cases', function () { + assert.strictEqual(lb.isBolt12OfferString('lnr1qqqq'), false); + assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnbc'), false); + assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnbc1'), true); + assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnsb1qq'), true); + }); + + it('FabricLightningOfferRole has expected keys', function () { + assert.deepStrictEqual(Object.keys(lb.FabricLightningOfferRole).sort(), [ + 'fabric_document_commerce', + 'fabric_peer_handshake', + 'lightning_bolt12_invoice_request', + 'lightning_bolt12_offer' + ]); + }); + + it('exports BOLT11_INVOICE_HRPS', function () { + assert.ok(Array.isArray(lb.BOLT11_INVOICE_HRPS)); + assert.ok(lb.BOLT11_INVOICE_HRPS.includes('lnbc')); + }); + + it('FabricLightningMarketRole aliases FabricLightningOfferRole', function () { + assert.strictEqual(lb.FabricLightningMarketRole, lb.FabricLightningOfferRole); + assert.strictEqual(lb.FabricLightningMarketRole.lightning_bolt12_offer, 'lightning_bolt12_offer'); + }); +}); + +describe('@fabric/core/services/lightning Bolt12 bridge', function () { + it('Lightning.Bolt12 re-exports lightningBolt12', function () { + assert.strictEqual(Lightning.Bolt12, lb); + assert.strictEqual(Lightning.Bolt12.classifyLightningEncodedString('lno1x'), 'bolt12_offer'); + }); + + it('Lightning.FabricPayment re-exports fabricPaymentBech32', function () { + assert.strictEqual(Lightning.FabricPayment, fp); + }); + + it('Lightning.DOCS includes fabricLightningMarkets, fabricLightningOffers, fabricPaymentBech32', function () { + assert.strictEqual(Lightning.DOCS.fabricLightningOffers, 'docs/FABRIC_LIGHTNING_OFFERS.md'); + assert.strictEqual(Lightning.DOCS.fabricLightningMarkets, Lightning.DOCS.fabricLightningOffers); + assert.strictEqual(Lightning.DOCS.fabricPaymentBech32, 'docs/FABRIC_PAYMENT_BECH32.md'); + }); +}); diff --git a/tests/lightning/bolt12.semantics.unit.js b/tests/lightning/bolt12.semantics.unit.js new file mode 100644 index 000000000..36359d919 --- /dev/null +++ b/tests/lightning/bolt12.semantics.unit.js @@ -0,0 +1,58 @@ +'use strict'; + +const assert = require('assert'); +const bs = require('../../functions/bolt12Semantics'); + +describe('@fabric/core/functions/bolt12Semantics', function () { + it('classifyDecodedBolt12 recognizes CLN-style type strings', function () { + assert.strictEqual(bs.classifyDecodedBolt12({ type: 'bolt12 offer' }), bs.Bolt12StreamKind.bolt12_offer); + assert.strictEqual( + bs.classifyDecodedBolt12({ type: 'bolt12 invoice_request' }), + bs.Bolt12StreamKind.bolt12_invoice_request + ); + assert.strictEqual( + bs.classifyDecodedBolt12({ type: 'bolt12 invoice' }), + bs.Bolt12StreamKind.bolt12_invoice + ); + assert.strictEqual( + bs.classifyDecodedBolt12({ type: 'bolt11 invoice' }), + bs.Bolt12StreamKind.bolt11_invoice + ); + assert.strictEqual(bs.classifyDecodedBolt12(null), bs.Bolt12StreamKind.unknown); + }); + + it('bip340SignatureApplies only for invoice_request and invoice', function () { + assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_offer), false); + assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice_request), true); + assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice), true); + }); + + it('summarizeBolt12RecurrenceFromDecode collects known keys', function () { + const decoded = { + offer_recurrence: { period: 7, time_unit: 1 }, + invreq_recurrence_counter: 2, + invoice_recurrence_basetime: 1700000000 + }; + const sum = bs.summarizeBolt12RecurrenceFromDecode(decoded); + assert.ok(sum); + assert.strictEqual(sum.invreq_recurrence_counter, 2); + assert.ok(sum.offer_recurrence); + }); + + it('BOLT12_TLV aliases CLN_BOLT12_TLV', function () { + assert.strictEqual(bs.BOLT12_TLV, bs.CLN_BOLT12_TLV); + }); + + it('CLN_BOLT12_TLV exposes recurrence and BOLT #12 invoice_request / invoice TLV ids', function () { + assert.strictEqual(bs.CLN_BOLT12_TLV.OFFER_RECURRENCE_COMPULSORY, 24); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_FEATURES, 84); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_PAYER_NOTE, 89); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_PATHS, 90); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_BIP353_NAME, 91); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_RECURRENCE_COUNTER, 92); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVOICE_RELATIVE_EXPIRY, 166); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVOICE_FALLBACKS, 172); + assert.strictEqual(bs.CLN_BOLT12_TLV.INVOICE_FEATURES, 174); + assert.strictEqual(bs.CLN_BOLT12_TLV.SIGNATURE_MIN, 240); + }); +}); diff --git a/tests/lightning/lightning.service.unit.js b/tests/lightning/lightning.service.unit.js index eda206cdf..a65aa0200 100644 --- a/tests/lightning/lightning.service.unit.js +++ b/tests/lightning/lightning.service.unit.js @@ -10,6 +10,32 @@ const net = require('net'); const Lightning = require('../../services/lightning'); describe('@fabric/core/services/lightning (unit)', function () { + describe('Lightning.DOCS', function () { + it('exposes Markdown paths relative to the package root', function () { + assert.strictEqual(Lightning.DOCS.boltCompatibility, 'docs/BOLT_COMPATIBILITY.md'); + assert.strictEqual(Lightning.DOCS.fabricLightningOffers, 'docs/FABRIC_LIGHTNING_OFFERS.md'); + assert.strictEqual(Lightning.DOCS.fabricLightningMarkets, Lightning.DOCS.fabricLightningOffers); + assert.strictEqual(Lightning.DOCS.fabricPaymentBech32, 'docs/FABRIC_PAYMENT_BECH32.md'); + assert.strictEqual(Lightning.DOCS.lightningCompat, 'docs/LIGHTNING_COMPAT.md'); + }); + }); + + describe('Lightning BOLT12 static exports', function () { + it('re-exports bolt12Semantics as Bolt12Semantics', function () { + const bolt12Semantics = require('../../functions/bolt12Semantics'); + const lightningPath = require.resolve('../../services/lightning'); + const src = fs.readFileSync(lightningPath, 'utf8'); + assert.ok( + /\bBolt12Semantics\b/.test(src) && src.includes('bolt12Semantics'), + `expected ${path.relative(process.cwd(), lightningPath)} to wire Bolt12Semantics` + ); + assert.ok(Lightning.Bolt12, 'Lightning.Bolt12'); + assert.ok(Lightning.FabricPayment, 'Lightning.FabricPayment'); + assert.ok(Lightning.Bolt12Semantics, 'Lightning.Bolt12Semantics'); + assert.strictEqual(Lightning.Bolt12Semantics, bolt12Semantics); + }); + }); + describe('defaultListenPortForNetwork', function () { it('maps networks to conventional lightningd bind ports', function () { assert.strictEqual(Lightning.defaultListenPortForNetwork('mainnet'), 9735); @@ -193,6 +219,209 @@ describe('@fabric/core/services/lightning (unit)', function () { }); }); + describe('JSON-RPC extension & BOLT12 helpers', function () { + it('callRpc forwards to _makeRPCRequest with timeout', async function () { + const ln = new Lightning(); + let seenTimeout; + ln._makeRPCRequest = async (method, params, timeoutMs) => { + assert.strictEqual(method, 'listpeers'); + assert.deepStrictEqual(params, []); + seenTimeout = timeoutMs; + return { peers: [] }; + }; + const out = await ln.callRpc('listpeers', [], 5000); + assert.deepStrictEqual(out, { peers: [] }); + assert.strictEqual(seenTimeout, 5000); + }); + + it('constructor leaves rpc null without shadowing callRpc', async function () { + const ln = new Lightning(); + assert.strictEqual(ln.rpc, null); + assert.strictEqual(typeof ln.callRpc, 'function'); + }); + + it('decodeLightning calls decode', async function () { + const ln = new Lightning(); + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'decode'); + assert.deepStrictEqual(params, ['lno1xxx']); + return { type: 'bolt12 offer' }; + }; + const out = await ln.decodeLightning('lno1xxx'); + assert.strictEqual(out.type, 'bolt12 offer'); + }); + + it('decodePay calls decodepay', async function () { + const ln = new Lightning(); + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'decodepay'); + assert.deepStrictEqual(params, ['lnbc1xxx']); + return { payment_hash: 'ph' }; + }; + await ln.decodePay('lnbc1xxx'); + }); + + it('createOffer calls offer with object', async function () { + const ln = new Lightning(); + const p = { amount_msat: '1000', description: 'd' }; + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'offer'); + assert.deepStrictEqual(params, [p]); + return { bolt12: 'lno1...' }; + }; + const out = await ln.createOffer(p); + assert.strictEqual(out.bolt12, 'lno1...'); + }); + + it('createOffer rejects non-object', async function () { + const ln = new Lightning(); + await assert.rejects(() => ln.createOffer(null), /requires a params object/); + }); + + it('fetchInvoice passes offer only, offer + params, keyword object, or merged keyword objects', async function () { + const ln = new Lightning(); + let calls = 0; + ln._makeRPCRequest = async (method, params) => { + calls++; + assert.strictEqual(method, 'fetchinvoice'); + if (calls === 1) assert.deepStrictEqual(params, ['lno1a']); + else if (calls === 2) assert.deepStrictEqual(params, ['lno1b', { amount_msat: '5000' }]); + else if (calls === 3) { + assert.deepStrictEqual(params, [{ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' }]); + } else { + assert.deepStrictEqual(params, [{ + offer: 'lno1d', + amount_msat: '1000', + payer_note: 'merged' + }]); + } + return {}; + }; + await ln.fetchInvoice('lno1a'); + await ln.fetchInvoice('lno1b', { amount_msat: '5000' }); + await ln.fetchInvoice({ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' }); + await ln.fetchInvoice({ offer: 'lno1d', amount_msat: '1000' }, { payer_note: 'merged' }); + }); + + it('fetchInvoice rejects invalid first arg', async function () { + const ln = new Lightning(); + await assert.rejects(() => ln.fetchInvoice(1), /offer string or params object/); + await assert.rejects(() => ln.fetchInvoice({}), /offer string or params object/); + }); + + it('pay passes string or object to pay RPC', async function () { + const ln = new Lightning(); + let n = 0; + ln._makeRPCRequest = async (method, params, t) => { + n++; + assert.strictEqual(method, 'pay'); + if (n === 1) assert.deepStrictEqual(params, ['lnbc1']); + else assert.deepStrictEqual(params, [{ bolt11: 'lnbc1', maxfeepercent: 0.5 }]); + assert.strictEqual(t, 8000); + return { status: 'complete' }; + }; + await ln.pay('lnbc1', 8000); + await ln.pay({ bolt11: 'lnbc1', maxfeepercent: 0.5 }, 8000); + }); + + it('listOffers with no filter, object, or offer_id string', async function () { + const ln = new Lightning(); + let n = 0; + ln._makeRPCRequest = async (method, params) => { + n++; + assert.strictEqual(method, 'listoffers'); + if (n === 1) assert.deepStrictEqual(params, []); + else if (n === 2) assert.deepStrictEqual(params, [{ active_only: true }]); + else assert.deepStrictEqual(params, ['offer_id_hex']); + return { offers: [] }; + }; + await ln.listOffers(); + await ln.listOffers({ active_only: true }); + await ln.listOffers('offer_id_hex'); + }); + + it('createInvoiceRequest calls invoicerequest', async function () { + const ln = new Lightning(); + const p = { amount: '1000sat', description: 'req' }; + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'invoicerequest'); + assert.deepStrictEqual(params, [p]); + return { bolt12: 'lnr1...' }; + }; + const out = await ln.createInvoiceRequest(p); + assert.strictEqual(out.bolt12, 'lnr1...'); + }); + + it('listInvoiceRequests with null, string, or object', async function () { + const ln = new Lightning(); + let n = 0; + ln._makeRPCRequest = async (method, params) => { + n++; + assert.strictEqual(method, 'listinvoicerequests'); + if (n === 1) assert.deepStrictEqual(params, []); + else if (n === 2) assert.deepStrictEqual(params, ['invreqid']); + else assert.deepStrictEqual(params, [{ active_only: true }]); + return { invoicerequests: [] }; + }; + await ln.listInvoiceRequests(); + await ln.listInvoiceRequests('invreqid'); + await ln.listInvoiceRequests({ active_only: true }); + }); + + it('disableInvoiceRequest calls disableinvoicerequest', async function () { + const ln = new Lightning(); + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'disableinvoicerequest'); + assert.deepStrictEqual(params, ['invreq_x']); + return { active: false }; + }; + await ln.disableInvoiceRequest('invreq_x'); + }); + + it('sendInvoice calls sendinvoice with object', async function () { + const ln = new Lightning(); + const p = { invreq: 'lnr1abc', label: 'lbl' }; + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'sendinvoice'); + assert.deepStrictEqual(params, [p]); + return { status: 'paid' }; + }; + const out = await ln.sendInvoice(p); + assert.strictEqual(out.status, 'paid'); + }); + + it('disableOffer calls disableoffer', async function () { + const ln = new Lightning(); + ln._makeRPCRequest = async (method, params) => { + assert.strictEqual(method, 'disableoffer'); + assert.deepStrictEqual(params, ['offer_id_1']); + return { disabled: true }; + }; + await ln.disableOffer('offer_id_1'); + }); + + it('getRoute passes three args, fourth as cltv, or full tail via routeOptions (e.g. maxhops)', async function () { + const ln = new Lightning(); + let n = 0; + ln._makeRPCRequest = async (method, params) => { + n++; + assert.strictEqual(method, 'getroute'); + if (n === 1) assert.deepStrictEqual(params, ['dest', 1e7, 10]); + else if (n === 2) assert.deepStrictEqual(params, ['dest', 1e7, 1, 0]); + else { + assert.deepStrictEqual( + params, + ['dest', 1e7, 10, null, null, null, null, 5] + ); + } + return { route: [] }; + }; + await ln.getRoute('dest', 1e7); + await ln.getRoute('dest', 1e7, 1, 0); + await ln.getRoute('dest', 1e7, 10, { maxhops: 5 }); + }); + }); + describe('computeLiquidity', function () { it('returns outbound and inbound formatted BTC', async function () { const ln = new Lightning(); diff --git a/types/bitcoin/transaction.js b/types/bitcoin/transaction.js index cf3f2af49..9396eb3c3 100644 --- a/types/bitcoin/transaction.js +++ b/types/bitcoin/transaction.js @@ -5,6 +5,31 @@ const crypto = require('crypto'); const Actor = require('../actor'); const Key = require('../key'); // TODO: PSBTs +// PSBT support remains future work (see JS-PLAN / Bitcoin service). +function rawTransactionBuffer (raw) { + if (raw == null) return Buffer.alloc(0); + if (Buffer.isBuffer(raw)) return raw; + const s = String(raw).replace(/\s+/g, ''); + if (!s.length || s.length % 2 !== 0) return Buffer.alloc(0); + if (!/^[0-9a-fA-F]+$/.test(s)) return Buffer.alloc(0); + return Buffer.from(s, 'hex'); +} + +function bitcoinTxidHex (buf) { + const h = crypto.createHash('sha256').update(buf).digest(); + const h2 = crypto.createHash('sha256').update(h).digest(); + return Buffer.from(h2).reverse().toString('hex'); +} + +function doubleSha256Hex (buf) { + const h = crypto.createHash('sha256').update(buf).digest(); + return crypto.createHash('sha256').update(h).digest('hex'); +} + +function doubleSha256Buf (buf) { + const h = crypto.createHash('sha256').update(buf).digest(); + return crypto.createHash('sha256').update(h).digest(); +} class BitcoinTransaction extends Actor { constructor (settings = {}) { @@ -23,7 +48,7 @@ class BitcoinTransaction extends Actor { this._state = { content: { - raw: null + raw: this.settings.raw != null ? rawTransactionBuffer(this.settings.raw) : null }, status: 'PAUSED' }; @@ -31,21 +56,28 @@ class BitcoinTransaction extends Actor { return this; } + _rawBuf () { + if (this._state.content.raw != null) { + return this._state.content.raw; + } + return Buffer.alloc(0); + } + get hash () { - return ''; // TODO: real hash + return doubleSha256Hex(this._rawBuf()); } get id () { - return ''; // TODO: Fabric ID + return this.txid; } get txid () { - return ''; // TODO: bitcoin txid + return bitcoinTxidHex(this._rawBuf()); } signAsHolder () { - const hash = crypto.createHash('sha256').update('').digest('hex'); - this.signature = this.holder.sign(hash); + const digest = doubleSha256Buf(this._rawBuf()); + this.signature = this.holder.signSchnorrHash(digest); return this; } } diff --git a/types/chain.js b/types/chain.js index 46fd373b3..56836c18c 100644 --- a/types/chain.js +++ b/types/chain.js @@ -86,7 +86,7 @@ class Chain extends Actor { } get height () { - + return this.blocks.length; } get leaves () { @@ -119,12 +119,12 @@ class Chain extends Actor { actor: proposal.actor || Actor.randomBytes(32).toString('hex'), changes: proposal.changes, mode: proposal.mode || 'NAIVE_SIGHASH_SINGLE', - object: Buffer.concat( + object: Buffer.concat([ Buffer.alloc(32), // pubkey Buffer.alloc(32), // parent Buffer.alloc(32), // changes - Buffer.alloc(64), // signature - ), + Buffer.alloc(64) // signature + ]), parent: this.id, signature: Buffer.alloc(64), state: this.state, @@ -135,7 +135,11 @@ class Chain extends Actor { proposeTransaction (transaction) { const actor = new Transaction(transaction); - // TODO: reject duplicate transactions + const prior = this._state.transactions[actor.id]; + if (prior) { + return prior; + } + this._state.transactions[actor.id] = actor; this._state.mempool.push(actor.id); @@ -152,7 +156,7 @@ class Chain extends Actor { super.trust(source, 'TIMECHAIN'); - source.on('message', function TODO (message) { + source.on('message', function onTrustedSourceMessage (message) { self.emit('debug', `Message from trusted source: ${message}`); }); diff --git a/types/environment.js b/types/environment.js index 4069cb9ff..396602c90 100644 --- a/types/environment.js +++ b/types/environment.js @@ -83,12 +83,16 @@ class Environment extends Entity { } get seed () { - return [ - FIXTURE_SEED, + const explicit = [ this.settings.seed, this['FABRIC_SEED'], - this.readVariable('FABRIC_SEED') - ].find(any); + this.readVariable('FABRIC_SEED'), + this.local + ]; + if (process.env.NODE_ENV === 'test') { + explicit.push(FIXTURE_SEED); + } + return explicit.find(any); } get xprv () { @@ -598,9 +602,17 @@ class Environment extends Entity { }); } else if (this.walletExists()) { const data = this.readWallet(); + const text = typeof data === 'string' ? data : String(data ?? ''); + + if (text.trim() === '') { + if (this.emit) { + this.emit('warning', `[FABRIC:KEYGEN] Wallet file is empty (${this.settings.path}); remove it or regenerate with fabric setup`); + } + this.wallet = false; + return this; + } try { - const text = typeof data === 'string' ? data : String(data ?? ''); const pr = tryParsePersistedJson(text); if (!pr.ok) throw pr.error; const input = pr.value; @@ -617,7 +629,9 @@ class Environment extends Entity { } }); } catch (exception) { - if (this.emit) this.emit('error', `[FABRIC:KEYGEN] Could not load wallet data: ${exception.message || exception}`); + // Recoverable user-data issue; do not emit "error" (EventEmitter kills the process with no listeners). + if (this.emit) this.emit('warning', `[FABRIC:KEYGEN] Could not load wallet data: ${exception.message || exception}`); + this.wallet = false; } } else { this.wallet = false; diff --git a/types/peer.js b/types/peer.js index 5b3f6e768..2e6b3703b 100644 --- a/types/peer.js +++ b/types/peer.js @@ -34,6 +34,9 @@ const { whitelistedDocumentFields, purchaseContentHashHex } = require('../functions/publishedDocumentEnvelope'); +const { + normalizeFabricDocumentOfferEnvelopeForHandlers +} = require('../functions/fabricDocumentOfferEnvelope'); // Strict JSON const { @@ -131,7 +134,8 @@ class Peer extends Service { port: 7777, listenPortAttempts: 20, reconnectToKnownPeers: true, - // When true, answers INVENTORY_REQUEST (offerBtc) using local documents/rates. + // When true, answers INVENTORY_REQUEST: `offerBtc` (L1 offers) and `kind: 'documents'` + // (Hub / browser catalog) using local `_state.content.documents` (+ optional rates/collections). serveLocalDocumentInventory: false, // Re-send canonical DocumentPublish + pricing to new inbound peers. announceDocumentsOnPeerConnect: false, @@ -1648,47 +1652,51 @@ class Peer extends Service { } _handleGenericMessage (message, origin = null, socket = null, wireMessage = null) { - if (this.settings.debug) this.emit('debug', `Generic message:\n\tFrom: ${JSON.stringify(origin)}\n\tType: ${message.type}\n\tBody:\n\`\`\`\n${JSON.stringify(message.object, null, ' ')}\n\`\`\``); + const msg = normalizeFabricDocumentOfferEnvelopeForHandlers(message); + if (this.settings.debug) this.emit('debug', `Generic message:\n\tFrom: ${JSON.stringify(origin)}\n\tType: ${msg.type}\n\tBody:\n\`\`\`\n${JSON.stringify(msg.object, null, ' ')}\n\`\`\``); const signerPubkeyHex = wireMessage ? this._verifiedFabricSignerPubkeyHex(wireMessage) - : normalizePeerPubkeyHex(message && message.actor && (message.actor.publicKey || message.actor.pubkey)); + : normalizePeerPubkeyHex(msg && msg.actor && (msg.actor.publicKey || msg.actor.pubkey)); // Lookup the appropriate Actor for the message's origin const actor = new Actor(origin); - switch (message.type) { + switch (msg.type) { default: - this.emit('debug', `Unhandled Generic Message: ${message.type} ${JSON.stringify(message, null, ' ')}`); + this.emit('debug', `Unhandled Generic Message: ${msg.type} ${JSON.stringify(msg, null, ' ')}`); break; case 'INVENTORY_REQUEST': // Upstream Inventory request (typically for documents). Emit an 'inventory' // event so higher-level services (e.g. hub) can respond appropriately. - this.emit('inventory', { message, origin, socket }); + // JSON `type` may be legacy `INVENTORY_REQUEST` or Fabric alias `FABRIC_DOCUMENT_OFFER` (see `functions/fabricDocumentOfferEnvelope.js`). + this.emit('inventory', { message: msg, origin, socket }); if (this.settings.serveLocalDocumentInventory) { - const served = this._respondInventoryFromLocalDocuments(message, origin); - const req = message.object || {}; + const served = this._respondInventoryFromLocalDocuments(msg, origin); + const req = msg.object || {}; if (this.settings.relayInventoryRequest && !served && req.offerBtc === true) { - this._relayGenericPayload(origin && origin.name, message, socket, wireMessage); + // Relay path matches L1 `offerBtc` requests; Hub-driven `kind:documents` relays use TTL in app code. + this._relayGenericPayload(origin && origin.name, msg, socket, wireMessage); } } break; case 'INVENTORY_RESPONSE': // Document inventory reply (may include per-item L1 HTLC offers). - this.emit('inventoryResponse', { message, origin, socket }); + // JSON `type` may be legacy `INVENTORY_RESPONSE` or Fabric alias `FABRIC_DOCUMENT_OFFER_RESPONSE`. + this.emit('inventoryResponse', { message: msg, origin, socket }); if (this.settings.relayInventoryResponse) { - this._relayGenericPayload(origin && origin.name, message, socket, wireMessage); + this._relayGenericPayload(origin && origin.name, msg, socket, wireMessage); } break; case 'P2P_SESSION_OFFER': - this._handleSessionOfferGenericMessage(message, origin, socket, signerPubkeyHex); + this._handleSessionOfferGenericMessage(msg, origin, socket, signerPubkeyHex); break; case 'P2P_SESSION_OPEN': - this._handleSessionOpenGenericMessage(message, origin, signerPubkeyHex); + this._handleSessionOpenGenericMessage(msg, origin, signerPubkeyHex); break; case 'P2P_CHAT_MESSAGE': - this.emit('chat', message); - const relay = Message.fromVector(['ChatMessage', JSON.stringify(message)]); + this.emit('chat', msg); + const relay = Message.fromVector(['ChatMessage', JSON.stringify(msg)]); relay.signWithKey(this.key); // this.emit('debug', `Relayed chat message: ${JSON.stringify(relay.toGenericMessage())}`); this.relayFrom(origin.name, relay); @@ -1956,41 +1964,104 @@ class Peer extends Service { } /** - * Reply to `INVENTORY_REQUEST` with `INVENTORY_RESPONSE` built from local documents and rates. - * @param {{type: string, object: (Object|undefined)}} message Generic body from {@link Peer#_handleGenericMessage} - * @param {{ name: string }} origin - * @returns {boolean} true if an `INVENTORY_RESPONSE` was written to the requester + * Items for Hub-style `kind: 'documents'` inventory (Fabric UI / `@fabric/hub` merge expects `object.kind`). + * @param {Object} [req] request object subset + * @returns {object[]} */ - _respondInventoryFromLocalDocuments (message, origin) { - if (!origin || !origin.name) return false; - const req = message.object || {}; - if (req.offerBtc !== true) return false; + _collectDocumentCatalogInventoryItems (_req) { const docs = this._state.content.documents; - if (!docs || typeof docs !== 'object') return false; + if (!docs || typeof docs !== 'object') return []; + const collections = this._state.content.collections && typeof this._state.content.collections.documents === 'object' + ? this._state.content.collections.documents + : {}; const rates = this._state.content.documentRates || {}; - const maxSats = req.maxSats; + /** @type {object[]} */ const items = []; for (const docId of Object.keys(docs)) { const body = docs[docId]; const parsed = this._buildDocumentParsedForPublish(docId, body); - const contentHash = purchaseContentHashHex(docId, parsed); + const row = collections[docId]; + const purchaseFromCollection = row && Number(row.purchasePriceSats) > 0 ? Math.round(Number(row.purchasePriceSats)) : null; const rateSats = Object.prototype.hasOwnProperty.call(rates, docId) ? rates[docId] : 0; - if (maxSats != null && Number.isFinite(maxSats) && rateSats > maxSats) continue; - items.push({ id: docId, rateSats, contentHash, network: 'bitcoin' }); + const purchasePriceSats = purchaseFromCollection != null ? purchaseFromCollection : (Number(rateSats) > 0 ? Math.round(Number(rateSats)) : undefined); + const published = row ? !!row.published : true; + items.push({ + id: parsed.id, + sha256: parsed.sha256 || parsed.id, + name: parsed.name, + mime: parsed.mime || 'application/octet-stream', + size: parsed.size, + created: parsed.created || new Date().toISOString(), + published, + ...(purchasePriceSats != null && purchasePriceSats > 0 ? { purchasePriceSats } : {}), + ...(row && row.bitcoinHeight != null && Number.isFinite(Number(row.bitcoinHeight)) + ? { bitcoinHeight: Math.round(Number(row.bitcoinHeight)) } + : {}), + ...(row && row.bitcoinBlockHash ? { bitcoinBlockHash: String(row.bitcoinBlockHash) } : {}), + ...(row && row.bitcoinTxid ? { bitcoinTxid: String(row.bitcoinTxid) } : {}) + }); } - if (!items.length) return false; - const conn = this.connections[origin.name]; + return items; + } + + /** + * Write {@link INVENTORY_RESPONSE} (`P2P_INVENTORY_RESPONSE`) compatible with `@fabric/hub` Bridge merging + * (body includes `kind: 'documents'` so the browser can merge `object.items`). + * @param {string} originName connection key {@link Peer#connections} + * @param {object[]} items + * @returns {boolean} + */ + _sendLocalInventoryDocumentsWireResponse (originName, items) { + if (!originName || !items || !items.length) return false; + const conn = this.connections[originName]; if (!conn || !conn._writeFabric) return false; - const payload = { - type: 'INVENTORY_RESPONSE', - object: { items } + const obj = { + kind: 'documents', + items, + created: Date.now() }; - const m = Message.fromVector(['P2P_INVENTORY_RESPONSE', JSON.stringify(payload.object || {})]); + const m = Message.fromVector(['P2P_INVENTORY_RESPONSE', JSON.stringify(obj)]); m.signWithKey(this.key); conn._writeFabric(m.toBuffer()); return true; } + /** + * Reply to `INVENTORY_REQUEST` with `INVENTORY_RESPONSE` built from local documents and rates. + * @param {{type: string, object: (Object|undefined)}} message Generic body from {@link Peer#_handleGenericMessage} + * @param {{ name: string }} origin + * @returns {boolean} true if an `INVENTORY_RESPONSE` was written to the requester + */ + _respondInventoryFromLocalDocuments (message, origin) { + if (!origin || !origin.name) return false; + const req = message.object || {}; + const docs = this._state.content.documents; + if (!docs || typeof docs !== 'object') return false; + + if (req.offerBtc === true) { + const rates = this._state.content.documentRates || {}; + const maxSats = req.maxSats; + /** @type {object[]} */ + const items = []; + for (const docId of Object.keys(docs)) { + const body = docs[docId]; + const parsed = this._buildDocumentParsedForPublish(docId, body); + const contentHash = purchaseContentHashHex(docId, parsed); + const rateSats = Object.prototype.hasOwnProperty.call(rates, docId) ? rates[docId] : 0; + if (maxSats != null && Number.isFinite(maxSats) && rateSats > maxSats) continue; + items.push({ id: docId, rateSats, contentHash, network: 'bitcoin' }); + } + if (!items.length) return false; + return this._sendLocalInventoryDocumentsWireResponse(origin.name, items); + } + + const reqKind = String(req.kind || '').trim().toLowerCase(); + if (reqKind !== 'documents') return false; + const items = this._collectDocumentCatalogInventoryItems({}); + if (!items.length) return false; + return this._sendLocalInventoryDocumentsWireResponse(origin.name, items); + } + /** * Send a locally stored document to a connected peer as `P2P_FILE_SEND`. * @param {string} documentId