@breeztech/breez-sdk-spark 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/breez-sdk-spark.tgz +0 -0
  2. package/bundler/breez_sdk_spark_wasm.d.ts +604 -237
  3. package/bundler/breez_sdk_spark_wasm.js +1 -1
  4. package/bundler/breez_sdk_spark_wasm_bg.js +729 -434
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  7. package/bundler/storage/index.js +356 -34
  8. package/deno/breez_sdk_spark_wasm.d.ts +604 -237
  9. package/deno/breez_sdk_spark_wasm.js +729 -434
  10. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  11. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  12. package/nodejs/breez_sdk_spark_wasm.d.ts +604 -237
  13. package/nodejs/breez_sdk_spark_wasm.js +741 -441
  14. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  15. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  16. package/nodejs/index.js +10 -10
  17. package/nodejs/index.mjs +13 -8
  18. package/nodejs/mysql-session-store/errors.cjs +13 -0
  19. package/nodejs/{mysql-session-manager → mysql-session-store}/index.cjs +24 -21
  20. package/nodejs/{mysql-session-manager → mysql-session-store}/migrations.cjs +17 -11
  21. package/nodejs/mysql-session-store/package.json +9 -0
  22. package/nodejs/mysql-storage/index.cjs +358 -125
  23. package/nodejs/mysql-storage/migrations.cjs +67 -2
  24. package/nodejs/mysql-token-store/index.cjs +99 -79
  25. package/nodejs/mysql-token-store/migrations.cjs +59 -2
  26. package/nodejs/mysql-tree-store/index.cjs +15 -9
  27. package/nodejs/mysql-tree-store/migrations.cjs +16 -2
  28. package/nodejs/package.json +2 -2
  29. package/nodejs/postgres-session-store/errors.cjs +13 -0
  30. package/nodejs/{postgres-session-manager → postgres-session-store}/index.cjs +23 -23
  31. package/nodejs/{postgres-session-manager → postgres-session-store}/migrations.cjs +14 -14
  32. package/nodejs/postgres-session-store/package.json +9 -0
  33. package/nodejs/postgres-storage/index.cjs +296 -119
  34. package/nodejs/postgres-storage/migrations.cjs +51 -0
  35. package/nodejs/postgres-token-store/index.cjs +89 -64
  36. package/nodejs/postgres-token-store/migrations.cjs +44 -0
  37. package/nodejs/storage/index.cjs +285 -125
  38. package/nodejs/storage/migrations.cjs +47 -0
  39. package/package.json +6 -1
  40. package/ssr/index.js +57 -28
  41. package/web/breez_sdk_spark_wasm.d.ts +667 -286
  42. package/web/breez_sdk_spark_wasm.js +729 -434
  43. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  44. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +63 -49
  45. package/web/passkey-prf-provider/index.d.ts +203 -0
  46. package/web/passkey-prf-provider/index.js +733 -0
  47. package/web/storage/index.js +356 -34
  48. package/nodejs/mysql-session-manager/errors.cjs +0 -13
  49. package/nodejs/mysql-session-manager/package.json +0 -9
  50. package/nodejs/postgres-session-manager/errors.cjs +0 -13
  51. package/nodejs/postgres-session-manager/package.json +0 -9
@@ -457,6 +457,135 @@ class MigrationManager {
457
457
  }
458
458
  }
459
459
  },
460
+ {
461
+ // Recovery migration for the missing "contacts" object store.
462
+ //
463
+ // The original "Create contacts store" migration above was INSERTED into
464
+ // the middle of this array (at index 12) instead of being appended, in
465
+ // SDK 0.11.0. That index was already occupied by the "Clear cached
466
+ // lightning address for LnurlInfo schema change" migration, which had
467
+ // shipped one release earlier in SDK 0.10.0 (dbVersion 13).
468
+ //
469
+ // Migration array indices map 1:1 to DB version transitions, so any
470
+ // database that ran 0.10.0 reached version 13 and, on upgrading to
471
+ // 0.11.0+ (dbVersion 14), ran only migrations[13] — permanently skipping
472
+ // the newly-inserted migrations[12]. Those databases never got a
473
+ // "contacts" object store, so every contact operation fails with
474
+ // NotFoundError.
475
+ //
476
+ // This migration is correctly appended and idempotently (re)creates the
477
+ // store, so affected databases recover on their next upgrade while
478
+ // unaffected databases treat it as a no-op. Keep the original migration
479
+ // in place; do not delete or reorder it.
480
+ name: "Create contacts store (recovery for skipped migration)",
481
+ upgrade: (db) => {
482
+ if (!db.objectStoreNames.contains("contacts")) {
483
+ const contactsStore = db.createObjectStore("contacts", { keyPath: "id" });
484
+ contactsStore.createIndex("name_identifier", ["name", "paymentIdentifier"], { unique: false });
485
+ contactsStore.createIndex("name", "name", { unique: false });
486
+ }
487
+ }
488
+ },
489
+ {
490
+ // Add deposit_vout to distinguish deposits sharing a funding tx. The
491
+ // new field is carried inside the JSON `details` blob. We can't safely
492
+ // backfill vout on the existing blobs: we never stored the original SSP
493
+ // output_index, and vout=0 is a valid output index, so defaulting would
494
+ // silently mislabel. Instead we clear `details` on legacy deposit blobs
495
+ // so the read path returns `details: None` (matches what the SQL
496
+ // backends do by leaving the payments row but having no matching
497
+ // payment_details_deposit row). Reset the bitcoin sync offset so the
498
+ // resync re-fetches the SSP user_request and the upsert rewrites the
499
+ // blob with the proper vout.
500
+ name: "Clear legacy deposit details and reset sync offset for vout",
501
+ upgrade: (db, transaction) => {
502
+ if (db.objectStoreNames.contains("payments")) {
503
+ const paymentStore = transaction.objectStore("payments");
504
+ const cursorRequest = paymentStore.openCursor();
505
+ cursorRequest.onsuccess = (event) => {
506
+ const cursor = event.target.result;
507
+ if (!cursor) return;
508
+ const payment = cursor.value;
509
+ let details = null;
510
+ if (payment.details && typeof payment.details === "string") {
511
+ try {
512
+ details = JSON.parse(payment.details);
513
+ } catch (e) {
514
+ details = null;
515
+ }
516
+ } else {
517
+ details = payment.details;
518
+ }
519
+ if (details && details.type === "deposit") {
520
+ payment.details = null;
521
+ cursor.update(payment);
522
+ }
523
+ cursor.continue();
524
+ };
525
+ }
526
+ if (db.objectStoreNames.contains("settings")) {
527
+ const settingsStore = transaction.objectStore("settings");
528
+ const getRequest = settingsStore.get("sync_offset");
529
+
530
+ getRequest.onsuccess = () => {
531
+ const syncCache = getRequest.result;
532
+ if (syncCache && syncCache.value) {
533
+ try {
534
+ const syncInfo = JSON.parse(syncCache.value);
535
+ syncInfo.offset = 0;
536
+ settingsStore.put({
537
+ key: "sync_offset",
538
+ value: JSON.stringify(syncInfo),
539
+ });
540
+ } catch (e) {
541
+ // If parsing fails, just continue
542
+ }
543
+ }
544
+ };
545
+ }
546
+ },
547
+ },
548
+ {
549
+ name: "Backfill conversion_info type discriminator for serde tagged enum",
550
+ upgrade: (db, transaction) => {
551
+ if (db.objectStoreNames.contains("payment_metadata")) {
552
+ const store = transaction.objectStore("payment_metadata");
553
+ const request = store.openCursor();
554
+ request.onsuccess = (event) => {
555
+ const cursor = event.target.result;
556
+ if (cursor) {
557
+ const record = cursor.value;
558
+ if (record.conversionInfo) {
559
+ try {
560
+ const ci = typeof record.conversionInfo === "string"
561
+ ? JSON.parse(record.conversionInfo)
562
+ : record.conversionInfo;
563
+ if (!ci.type) {
564
+ ci.type = "amm";
565
+ record.conversionInfo = JSON.stringify(ci);
566
+ cursor.update(record);
567
+ }
568
+ } catch (e) {
569
+ // Skip unparseable records
570
+ }
571
+ }
572
+ cursor.continue();
573
+ }
574
+ };
575
+ }
576
+ },
577
+ },
578
+ {
579
+ // listActiveCrossChainSwaps scans and filters.
580
+ name: "Create cross_chain_swaps store",
581
+ upgrade: (db) => {
582
+ if (!db.objectStoreNames.contains("cross_chain_swaps")) {
583
+ db.createObjectStore("cross_chain_swaps", {
584
+ keyPath: ["provider", "id"],
585
+ });
586
+ }
587
+ },
588
+ },
460
589
  ];
461
590
  }
462
591
  }
@@ -480,7 +609,12 @@ class IndexedDBStorage {
480
609
  this.db = null;
481
610
  this.migrationManager = null;
482
611
  this.logger = logger;
483
- this.dbVersion = 15; // Current schema version
612
+ // IMPORTANT: the migrations array in MigrationManager is append-only. The
613
+ // migration at array index N is the upgrade step from DB version N to N+1,
614
+ // so existing databases depend on indices never shifting. Never insert,
615
+ // reorder, or delete a migration — only append. dbVersion MUST equal the
616
+ // number of migrations (enforced by the guard in initialize()).
617
+ this.dbVersion = 20; // Current schema version (= migration count)
484
618
  }
485
619
 
486
620
  /**
@@ -495,6 +629,17 @@ class IndexedDBStorage {
495
629
  throw new StorageError("IndexedDB is not available in this environment");
496
630
  }
497
631
 
632
+ // Guard: dbVersion must equal the migration count. If they drift apart, the
633
+ // upgrade loop either skips the trailing migration(s) or requests a version
634
+ // no migration fills in. Fail fast — this is a programming error.
635
+ const migrationCount = new MigrationManager(null, StorageError).migrations.length;
636
+ if (this.dbVersion !== migrationCount) {
637
+ throw new StorageError(
638
+ `dbVersion (${this.dbVersion}) must equal the migration count (${migrationCount}). ` +
639
+ `Migrations are append-only: append a new migration and bump dbVersion to match.`
640
+ );
641
+ }
642
+
498
643
  return new Promise((resolve, reject) => {
499
644
  const request = indexedDB.open(this.dbName, this.dbVersion);
500
645
 
@@ -802,31 +947,98 @@ class IndexedDBStorage {
802
947
  });
803
948
  }
804
949
 
805
- async insertPayment(payment) {
950
+ async applyPaymentUpdate(payment) {
806
951
  if (!this.db) {
807
952
  throw new StorageError("Database not initialized");
808
953
  }
809
954
 
810
955
  return new Promise((resolve, reject) => {
956
+ let shouldEmit = false;
957
+ let settled = false;
811
958
  const transaction = this.db.transaction("payments", "readwrite");
812
959
  const store = transaction.objectStore("payments");
813
960
 
814
- // Ensure details and method are serialized properly
815
- const paymentToStore = {
816
- ...payment,
817
- details: payment.details ? JSON.stringify(payment.details) : null,
818
- method: payment.method ? JSON.stringify(payment.method) : null,
961
+ const rejectOnce = (message, error) => {
962
+ if (settled) {
963
+ return;
964
+ }
965
+ settled = true;
966
+ reject(new StorageError(message, error));
819
967
  };
820
968
 
821
- const request = store.put(paymentToStore);
822
- request.onsuccess = () => resolve();
823
- request.onerror = () => {
824
- reject(
825
- new StorageError(
826
- `Failed to insert payment '${payment.id}': ${request.error?.message || "Unknown error"
969
+ transaction.oncomplete = () => {
970
+ if (settled) {
971
+ return;
972
+ }
973
+ settled = true;
974
+ resolve(shouldEmit);
975
+ };
976
+
977
+ transaction.onerror = () => {
978
+ rejectOnce(
979
+ `Failed to apply payment update '${payment.id}': ${transaction.error?.message || "Unknown error"
980
+ }`,
981
+ transaction.error
982
+ );
983
+ };
984
+
985
+ transaction.onabort = () => {
986
+ rejectOnce(
987
+ `Payment update transaction aborted for '${payment.id}': ${transaction.error?.message || "Unknown error"
988
+ }`,
989
+ transaction.error
990
+ );
991
+ };
992
+
993
+ const getRequest = store.get(payment.id);
994
+
995
+ getRequest.onsuccess = () => {
996
+ const storedPayment = getRequest.result;
997
+ const stored = storedPayment
998
+ ? this._normalizePaymentStatus(storedPayment.status)
999
+ : null;
1000
+ const next = this._normalizePaymentStatus(payment.status);
1001
+
1002
+ let shouldPersist;
1003
+ if (stored == null) {
1004
+ shouldPersist = true;
1005
+ shouldEmit = true;
1006
+ } else if (stored === next) {
1007
+ console.debug(
1008
+ `Skipping redundant payment event: id=${payment.id} status=${next}`
1009
+ );
1010
+ shouldPersist = true;
1011
+ shouldEmit = false;
1012
+ } else if (this._isFinalPaymentStatus(stored)) {
1013
+ console.warn(
1014
+ `Skipping payment update (would replace terminal status): id=${payment.id} stored=${stored} new=${next}`
1015
+ );
1016
+ shouldPersist = false;
1017
+ shouldEmit = false;
1018
+ } else {
1019
+ shouldPersist = true;
1020
+ shouldEmit = true;
1021
+ }
1022
+
1023
+ if (!shouldPersist) {
1024
+ return;
1025
+ }
1026
+
1027
+ const putRequest = store.put(this._paymentToStore(payment));
1028
+ putRequest.onerror = () => {
1029
+ rejectOnce(
1030
+ `Failed to persist payment update '${payment.id}': ${putRequest.error?.message || "Unknown error"
827
1031
  }`,
828
- request.error
829
- )
1032
+ putRequest.error
1033
+ );
1034
+ };
1035
+ };
1036
+
1037
+ getRequest.onerror = () => {
1038
+ rejectOnce(
1039
+ `Failed to read payment '${payment.id}' before update: ${getRequest.error?.message || "Unknown error"
1040
+ }`,
1041
+ getRequest.error
830
1042
  );
831
1043
  };
832
1044
  });
@@ -2019,8 +2231,98 @@ class IndexedDBStorage {
2019
2231
  });
2020
2232
  }
2021
2233
 
2234
+ // ===== Cross-Chain Swap Operations =====
2235
+
2236
+ async setCrossChainSwap(swap) {
2237
+ if (!this.db) {
2238
+ throw new StorageError("Database not initialized");
2239
+ }
2240
+
2241
+ return new Promise((resolve, reject) => {
2242
+ const transaction = this.db.transaction("cross_chain_swaps", "readwrite");
2243
+ const store = transaction.objectStore("cross_chain_swaps");
2244
+ // IndexedDB stores nested objects natively, so `data` is kept as-is.
2245
+ const request = store.put(swap);
2246
+ request.onsuccess = () => resolve();
2247
+ request.onerror = () => {
2248
+ reject(
2249
+ new StorageError(
2250
+ `Failed to set cross-chain swap '${swap.provider}:${swap.id}': ${request.error?.message || "Unknown error"}`,
2251
+ request.error
2252
+ )
2253
+ );
2254
+ };
2255
+ });
2256
+ }
2257
+
2258
+ async getCrossChainSwap(provider, id) {
2259
+ if (!this.db) {
2260
+ throw new StorageError("Database not initialized");
2261
+ }
2262
+
2263
+ return new Promise((resolve, reject) => {
2264
+ const transaction = this.db.transaction("cross_chain_swaps", "readonly");
2265
+ const store = transaction.objectStore("cross_chain_swaps");
2266
+ const request = store.get([provider, id]);
2267
+ request.onsuccess = () => resolve(request.result || null);
2268
+ request.onerror = () => {
2269
+ reject(
2270
+ new StorageError(
2271
+ `Failed to get cross-chain swap '${provider}:${id}': ${request.error?.message || "Unknown error"}`,
2272
+ request.error
2273
+ )
2274
+ );
2275
+ };
2276
+ });
2277
+ }
2278
+
2279
+ async listActiveCrossChainSwaps(provider) {
2280
+ if (!this.db) {
2281
+ throw new StorageError("Database not initialized");
2282
+ }
2283
+
2284
+ return new Promise((resolve, reject) => {
2285
+ const transaction = this.db.transaction("cross_chain_swaps", "readonly");
2286
+ const store = transaction.objectStore("cross_chain_swaps");
2287
+ const request = store.getAll();
2288
+ request.onsuccess = () => {
2289
+ const all = request.result || [];
2290
+ resolve(
2291
+ all.filter((swap) => swap.provider === provider && !swap.isTerminal)
2292
+ );
2293
+ };
2294
+ request.onerror = () => {
2295
+ reject(
2296
+ new StorageError(
2297
+ `Failed to list active cross-chain swaps for '${provider}': ${request.error?.message || "Unknown error"}`,
2298
+ request.error
2299
+ )
2300
+ );
2301
+ };
2302
+ });
2303
+ }
2304
+
2022
2305
  // ===== Private Helper Methods =====
2023
2306
 
2307
+ _paymentToStore(payment) {
2308
+ // Ensure details and method are serialized properly
2309
+ return {
2310
+ ...payment,
2311
+ details: payment.details ? JSON.stringify(payment.details) : null,
2312
+ method: payment.method ? JSON.stringify(payment.method) : null,
2313
+ };
2314
+ }
2315
+
2316
+ _normalizePaymentStatus(status) {
2317
+ return typeof status === "string" ? status.toLowerCase() : status;
2318
+ }
2319
+
2320
+ _isFinalPaymentStatus(status) {
2321
+ const normalized = this._normalizePaymentStatus(status);
2322
+ return normalized === "completed" || normalized === "failed";
2323
+ }
2324
+
2325
+
2024
2326
  _matchesFilters(payment, request) {
2025
2327
  // Filter by payment type
2026
2328
  if (request.typeFilter && request.typeFilter.length > 0) {
@@ -2075,6 +2377,10 @@ class IndexedDBStorage {
2075
2377
  // Filter by payment details. If any filter matches, we include the payment
2076
2378
  let paymentDetailsFilterMatches = false;
2077
2379
  for (const paymentDetailsFilter of request.paymentDetailsFilter) {
2380
+ // Base type check: the payment's details type must match the filter type
2381
+ if (details.type !== paymentDetailsFilter.type) {
2382
+ continue;
2383
+ }
2078
2384
  // Filter by HTLC status (Spark or Lightning)
2079
2385
  if (
2080
2386
  (paymentDetailsFilter.type === "spark" ||
@@ -2092,11 +2398,12 @@ class IndexedDBStorage {
2092
2398
  continue;
2093
2399
  }
2094
2400
  }
2095
- // Filter by token conversion info presence
2401
+ // Filter by conversion type + status
2096
2402
  if (
2097
2403
  (paymentDetailsFilter.type === "spark" ||
2098
- paymentDetailsFilter.type === "token") &&
2099
- paymentDetailsFilter.conversionRefundNeeded != null
2404
+ paymentDetailsFilter.type === "token" ||
2405
+ paymentDetailsFilter.type === "lightning") &&
2406
+ paymentDetailsFilter.conversionFilter != null
2100
2407
  ) {
2101
2408
  if (
2102
2409
  details.type !== paymentDetailsFilter.type ||
@@ -2105,11 +2412,20 @@ class IndexedDBStorage {
2105
2412
  continue;
2106
2413
  }
2107
2414
 
2108
- if (
2109
- paymentDetailsFilter.conversionRefundNeeded ===
2110
- (details.conversionInfo.status !== "refundNeeded")
2111
- ) {
2112
- continue;
2415
+ const ci = details.conversionInfo;
2416
+ if (paymentDetailsFilter.conversionFilter === "ammRefundNeeded") {
2417
+ if (ci.type !== "amm" || ci.status !== "refundNeeded") {
2418
+ continue;
2419
+ }
2420
+ } else if (paymentDetailsFilter.conversionFilter === "orchestraPending") {
2421
+ if (ci.type !== "orchestra" || ["completed", "failed", "refunded"].includes(ci.status)) {
2422
+ continue;
2423
+ }
2424
+ } else if (paymentDetailsFilter.conversionFilter === "boltzPending") {
2425
+ // Boltz conversion lives on the Lightning leg.
2426
+ if (ci.type !== "boltz" || ["completed", "failed", "refunded"].includes(ci.status)) {
2427
+ continue;
2428
+ }
2113
2429
  }
2114
2430
  }
2115
2431
  // Filter by token transaction hash
@@ -2250,17 +2566,23 @@ class IndexedDBStorage {
2250
2566
  );
2251
2567
  }
2252
2568
  }
2253
- } else if (details.type == "spark" || details.type == "token") {
2254
- // If conversionInfo exists, parse and add to details
2255
- if (metadata.conversionInfo) {
2256
- try {
2257
- details.conversionInfo = JSON.parse(metadata.conversionInfo);
2258
- } catch (e) {
2259
- throw new StorageError(
2260
- `Failed to parse conversionInfo JSON for payment ${payment.id}: ${e.message}`,
2261
- e
2262
- );
2263
- }
2569
+ }
2570
+
2571
+ // conversionInfo is surfaced on Lightning (Boltz hold-invoice pay),
2572
+ // Spark, and Token details — every variant that can carry a conversion.
2573
+ if (
2574
+ (details.type == "lightning" ||
2575
+ details.type == "spark" ||
2576
+ details.type == "token") &&
2577
+ metadata.conversionInfo
2578
+ ) {
2579
+ try {
2580
+ details.conversionInfo = JSON.parse(metadata.conversionInfo);
2581
+ } catch (e) {
2582
+ throw new StorageError(
2583
+ `Failed to parse conversionInfo JSON for payment ${payment.id}: ${e.message}`,
2584
+ e
2585
+ );
2264
2586
  }
2265
2587
  }
2266
2588
  }
@@ -1,13 +0,0 @@
1
- // errors.cjs - Session manager error wrapper with cause chain support
2
- class SessionManagerError extends Error {
3
- constructor(message, cause = null) {
4
- super(message);
5
- this.name = 'SessionManagerError';
6
- this.cause = cause;
7
- if (Error.captureStackTrace) {
8
- Error.captureStackTrace(this, SessionManagerError);
9
- }
10
- }
11
- }
12
-
13
- module.exports = { SessionManagerError };
@@ -1,9 +0,0 @@
1
- {
2
- "dependencies": {
3
- "mysql2": "^3.11.0"
4
- },
5
- "description": "Node.js MySQL session manager implementation for Breez SDK WASM (CommonJS)",
6
- "main": "index.cjs",
7
- "name": "@breez-sdk/mysql-session-manager",
8
- "version": "1.0.0"
9
- }
@@ -1,13 +0,0 @@
1
- // errors.cjs - Session manager error wrapper with cause chain support
2
- class SessionManagerError extends Error {
3
- constructor(message, cause = null) {
4
- super(message);
5
- this.name = 'SessionManagerError';
6
- this.cause = cause;
7
- if (Error.captureStackTrace) {
8
- Error.captureStackTrace(this, SessionManagerError);
9
- }
10
- }
11
- }
12
-
13
- module.exports = { SessionManagerError };
@@ -1,9 +0,0 @@
1
- {
2
- "dependencies": {
3
- "pg": "^8.18.0"
4
- },
5
- "description": "Node.js PostgreSQL session manager implementation for Breez SDK WASM (CommonJS)",
6
- "main": "index.cjs",
7
- "name": "@breez-sdk/postgres-session-manager",
8
- "version": "1.0.0"
9
- }