@openreceive/node 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,8 @@
2
2
  import {
3
3
  isErrorCode,
4
4
  isRetryableErrorCode,
5
- OpenReceiveError
5
+ OpenReceiveError,
6
+ publicErrorBody
6
7
  } from "@openreceive/core";
7
8
  var WalletPreflightError = class extends Error {
8
9
  code;
@@ -69,7 +70,8 @@ var OPENRECEIVE_ERROR_MESSAGES = {
69
70
  CONFLICT: "NWC wallet service reported a conflicting request."
70
71
  };
71
72
  function normalizeNwcWalletError(error) {
72
- if (error instanceof OpenReceiveError) return error;
73
+ if (error instanceof OpenReceiveError)
74
+ return new OpenReceiveError(publicErrorBody(error.toJSON()));
73
75
  const records = collectErrorRecords(error);
74
76
  const code = knownErrorCode(error) ?? errorCodeFromRecords(records) ?? (typeof error === "string" ? normalizeNwcErrorCode(error) : void 0) ?? "OTHER";
75
77
  const message = errorMessageFromRecords(records, error, code);
@@ -83,7 +85,7 @@ function normalizeNwcWalletError(error) {
83
85
  ...requestId === void 0 ? {} : { request_id: requestId },
84
86
  ...details === void 0 ? {} : { details }
85
87
  };
86
- return new OpenReceiveError(body, { cause: error });
88
+ return new OpenReceiveError(publicErrorBody(body));
87
89
  }
88
90
  function knownErrorCode(error) {
89
91
  if (error instanceof ReceiveCheckoutValidationError) {
@@ -309,7 +311,7 @@ function normalizeMakeInvoiceResult(rawResult) {
309
311
  const expiresAt = parseOptionalInteger(result.expires_at ?? result.expiresAt, "expires_at");
310
312
  return {
311
313
  invoice,
312
- payment_hash: paymentHash,
314
+ payment_hash: paymentHash.toLowerCase(),
313
315
  amount_msats: amountMsats,
314
316
  ...createdAt === void 0 ? {} : { created_at: createdAt },
315
317
  ...expiresAt === void 0 ? {} : { expires_at: expiresAt }
@@ -331,6 +333,10 @@ function normalizeListTransactionsResult(rawResult) {
331
333
  const transactions = [];
332
334
  let skippedRows = 0;
333
335
  for (const rawTransaction of rawTransactions) {
336
+ if (typeof rawTransaction !== "object" || rawTransaction === null || Array.isArray(rawTransaction)) {
337
+ skippedRows += 1;
338
+ continue;
339
+ }
334
340
  try {
335
341
  transactions.push(normalizeNwcTransaction(rawTransaction));
336
342
  } catch {
@@ -354,7 +360,7 @@ function normalizeNwcTransaction(rawTransaction) {
354
360
  if (!/^[0-9a-fA-F]{64}$/.test(paymentHash)) {
355
361
  throw new TypeError("payment_hash must be 64 hexadecimal characters");
356
362
  }
357
- normalized.payment_hash = paymentHash;
363
+ normalized.payment_hash = paymentHash.toLowerCase();
358
364
  }
359
365
  if (result.amount_msats !== void 0 || result.amount !== void 0) {
360
366
  normalized.amount_msats = toBigInt(result.amount_msats ?? result.amount, "amount_msats");
@@ -394,7 +400,7 @@ function normalizeNwcNotification(rawNotification) {
394
400
  }
395
401
  }
396
402
  const rawHash = payload.payment_hash ?? payload.paymentHash ?? record.payment_hash ?? record.paymentHash;
397
- const paymentHash = typeof rawHash === "string" && rawHash.length > 0 ? rawHash : transaction?.payment_hash;
403
+ const paymentHash = typeof rawHash === "string" && rawHash.length > 0 ? rawHash.toLowerCase() : transaction?.payment_hash;
398
404
  return {
399
405
  type,
400
406
  ...paymentHash === void 0 ? {} : { payment_hash: paymentHash },
@@ -485,15 +491,85 @@ function unwrapNwcResult(value) {
485
491
  import { createRequire } from "module";
486
492
  import { pathToFileURL } from "url";
487
493
  import { recordOrEmpty as recordOrEmpty2 } from "@openreceive/core";
494
+
495
+ // src/nwc/history-request.ts
496
+ async function historyRequest(client, params, signal) {
497
+ signal.throwIfAborted();
498
+ const content = await client.encrypt(
499
+ client.walletPubkey,
500
+ JSON.stringify({ method: "list_transactions", params })
501
+ );
502
+ signal.throwIfAborted();
503
+ const event = await client.signEvent({
504
+ kind: 23194,
505
+ created_at: Math.floor(Date.now() / 1e3),
506
+ tags: [
507
+ ["p", client.walletPubkey],
508
+ ["v", client.encryptionType === "nip44_v2" ? "1.0" : "0.0"],
509
+ ["encryption", client.encryptionType]
510
+ ],
511
+ content
512
+ });
513
+ signal.throwIfAborted();
514
+ return new Promise((resolve, reject) => {
515
+ const controller = new AbortController();
516
+ let done = false;
517
+ let subscription;
518
+ const finish = (error, result) => {
519
+ if (done) return;
520
+ done = true;
521
+ signal.removeEventListener("abort", abort);
522
+ controller.abort();
523
+ subscription?.close();
524
+ if (error === void 0) resolve(result);
525
+ else reject(error);
526
+ };
527
+ const abort = () => finish(signal.reason ?? new Error("Wallet scan cancelled."));
528
+ signal.addEventListener("abort", abort, { once: true });
529
+ if (signal.aborted) {
530
+ abort();
531
+ return;
532
+ }
533
+ try {
534
+ subscription = client.pool.subscribe(
535
+ client.relayUrls,
536
+ { kinds: [23195], authors: [client.walletPubkey], "#e": [event.id] },
537
+ {
538
+ abort: controller.signal,
539
+ onevent: async (reply) => {
540
+ if (done || reply.pubkey !== client.walletPubkey || !reply.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))
541
+ return;
542
+ try {
543
+ const body = JSON.parse(await client.decrypt(client.walletPubkey, reply.content));
544
+ if (done || signal.aborted) return;
545
+ if (body.error !== void 0 && body.error !== null) finish(body.error);
546
+ else finish(void 0, body.result);
547
+ } catch (error) {
548
+ finish(error);
549
+ }
550
+ }
551
+ }
552
+ );
553
+ if (done) {
554
+ subscription.close();
555
+ return;
556
+ }
557
+ void Promise.any(
558
+ client.pool.publish(client.relayUrls, event, { abort: controller.signal })
559
+ ).catch((error) => finish(error));
560
+ } catch (error) {
561
+ finish(error);
562
+ }
563
+ });
564
+ }
565
+
566
+ // src/nwc/transport.ts
488
567
  var require2 = createRequire(import.meta.url);
489
- async function callRequiredMethod(client, names, request) {
568
+ async function callRequiredMethod(client, names, request, options) {
490
569
  for (const name of names) {
491
570
  const method = client[name];
492
571
  if (typeof method === "function") {
493
- return await method.call(
494
- client,
495
- request
496
- );
572
+ return await method.call(client, request, options);
497
573
  }
498
574
  }
499
575
  throw new WalletPreflightError(
@@ -515,9 +591,10 @@ async function createDefaultAlbyNwcClient(connectionString) {
515
591
  );
516
592
  }
517
593
  const NWCClientConstructor = Constructor;
518
- return new NWCClientConstructor({
519
- nostrWalletConnectUrl: connectionString
520
- });
594
+ const client = new NWCClientConstructor({ nostrWalletConnectUrl: connectionString });
595
+ const receiveClient = client;
596
+ receiveClient.listTransactions = (request, options) => historyRequest(client, request, options?.signal ?? AbortSignal.timeout(1e4));
597
+ return receiveClient;
521
598
  }
522
599
  function ensureNodeWebSocket() {
523
600
  if (globalThis.WebSocket !== void 0) return;
@@ -555,7 +632,6 @@ var AlbyNwcReceiveClient = class {
555
632
  #clientFactory;
556
633
  #preflightSummary;
557
634
  #preflightPromise;
558
- #requirePreflight;
559
635
  #logger;
560
636
  #allowSpendCapableWallet;
561
637
  #spendCapabilityWarningDelayMs;
@@ -572,7 +648,6 @@ var AlbyNwcReceiveClient = class {
572
648
  };
573
649
  this.#client = options.client;
574
650
  this.#clientFactory = options.clientFactory;
575
- this.#requirePreflight = options.requirePreflight ?? true;
576
651
  this.#logger = options.logger;
577
652
  this.#allowSpendCapableWallet = options.allowSpendCapableWallet ?? false;
578
653
  this.#spendCapabilityWarningDelayMs = options.spendCapabilityWarningDelayMs ?? 0;
@@ -729,7 +804,7 @@ var AlbyNwcReceiveClient = class {
729
804
  });
730
805
  return result;
731
806
  }
732
- async listTransactions(request) {
807
+ async listTransactions(request, options) {
733
808
  await this.ensurePreflight();
734
809
  validateListTransactionsRequest(request);
735
810
  this.#log(
@@ -752,7 +827,8 @@ var AlbyNwcReceiveClient = class {
752
827
  rawResult = await callRequiredMethod(
753
828
  await this.getClient(),
754
829
  ["listTransactions", "list_transactions"],
755
- toNip47ListTransactionsParams(request)
830
+ toNip47ListTransactionsParams(request),
831
+ options
756
832
  );
757
833
  } catch (error) {
758
834
  const normalized = normalizeNwcWalletError(error);
@@ -909,7 +985,7 @@ var AlbyNwcReceiveClient = class {
909
985
  await client?.close?.();
910
986
  }
911
987
  async ensurePreflight() {
912
- if (!this.#requirePreflight || this.#preflightSummary !== void 0) return;
988
+ if (this.#preflightSummary !== void 0) return;
913
989
  this.#preflightPromise ??= this.preflight();
914
990
  try {
915
991
  await this.#preflightPromise;
@@ -1740,6 +1816,7 @@ function fixedFloatAvailabilityMessage(reason) {
1740
1816
 
1741
1817
  // src/swap/fixedfloat-transport.ts
1742
1818
  import { createHmac } from "crypto";
1819
+ import { isRecord } from "@openreceive/core";
1743
1820
  var FixedFloatApiError = class _FixedFloatApiError extends Error {
1744
1821
  path;
1745
1822
  kind;
@@ -1876,22 +1953,31 @@ var FixedFloatTransport = class {
1876
1953
  return parsed.data;
1877
1954
  }
1878
1955
  logApiRequest(path, body = {}) {
1879
- this.apiRequestLogger?.({
1880
- provider: this.provider,
1881
- path,
1882
- body
1883
- });
1956
+ try {
1957
+ this.apiRequestLogger?.({
1958
+ provider: this.provider,
1959
+ path,
1960
+ has_body: Object.keys(body).length > 0,
1961
+ has_token: body.token != null
1962
+ });
1963
+ } catch {
1964
+ }
1884
1965
  }
1885
1966
  logApiResponse(input) {
1886
- this.apiResponseLogger?.({
1887
- provider: this.provider,
1888
- path: input.path,
1889
- status: input.status,
1890
- ok: input.ok,
1891
- code: input.code,
1892
- msg: input.msg,
1893
- data: input.data
1894
- });
1967
+ const pairCount = isRecord(input.data) ? input.data.pair_count : void 0;
1968
+ try {
1969
+ this.apiResponseLogger?.({
1970
+ provider: this.provider,
1971
+ path: input.path,
1972
+ status: input.status,
1973
+ ok: input.ok,
1974
+ code: typeof input.code === "number" ? input.code : void 0,
1975
+ has_data: input.data != null,
1976
+ items: Array.isArray(input.data) ? input.data.length : void 0,
1977
+ pair_count: typeof pairCount === "number" ? pairCount : void 0
1978
+ });
1979
+ } catch {
1980
+ }
1895
1981
  }
1896
1982
  };
1897
1983
  function formatFixedFloatApiErrorMessage(path, status, msg) {
@@ -2380,7 +2466,8 @@ function providerIdFromUri(uri) {
2380
2466
  }
2381
2467
 
2382
2468
  // src/service/logging.ts
2383
- import { compact as compact3, isRecord } from "@openreceive/core";
2469
+ import { isSensitiveLogKey, sanitizeLogValue } from "@openreceive/core";
2470
+ import { isSensitiveLogKey as isSensitiveLogKey2, redactSecrets } from "@openreceive/core";
2384
2471
  function emitLog(options, level, event, message, fields = {}) {
2385
2472
  emitEvent(options, {
2386
2473
  level,
@@ -2426,113 +2513,25 @@ function sanitizeEvent(entry) {
2426
2513
  }
2427
2514
  return clean;
2428
2515
  }
2429
- function sanitizeLogValue(value) {
2430
- if (typeof value === "string") return redactSecrets(value);
2431
- if (Array.isArray(value)) return value.map(sanitizeLogValue);
2432
- if (typeof value !== "object" || value === null) return value;
2433
- const clean = {};
2434
- for (const [key, nested] of Object.entries(value)) {
2435
- if (isSensitiveLogKey(key)) {
2436
- clean[key] = "[REDACTED]";
2437
- } else {
2438
- clean[key] = sanitizeLogValue(nested);
2439
- }
2440
- }
2441
- return clean;
2442
- }
2443
- function isSensitiveLogKey(key) {
2444
- if (/_present$/i.test(key)) return false;
2445
- return /secret|token|authorization|cookie|nwc|dsn|preimage|invoice|bolt11|swap_?data|(?:private|api)[_-]?key|^key$|api[_-]?sign/i.test(
2446
- key
2447
- );
2448
- }
2449
- function redactSecrets(value) {
2450
- return value.replace(/nostr\+walletconnect:[^\s"'`<>]+/g, "[REDACTED_NWC]").replace(/lightning\+swapconnect:[^\s"'`<>]+/g, "[REDACTED_LSC]").replace(/([?&](?:token|secret|key)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
2451
- }
2452
2516
  function summarizeSwapProviderApiRequest(entry) {
2453
- const body = isRecord(entry.body) ? entry.body : void 0;
2454
- return compact3({
2517
+ return {
2455
2518
  provider: entry.provider,
2456
2519
  path: entry.path,
2457
- reference: optionalLogString(body?.id),
2458
- choice: optionalLogString(body?.choice),
2459
- from_ccy: optionalLogString(body?.fromCcy),
2460
- to_ccy: optionalLogString(body?.toCcy),
2461
- amount: optionalLogString(body?.amount) ?? optionalLogNumber(body?.amount)
2462
- });
2520
+ has_body: entry.has_body,
2521
+ has_token: entry.has_token
2522
+ };
2463
2523
  }
2464
2524
  function summarizeSwapProviderApiResponse(entry) {
2465
- const summary = {
2525
+ return {
2466
2526
  provider: entry.provider,
2467
2527
  path: entry.path,
2468
2528
  status: entry.status,
2469
- ok: entry.ok
2529
+ ok: entry.ok,
2530
+ code: entry.code,
2531
+ has_data: entry.has_data,
2532
+ items: entry.items,
2533
+ pair_count: entry.pair_count
2470
2534
  };
2471
- if (entry.code !== void 0 && entry.code !== null) summary.code = entry.code;
2472
- const msg = optionalLogString(entry.msg);
2473
- if (msg !== void 0 && msg !== "OK") summary.msg = msg;
2474
- const data = entry.data;
2475
- if (Array.isArray(data)) {
2476
- summary.items = data.length;
2477
- return summary;
2478
- }
2479
- if (!isRecord(data)) return summary;
2480
- const pairCount = optionalLogNumber(data.pair_count);
2481
- if (pairCount !== void 0) {
2482
- summary.pair_count = pairCount;
2483
- return summary;
2484
- }
2485
- const reference = optionalLogString(data.id);
2486
- const orderStatus = optionalLogString(data.status);
2487
- if (reference !== void 0) summary.reference = reference;
2488
- if (orderStatus !== void 0) summary.order_status = orderStatus;
2489
- const from = summarizeSwapProviderSide(data.from);
2490
- const to = summarizeSwapProviderSide(data.to);
2491
- if (from !== void 0) summary.from = from;
2492
- if (to !== void 0) summary.to = to;
2493
- if (isRecord(data.time)) {
2494
- const left = optionalLogNumber(data.time.left);
2495
- if (left !== void 0) summary.left = left;
2496
- }
2497
- if (isRecord(data.emergency)) {
2498
- const choice = optionalLogString(data.emergency.choice);
2499
- if (choice !== void 0 && choice !== "NONE") summary.emergency = choice;
2500
- const statuses = Array.isArray(data.emergency.status) ? data.emergency.status.filter((item) => typeof item === "string" && item.length > 0).map((item) => item.toUpperCase()) : [];
2501
- if (statuses.length > 0) summary.emergency_status = statuses.join(",");
2502
- const repeat = data.emergency.repeat;
2503
- if (repeat === true || repeat === "1" || repeat === 1) summary.emergency_repeat = true;
2504
- }
2505
- if (isRecord(data.from) && isRecord(data.from.tx)) {
2506
- const received = optionalLogString(data.from.tx.amount);
2507
- if (received !== void 0) summary.deposit_received = received;
2508
- }
2509
- if (isRecord(data.back)) {
2510
- const refundAmount = optionalLogString(data.back.amount);
2511
- if (refundAmount !== void 0) summary.refund_amount = refundAmount;
2512
- }
2513
- if (reference === void 0) {
2514
- const fromRecord = isRecord(data.from) ? data.from : void 0;
2515
- const toRecord = isRecord(data.to) ? data.to : void 0;
2516
- const fromAmount = optionalLogString(fromRecord?.amount) ?? optionalLogString(data.fromAmount);
2517
- const toAmount = optionalLogString(toRecord?.amount) ?? optionalLogString(data.toAmount);
2518
- if (fromAmount !== void 0) summary.from_amount = fromAmount;
2519
- if (toAmount !== void 0) summary.to_amount = toAmount;
2520
- }
2521
- return summary;
2522
- }
2523
- function summarizeSwapProviderSide(side) {
2524
- if (!isRecord(side)) return void 0;
2525
- const code = optionalLogString(side.code) ?? optionalLogString(side.coin);
2526
- const amount = optionalLogString(side.amount);
2527
- if (code === void 0 && amount === void 0) return void 0;
2528
- if (code !== void 0 && amount !== void 0) return `${code} ${amount}`;
2529
- return code ?? amount;
2530
- }
2531
- function optionalLogString(value) {
2532
- return typeof value === "string" && value.length > 0 ? value : void 0;
2533
- }
2534
- function optionalLogNumber(value) {
2535
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2536
2535
  }
2537
2536
 
2538
2537
  export {
@@ -2555,7 +2554,7 @@ export {
2555
2554
  createNwcEndpointLogger,
2556
2555
  summarizeReconcilePass,
2557
2556
  sanitizeEvent,
2558
- redactSecrets,
2559
2557
  summarizeSwapProviderApiRequest,
2560
- summarizeSwapProviderApiResponse
2558
+ summarizeSwapProviderApiResponse,
2559
+ redactSecrets
2561
2560
  };
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  createNwcReceiveClient,
3
3
  readLscConnectionsFromEnvironment,
4
4
  redactSecrets
5
- } from "./chunk-5LIZZ7XG.js";
5
+ } from "./chunk-7WTXLEDH.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { existsSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { WalletCapabilitySummary, ReceiveNwcClient, NwcTransaction, ParsedNwcConnection, RedactedNwcConnection, MakeInvoiceRequest, MakeInvoiceResult, ListTransactionsRequest, ListTransactionsResult, ErrorCode, ErrorBody, RateQuote, SourcedPriceProvider, SimplePriceFetch, PaidPayment, PaymentCheck, BtcFiatRateMapWithSource, MoneyAmount, CachedPriceFeed } from '@openreceive/core';
1
+ import { WalletCapabilitySummary, ReceiveNwcClient, NwcTransaction, ParsedNwcConnection, RedactedNwcConnection, MakeInvoiceRequest, MakeInvoiceResult, ListTransactionsRequest, ListTransactionsResult, ErrorCode, ErrorBody, RateQuote, SourcedPriceProvider, SimplePriceFetch, PaidPayment, PaymentCheck, PaymentScanWindow, PaymentScanSlice, BtcFiatRateMapWithSource, MoneyAmount, CachedPriceFeed } from '@openreceive/core';
2
2
  export { ErrorBody, ErrorCode, NwcTransaction, OpenReceiveError, PaidPayment, PaymentCheck, RateQuote, ReceiveNwcClient } from '@openreceive/core';
3
3
 
4
4
  /**
@@ -12,8 +12,12 @@ interface AlbyNwcCompatibleClient {
12
12
  getWalletServiceInfo?: () => Promise<unknown>;
13
13
  makeInvoice?: (request: Record<string, unknown>) => Promise<unknown>;
14
14
  make_invoice?: (request: Record<string, unknown>) => Promise<unknown>;
15
- listTransactions?: (request: Record<string, unknown>) => Promise<unknown>;
16
- list_transactions?: (request: Record<string, unknown>) => Promise<unknown>;
15
+ listTransactions?: (request: Record<string, unknown>, options?: {
16
+ readonly signal?: AbortSignal;
17
+ }) => Promise<unknown>;
18
+ list_transactions?: (request: Record<string, unknown>, options?: {
19
+ readonly signal?: AbortSignal;
20
+ }) => Promise<unknown>;
17
21
  subscribeNotifications?: (callback: (notification: unknown) => void, notificationTypes?: string[]) => unknown;
18
22
  close?: () => Promise<void> | void;
19
23
  }
@@ -97,7 +101,6 @@ interface AlbyNwcReceiveClientOptions {
97
101
  connectionString: string;
98
102
  client?: AlbyNwcCompatibleClient;
99
103
  clientFactory?: AlbyNwcClientFactory;
100
- requirePreflight?: boolean;
101
104
  logger?: NwcEndpointLogger;
102
105
  /**
103
106
  * Explicit override: boot even when the connection advertises spend methods
@@ -128,7 +131,9 @@ declare class AlbyNwcReceiveClient implements ReceiveNwcClient {
128
131
  constructor(options: AlbyNwcReceiveClientOptions);
129
132
  preflight(): Promise<WalletCapabilitySummary>;
130
133
  makeInvoice(request: MakeInvoiceRequest): Promise<MakeInvoiceResult>;
131
- listTransactions(request: ListTransactionsRequest): Promise<ListTransactionsResult>;
134
+ listTransactions(request: ListTransactionsRequest, options?: {
135
+ readonly signal?: AbortSignal;
136
+ }): Promise<ListTransactionsResult>;
132
137
  /**
133
138
  * Opt-in NWC-02 notification subscription, limited to `payment_received`.
134
139
  * Notifications are authenticated wallet data: the handler receives the
@@ -279,32 +284,23 @@ interface SwapOrder {
279
284
  readonly fee?: SwapFee;
280
285
  readonly raw?: unknown;
281
286
  }
282
- /**
283
- * A single raw provider API response, surfaced for server-side observability.
284
- * Carries the HTTP status and the parsed `{code, msg, data}` envelope. Emitted
285
- * through the service's sanitizing log sink, so any nested secret (e.g. a
286
- * FixedFloat order token) is redacted before it reaches a log line.
287
- */
287
+ /** Allowlisted metadata, sanitized before invoking any diagnostic sink. */
288
288
  interface SwapProviderApiResponseLog {
289
289
  readonly provider: string;
290
290
  readonly path: string;
291
291
  readonly status: number;
292
292
  readonly ok: boolean;
293
- readonly code: unknown;
294
- readonly msg: unknown;
295
- readonly data: unknown;
293
+ readonly code?: number;
294
+ readonly has_data: boolean;
295
+ readonly items?: number;
296
+ readonly pair_count?: number;
296
297
  }
297
- /**
298
- * A single outbound provider API request, surfaced for server-side observability
299
- * alongside {@link SwapProviderApiResponseLog}. Carries the request path and body.
300
- * Emitted through the service's sanitizing log sink, so any secret in the body
301
- * (e.g. a FixedFloat order token on status/refund calls) is redacted; provider
302
- * auth headers are never included here.
303
- */
298
+ /** Request bodies, credentials, invoices and addresses never enter this hook. */
304
299
  interface SwapProviderApiRequestLog {
305
300
  readonly provider: string;
306
301
  readonly path: string;
307
- readonly body: unknown;
302
+ readonly has_body: boolean;
303
+ readonly has_token: boolean;
308
304
  }
309
305
  interface SwapProvider {
310
306
  readonly name: string;
@@ -314,15 +310,13 @@ interface SwapProvider {
314
310
  attachSwapCache?(cache: TransientSwapCache): void;
315
311
  /**
316
312
  * Attach a sink for outbound provider API requests, mirroring
317
- * {@link attachApiResponseLogger}. The
318
- * service routes entries through its sanitizing log sink, so secrets in the body
319
- * are redacted. Providers that make no remote calls may omit this.
313
+ * {@link attachApiResponseLogger}. Emit only the allowlisted metadata;
314
+ * diagnostics must not affect payment behavior. Providers without remote calls may omit this.
320
315
  */
321
316
  attachApiRequestLogger?(log: (entry: SwapProviderApiRequestLog) => void): void;
322
317
  /**
323
- * Attach a sink for raw provider API responses. The service routes entries through
324
- * its sanitizing log sink, so nested secrets are redacted. Providers that make no
325
- * remote calls may omit this.
318
+ * Attach a sink for allowlisted response metadata, never raw envelopes.
319
+ * Providers without remote calls may omit this.
326
320
  */
327
321
  attachApiResponseLogger?(log: (entry: SwapProviderApiResponseLog) => void): void;
328
322
  /**
@@ -547,6 +541,8 @@ interface Checkout {
547
541
  readonly bolt11: string;
548
542
  readonly amountMsats: number;
549
543
  readonly createdAt: number;
544
+ /** Missing on legacy snapshots; only wallet timestamps narrow history scans. */
545
+ readonly createdAtSource?: "wallet" | "host";
550
546
  readonly expiresAt: number;
551
547
  readonly fiatQuote: RateQuote | null;
552
548
  }
@@ -705,6 +701,14 @@ interface OpenReceive {
705
701
  }>;
706
702
  createCheckout(input: CreateCheckoutRequest): Promise<Checkout>;
707
703
  reconcilePayments(input: ReconcilePaymentsRequest): Promise<readonly PaymentCheck[]>;
704
+ /** Bounded durable history slice used by repository-backed HTTP and workers. */
705
+ scanPaymentSlice(input: {
706
+ window: PaymentScanWindow;
707
+ maxPages?: number;
708
+ deadline?: number;
709
+ signal?: AbortSignal;
710
+ onFinality?: (check: PaymentCheck) => Promise<void>;
711
+ }): Promise<PaymentScanSlice>;
708
712
  /**
709
713
  * Opt-in NWC-02 notifications: subscribe to wallet `payment_received`
710
714
  * notifications. Notifications are authenticated wallet data — a payload
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  summarizeReconcilePass,
20
20
  summarizeSwapProviderApiRequest,
21
21
  summarizeSwapProviderApiResponse
22
- } from "./chunk-5LIZZ7XG.js";
22
+ } from "./chunk-7WTXLEDH.js";
23
23
 
24
24
  // src/index.ts
25
25
  import { OpenReceiveError as OpenReceiveError2 } from "@openreceive/core";
@@ -33,6 +33,7 @@ import {
33
33
  OpenReceiveError,
34
34
  parseNwcUri,
35
35
  reconcilePaymentAttempts,
36
+ scanPaymentSlice,
36
37
  unixSeconds
37
38
  } from "@openreceive/core";
38
39
 
@@ -765,6 +766,7 @@ async function createCheckout(context, request) {
765
766
  bolt11: walletInvoice.invoice,
766
767
  amountMsats: toSafeInteger(walletInvoice.amount_msats, "amount_msats"),
767
768
  createdAt,
769
+ createdAtSource: walletInvoice.created_at === void 0 ? "host" : "wallet",
768
770
  expiresAt,
769
771
  fiatQuote: resolved.fiatQuote
770
772
  };
@@ -1184,11 +1186,14 @@ function parsePayInAsset(value) {
1184
1186
  return value;
1185
1187
  }
1186
1188
  function parseRefundAddress(value, payInAsset) {
1189
+ if (!isSwapPayInAsset(payInAsset)) {
1190
+ throw serviceError(503, "INTERNAL", "Swap recovery requires a supported pay-in asset/network.");
1191
+ }
1187
1192
  const normalized = value.trim();
1188
1193
  if (normalized.length === 0 || normalized.length > 300) {
1189
1194
  throw serviceError(400, "INVALID_REQUEST", "refundAddress is invalid.");
1190
1195
  }
1191
- if (typeof payInAsset === "string" && !isValidSwapAddressForPayInAsset(payInAsset, normalized)) {
1196
+ if (!isValidSwapAddressForPayInAsset(payInAsset, normalized)) {
1192
1197
  throw serviceError(
1193
1198
  400,
1194
1199
  "INVALID_REQUEST",
@@ -1347,6 +1352,7 @@ async function createOpenReceive(supplied = {}) {
1347
1352
  };
1348
1353
  const service = {
1349
1354
  priceCurrencies,
1355
+ scanPaymentSlice: (input) => scanPaymentSlice({ ...input, client, clock }),
1350
1356
  prepareCheckout: (input) => prepareCheckout(context, input),
1351
1357
  createCheckout: (input) => createCheckout(context, input),
1352
1358
  reconcilePayments: async (input) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openreceive/node",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Accept Bitcoin Lightning payments in Node.js with your own wallet and optional USDT, USDC, SOL and ETH swaps.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -17,7 +17,7 @@
17
17
  "types": "./dist/index.d.ts",
18
18
  "dependencies": {
19
19
  "@getalby/sdk": "^8.0.3",
20
- "@openreceive/core": "0.4.10"
20
+ "@openreceive/core": "0.4.12"
21
21
  },
22
22
  "bin": {
23
23
  "openreceive": "./bin/openreceive.mjs"
@@ -34,9 +34,10 @@ code** (`NWC_URI`).
34
34
  - BTCPay Server: [references/btcpay.md](references/btcpay.md) — a plugin,
35
35
  configured in BTCPay's store UI or Greenfield API; no application code,
36
36
  no npm packages, no gem. The rest of this file is about the library.
37
- 3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
38
- environment before writing code. Never print the value; never invent a
39
- placeholder.
37
+ 3. Follow its **Step 0** first: before writing code or searching the machine,
38
+ ask the user for the receive-only NWC code (then the swap URI), one question
39
+ per message, and store each pasted code in the project's env file yourself.
40
+ Never print the value; never invent a placeholder.
40
41
 
41
42
  Install, per adapter — Express: `npm install @openreceive/express @openreceive/react`;
42
43
  Fastify: `npm install @openreceive/fastify @openreceive/react`; Next.js: