@breeztech/breez-sdk-spark 0.15.0 → 0.16.1-dev1

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 +511 -215
  3. package/bundler/breez_sdk_spark_wasm.js +1 -1
  4. package/bundler/breez_sdk_spark_wasm_bg.js +567 -414
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  7. package/bundler/storage/index.js +205 -15
  8. package/deno/breez_sdk_spark_wasm.d.ts +511 -215
  9. package/deno/breez_sdk_spark_wasm.js +567 -414
  10. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  11. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  12. package/nodejs/breez_sdk_spark_wasm.d.ts +511 -215
  13. package/nodejs/breez_sdk_spark_wasm.js +578 -421
  14. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  15. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  16. package/nodejs/index.js +10 -10
  17. package/nodejs/index.mjs +12 -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 +229 -111
  23. package/nodejs/mysql-storage/migrations.cjs +37 -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 +174 -107
  34. package/nodejs/postgres-storage/migrations.cjs +24 -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 +167 -113
  38. package/nodejs/storage/migrations.cjs +23 -0
  39. package/package.json +6 -1
  40. package/ssr/index.js +52 -28
  41. package/web/breez_sdk_spark_wasm.d.ts +566 -261
  42. package/web/breez_sdk_spark_wasm.js +567 -414
  43. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  44. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +55 -46
  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 +205 -15
  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
@@ -226,25 +226,27 @@ class MysqlTokenStore {
226
226
  );
227
227
 
228
228
  const [spentRows] = await conn.query(
229
- "SELECT output_id FROM brz_token_spent_outputs WHERE user_id = ? AND spent_at >= ?",
229
+ "SELECT prev_tx_hash, prev_tx_vout FROM brz_token_spent_outputs WHERE user_id = ? AND spent_at >= ?",
230
230
  [this.identity, refreshTimestamp]
231
231
  );
232
- const spentIds = new Set(spentRows.map((r) => r.output_id));
232
+ const spentOutpoints = new Set(
233
+ spentRows.map((r) => `${r.prev_tx_hash}:${r.prev_tx_vout}`)
234
+ );
233
235
 
234
236
  await conn.query(
235
237
  "DELETE FROM brz_token_outputs WHERE user_id = ? AND reservation_id IS NULL AND added_at < ?",
236
238
  [this.identity, refreshTimestamp]
237
239
  );
238
240
 
239
- const incomingOutputIds = new Set();
241
+ const incomingOutpoints = new Set();
240
242
  for (const to of tokenOutputs) {
241
243
  for (const o of to.outputs) {
242
- incomingOutputIds.add(o.output.id);
244
+ incomingOutpoints.add(`${o.prevTxHash}:${o.prevTxVout}`);
243
245
  }
244
246
  }
245
247
 
246
248
  const [reservedRows] = await conn.query(
247
- `SELECT r.id, o.id AS output_id
249
+ `SELECT r.id, o.prev_tx_hash, o.prev_tx_vout
248
250
  FROM brz_token_reservations r
249
251
  JOIN brz_token_outputs o
250
252
  ON o.reservation_id = r.id AND o.user_id = r.user_id
@@ -257,21 +259,23 @@ class MysqlTokenStore {
257
259
  if (!reservationOutputs.has(row.id)) {
258
260
  reservationOutputs.set(row.id, []);
259
261
  }
260
- reservationOutputs.get(row.id).push(row.output_id);
262
+ reservationOutputs.get(row.id).push([row.prev_tx_hash, row.prev_tx_vout]);
261
263
  }
262
264
 
263
265
  const reservationsToDelete = [];
264
- const outputsToRemoveFromReservation = [];
265
- for (const [reservationId, outputIds] of reservationOutputs) {
266
- const validIds = outputIds.filter((id) => incomingOutputIds.has(id));
267
- if (validIds.length === 0) {
268
- reservationsToDelete.push(reservationId);
269
- } else {
270
- for (const id of outputIds) {
271
- if (!incomingOutputIds.has(id)) {
272
- outputsToRemoveFromReservation.push(id);
266
+ const outpointsToRemoveFromReservation = [];
267
+ for (const [reservationId, outpoints] of reservationOutputs) {
268
+ const hasValid = outpoints.some(([h, v]) =>
269
+ incomingOutpoints.has(`${h}:${v}`)
270
+ );
271
+ if (hasValid) {
272
+ for (const [h, v] of outpoints) {
273
+ if (!incomingOutpoints.has(`${h}:${v}`)) {
274
+ outpointsToRemoveFromReservation.push([h, v]);
273
275
  }
274
276
  }
277
+ } else {
278
+ reservationsToDelete.push(reservationId);
275
279
  }
276
280
  }
277
281
 
@@ -287,20 +291,25 @@ class MysqlTokenStore {
287
291
  );
288
292
  }
289
293
 
290
- if (outputsToRemoveFromReservation.length > 0) {
291
- const placeholders = buildPlaceholders(
292
- outputsToRemoveFromReservation.length
293
- );
294
+ if (outpointsToRemoveFromReservation.length > 0) {
295
+ const pairPlaceholders = outpointsToRemoveFromReservation
296
+ .map(() => "(?, ?)")
297
+ .join(", ");
298
+ const params = [this.identity];
299
+ for (const [h, v] of outpointsToRemoveFromReservation) {
300
+ params.push(h, v);
301
+ }
294
302
  await conn.query(
295
- `DELETE FROM brz_token_outputs WHERE user_id = ? AND id IN (${placeholders})`,
296
- [this.identity, ...outputsToRemoveFromReservation]
303
+ `DELETE FROM brz_token_outputs WHERE user_id = ?
304
+ AND (prev_tx_hash, prev_tx_vout) IN (${pairPlaceholders})`,
305
+ params
297
306
  );
298
307
 
299
308
  const [emptyRows] = await conn.query(
300
309
  `SELECT r.id FROM brz_token_reservations r
301
310
  LEFT JOIN brz_token_outputs o
302
311
  ON o.reservation_id = r.id AND o.user_id = r.user_id
303
- WHERE r.user_id = ? AND o.id IS NULL`,
312
+ WHERE r.user_id = ? AND o.prev_tx_hash IS NULL`,
304
313
  [this.identity]
305
314
  );
306
315
  const emptyIds = emptyRows.map((r) => r.id);
@@ -314,10 +323,12 @@ class MysqlTokenStore {
314
323
  }
315
324
 
316
325
  const [reservedOutputRows] = await conn.query(
317
- "SELECT id FROM brz_token_outputs WHERE user_id = ? AND reservation_id IS NOT NULL",
326
+ "SELECT prev_tx_hash, prev_tx_vout FROM brz_token_outputs WHERE user_id = ? AND reservation_id IS NOT NULL",
318
327
  [this.identity]
319
328
  );
320
- const reservedOutputIds = new Set(reservedOutputRows.map((r) => r.id));
329
+ const reservedOutpoints = new Set(
330
+ reservedOutputRows.map((r) => `${r.prev_tx_hash}:${r.prev_tx_vout}`)
331
+ );
321
332
 
322
333
  await conn.query(
323
334
  `DELETE FROM brz_token_metadata
@@ -332,10 +343,8 @@ class MysqlTokenStore {
332
343
  await this._upsertMetadata(conn, to.metadata);
333
344
 
334
345
  for (const output of to.outputs) {
335
- if (
336
- reservedOutputIds.has(output.output.id) ||
337
- spentIds.has(output.output.id)
338
- ) {
346
+ const outpoint = `${output.prevTxHash}:${output.prevTxVout}`;
347
+ if (reservedOutpoints.has(outpoint) || spentOutpoints.has(outpoint)) {
339
348
  continue;
340
349
  }
341
350
  await this._insertSingleOutput(
@@ -410,7 +419,7 @@ class MysqlTokenStore {
410
419
  const [rows] = await this.pool.query(
411
420
  `SELECT m.identifier, m.issuer_public_key, m.name, m.ticker, m.decimals,
412
421
  m.max_supply, m.is_freezable, m.creation_entity_public_key,
413
- o.id AS output_id, o.owner_public_key, o.revocation_commitment,
422
+ o.owner_public_key, o.revocation_commitment,
414
423
  o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
415
424
  o.token_public_key, o.token_amount, o.token_identifier,
416
425
  o.prev_tx_hash, o.prev_tx_vout, o.reservation_id,
@@ -439,7 +448,7 @@ class MysqlTokenStore {
439
448
 
440
449
  const entry = map.get(row.identifier);
441
450
 
442
- if (!row.output_id) {
451
+ if (!row.prev_tx_hash) {
443
452
  continue;
444
453
  }
445
454
 
@@ -482,7 +491,7 @@ class MysqlTokenStore {
482
491
  const [rows] = await this.pool.query(
483
492
  `SELECT m.identifier, m.issuer_public_key, m.name, m.ticker, m.decimals,
484
493
  m.max_supply, m.is_freezable, m.creation_entity_public_key,
485
- o.id AS output_id, o.owner_public_key, o.revocation_commitment,
494
+ o.owner_public_key, o.revocation_commitment,
486
495
  o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
487
496
  o.token_public_key, o.token_amount, o.token_identifier,
488
497
  o.prev_tx_hash, o.prev_tx_vout, o.reservation_id,
@@ -510,7 +519,7 @@ class MysqlTokenStore {
510
519
  };
511
520
 
512
521
  for (const row of rows) {
513
- if (!row.output_id) {
522
+ if (!row.prev_tx_hash) {
514
523
  continue;
515
524
  }
516
525
 
@@ -543,23 +552,20 @@ class MysqlTokenStore {
543
552
  */
544
553
  async updateTokenOutputs(outputsToRemove, outputsToAdd) {
545
554
  try {
546
- await this._withTransaction(async (conn) => {
555
+ // Serialize against the other token-store mutators (refresh, reservation,
556
+ // finalization), which take the same per-user advisory lock.
557
+ await this._withWriteTransaction(async (conn) => {
547
558
  // 1. Remove spent outputs and mark as spent.
548
559
  if (outputsToRemove && outputsToRemove.length > 0) {
549
560
  for (const [txHash, vout] of outputsToRemove) {
550
- const [rows] = await conn.query(
551
- "SELECT id FROM brz_token_outputs WHERE user_id = ? AND prev_tx_hash = ? AND prev_tx_vout = ?",
561
+ const [result] = await conn.query(
562
+ "DELETE FROM brz_token_outputs WHERE user_id = ? AND prev_tx_hash = ? AND prev_tx_vout = ?",
552
563
  [this.identity, txHash, vout]
553
564
  );
554
- if (rows.length > 0) {
555
- const outputId = rows[0].id;
556
- await conn.query(
557
- "DELETE FROM brz_token_outputs WHERE user_id = ? AND id = ?",
558
- [this.identity, outputId]
559
- );
565
+ if (result.affectedRows > 0) {
560
566
  await conn.query(
561
- "INSERT IGNORE INTO brz_token_spent_outputs (user_id, output_id, spent_at) VALUES (?, ?, NOW())",
562
- [this.identity, outputId]
567
+ "INSERT IGNORE INTO brz_token_spent_outputs (user_id, prev_tx_hash, prev_tx_vout, spent_at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))",
568
+ [this.identity, txHash, vout]
563
569
  );
564
570
  }
565
571
  }
@@ -569,12 +575,17 @@ class MysqlTokenStore {
569
575
  if (outputsToAdd) {
570
576
  await this._upsertMetadata(conn, outputsToAdd.metadata);
571
577
 
572
- const outputIds = outputsToAdd.outputs.map((o) => o.output.id);
573
- if (outputIds.length > 0) {
574
- const placeholders = buildPlaceholders(outputIds.length);
578
+ if (outputsToAdd.outputs.length > 0) {
579
+ const pairPlaceholders = outputsToAdd.outputs
580
+ .map(() => "(?, ?)")
581
+ .join(", ");
582
+ const params = [this.identity];
583
+ for (const o of outputsToAdd.outputs) {
584
+ params.push(o.prevTxHash, o.prevTxVout);
585
+ }
575
586
  await conn.query(
576
- `DELETE FROM brz_token_spent_outputs WHERE user_id = ? AND output_id IN (${placeholders})`,
577
- [this.identity, ...outputIds]
587
+ `DELETE FROM brz_token_spent_outputs WHERE user_id = ? AND (prev_tx_hash, prev_tx_vout) IN (${pairPlaceholders})`,
588
+ params
578
589
  );
579
590
  }
580
591
 
@@ -636,7 +647,7 @@ class MysqlTokenStore {
636
647
  const metadata = this._metadataFromRow(metadataRows[0]);
637
648
 
638
649
  const [outputRows] = await conn.query(
639
- `SELECT o.id AS output_id, o.owner_public_key, o.revocation_commitment,
650
+ `SELECT o.owner_public_key, o.revocation_commitment,
640
651
  o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
641
652
  o.token_public_key, o.token_amount, o.token_identifier,
642
653
  o.prev_tx_hash, o.prev_tx_vout
@@ -648,10 +659,12 @@ class MysqlTokenStore {
648
659
  let outputs = outputRows.map((row) => this._outputFromRow(row));
649
660
 
650
661
  if (preferredOutputs && preferredOutputs.length > 0) {
651
- const preferredIds = new Set(
652
- preferredOutputs.map((p) => p.output.id)
662
+ const preferredOutpoints = new Set(
663
+ preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
664
+ );
665
+ outputs = outputs.filter((o) =>
666
+ preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
653
667
  );
654
- outputs = outputs.filter((o) => preferredIds.has(o.output.id));
655
668
  }
656
669
 
657
670
  let selectedOutputs;
@@ -727,16 +740,22 @@ class MysqlTokenStore {
727
740
  const reservationId = this._generateId();
728
741
 
729
742
  await conn.query(
730
- "INSERT INTO brz_token_reservations (user_id, id, purpose) VALUES (?, ?, ?)",
743
+ "INSERT INTO brz_token_reservations (user_id, id, purpose, created_at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))",
731
744
  [this.identity, reservationId, purpose]
732
745
  );
733
746
 
734
- const selectedIds = selectedOutputs.map((o) => o.output.id);
735
- if (selectedIds.length > 0) {
736
- const placeholders = buildPlaceholders(selectedIds.length);
747
+ if (selectedOutputs.length > 0) {
748
+ const pairPlaceholders = selectedOutputs
749
+ .map(() => "(?, ?)")
750
+ .join(", ");
751
+ const params = [reservationId, this.identity];
752
+ for (const o of selectedOutputs) {
753
+ params.push(o.prevTxHash, o.prevTxVout);
754
+ }
737
755
  await conn.query(
738
- `UPDATE brz_token_outputs SET reservation_id = ? WHERE user_id = ? AND id IN (${placeholders})`,
739
- [reservationId, this.identity, ...selectedIds]
756
+ `UPDATE brz_token_outputs SET reservation_id = ? WHERE user_id = ?
757
+ AND (prev_tx_hash, prev_tx_vout) IN (${pairPlaceholders})`,
758
+ params
740
759
  );
741
760
  }
742
761
 
@@ -794,23 +813,22 @@ class MysqlTokenStore {
794
813
  const isSwap = reservationRows[0].purpose === "Swap";
795
814
 
796
815
  const [reservedRows] = await conn.query(
797
- "SELECT id FROM brz_token_outputs WHERE user_id = ? AND reservation_id = ?",
816
+ "SELECT prev_tx_hash, prev_tx_vout FROM brz_token_outputs WHERE user_id = ? AND reservation_id = ?",
798
817
  [this.identity, id]
799
818
  );
800
- const reservedOutputIds = reservedRows.map((r) => r.id);
801
819
 
802
- if (reservedOutputIds.length > 0) {
803
- const valueClauses = new Array(reservedOutputIds.length)
804
- .fill("(?, ?)")
820
+ if (reservedRows.length > 0) {
821
+ const valueClauses = new Array(reservedRows.length)
822
+ .fill("(?, ?, ?, UTC_TIMESTAMP(6))")
805
823
  .join(", ");
806
824
  const params = [];
807
- for (const outputId of reservedOutputIds) {
808
- params.push(this.identity, outputId);
825
+ for (const row of reservedRows) {
826
+ params.push(this.identity, row.prev_tx_hash, row.prev_tx_vout);
809
827
  }
810
828
  // Suppress duplicate-PK errors only.
811
829
  await conn.query(
812
- `INSERT INTO brz_token_spent_outputs (user_id, output_id) VALUES ${valueClauses}
813
- ON DUPLICATE KEY UPDATE output_id = output_id`,
830
+ `INSERT INTO brz_token_spent_outputs (user_id, prev_tx_hash, prev_tx_vout, spent_at) VALUES ${valueClauses}
831
+ ON DUPLICATE KEY UPDATE prev_tx_hash = prev_tx_hash`,
814
832
  params
815
833
  );
816
834
  }
@@ -828,7 +846,7 @@ class MysqlTokenStore {
828
846
  // (and thus has no row) gets one created lazily.
829
847
  if (isSwap) {
830
848
  await conn.query(
831
- `INSERT INTO brz_token_swap_status (user_id, last_completed_at) VALUES (?, NOW(6))
849
+ `INSERT INTO brz_token_swap_status (user_id, last_completed_at) VALUES (?, UTC_TIMESTAMP(6))
832
850
  ON DUPLICATE KEY UPDATE last_completed_at = VALUES(last_completed_at)`,
833
851
  [this.identity]
834
852
  );
@@ -854,7 +872,7 @@ class MysqlTokenStore {
854
872
 
855
873
  async now() {
856
874
  try {
857
- const [rows] = await this.pool.query("SELECT NOW(6) AS now");
875
+ const [rows] = await this.pool.query("SELECT UTC_TIMESTAMP(6) AS now");
858
876
  const value = rows[0].now;
859
877
  if (value instanceof Date) return value.getTime();
860
878
  return new Date(value).getTime();
@@ -892,14 +910,14 @@ class MysqlTokenStore {
892
910
  SELECT id FROM (
893
911
  SELECT id FROM brz_token_reservations
894
912
  WHERE user_id = ?
895
- AND created_at < DATE_SUB(NOW(6), INTERVAL ? SECOND)
913
+ AND created_at < DATE_SUB(UTC_TIMESTAMP(6), INTERVAL ? SECOND)
896
914
  ) AS stale
897
915
  )`,
898
916
  [this.identity, this.identity, RESERVATION_TIMEOUT_SECS]
899
917
  );
900
918
  await conn.query(
901
919
  `DELETE FROM brz_token_reservations
902
- WHERE user_id = ? AND created_at < DATE_SUB(NOW(6), INTERVAL ? SECOND)`,
920
+ WHERE user_id = ? AND created_at < DATE_SUB(UTC_TIMESTAMP(6), INTERVAL ? SECOND)`,
903
921
  [this.identity, RESERVATION_TIMEOUT_SECS]
904
922
  );
905
923
  }
@@ -933,19 +951,18 @@ class MysqlTokenStore {
933
951
  }
934
952
 
935
953
  async _insertSingleOutput(conn, tokenIdentifier, output) {
936
- // ON DUPLICATE KEY UPDATE id = id no-ops on the (user_id, id) primary key
937
- // conflict only — unlike INSERT IGNORE, FK / NOT NULL / type errors
938
- // still propagate.
954
+ // ON DUPLICATE KEY UPDATE prev_tx_hash = prev_tx_hash no-ops on the
955
+ // (user_id, prev_tx_hash, prev_tx_vout) primary key conflict only unlike
956
+ // INSERT IGNORE, FK / NOT NULL / type errors still propagate.
939
957
  await conn.query(
940
958
  `INSERT INTO brz_token_outputs
941
- (user_id, id, token_identifier, owner_public_key, revocation_commitment,
959
+ (user_id, token_identifier, owner_public_key, revocation_commitment,
942
960
  withdraw_bond_sats, withdraw_relative_block_locktime,
943
961
  token_public_key, token_amount, prev_tx_hash, prev_tx_vout, added_at)
944
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(6))
945
- ON DUPLICATE KEY UPDATE id = id`,
962
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(6))
963
+ ON DUPLICATE KEY UPDATE prev_tx_hash = prev_tx_hash`,
946
964
  [
947
965
  this.identity,
948
- output.output.id,
949
966
  tokenIdentifier,
950
967
  output.output.ownerPublicKey,
951
968
  output.output.revocationCommitment,
@@ -975,7 +992,6 @@ class MysqlTokenStore {
975
992
  _outputFromRow(row) {
976
993
  return {
977
994
  output: {
978
- id: row.output_id,
979
995
  ownerPublicKey: row.owner_public_key,
980
996
  revocationCommitment: row.revocation_commitment,
981
997
  withdrawBondSats: Number(row.withdraw_bond_sats),
@@ -999,6 +1015,10 @@ function createMysqlPool(config) {
999
1015
  connectTimeout: (config.createTimeoutSecs || 0) * 1000 || 10000,
1000
1016
  idleTimeout: (config.recycleTimeoutSecs || 0) * 1000 || 10000,
1001
1017
  waitForConnections: true,
1018
+ // Serialize JS `Date` parameters as UTC strings rather than host-local
1019
+ // time. Paired with explicit `UTC_TIMESTAMP(6)` on the server side, this
1020
+ // keeps timestamp comparisons consistent regardless of the host TZ.
1021
+ timezone: "Z",
1002
1022
  });
1003
1023
  }
1004
1024
 
@@ -109,7 +109,7 @@ class MysqlTokenStoreMigrationManager {
109
109
  await conn.query(`
110
110
  CREATE TABLE IF NOT EXISTS \`${TOKEN_MIGRATIONS_TABLE}\` (
111
111
  version INT PRIMARY KEY,
112
- applied_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
112
+ applied_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))
113
113
  )
114
114
  `);
115
115
 
@@ -132,7 +132,7 @@ class MysqlTokenStoreMigrationManager {
132
132
  await runMigrationStep(conn, step);
133
133
  }
134
134
  await conn.query(
135
- `INSERT INTO \`${TOKEN_MIGRATIONS_TABLE}\` (version) VALUES (?)`,
135
+ `INSERT INTO \`${TOKEN_MIGRATIONS_TABLE}\` (version, applied_at) VALUES (?, UTC_TIMESTAMP(6))`,
136
136
  [version]
137
137
  );
138
138
  }
@@ -447,6 +447,63 @@ class MysqlTokenStoreMigrationManager {
447
447
  `ALTER TABLE brz_token_swap_status ADD PRIMARY KEY (user_id)`,
448
448
  ],
449
449
  },
450
+ {
451
+ // Pin DATETIME defaults to UTC. Server-side INSERTs already pass
452
+ // `UTC_TIMESTAMP(6)` explicitly; this migration makes the column
453
+ // default match, so any future callsite that omits the column also
454
+ // gets a UTC value rather than a session-TZ-dependent one.
455
+ name: "Pin DATETIME defaults to UTC",
456
+ sql: [
457
+ `ALTER TABLE brz_token_outputs MODIFY COLUMN added_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
458
+ `ALTER TABLE brz_token_reservations MODIFY COLUMN created_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
459
+ `ALTER TABLE brz_token_spent_outputs MODIFY COLUMN spent_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
460
+ `ALTER TABLE brz_token_schema_migrations MODIFY COLUMN applied_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
461
+ ],
462
+ },
463
+ {
464
+ // Mirrors Rust migration 4 in spark-mysql/src/token_store.rs.
465
+ // Re-keys brz_token_spent_outputs by (prev_tx_hash, prev_tx_vout) instead
466
+ // of the operator-issued output id. v3 FinalTokenOutput carries no id
467
+ // field, so post-broadcast spent markers only have an outpoint to work
468
+ // with. Existing output_id-keyed rows can't be backfilled (no outpoint
469
+ // stored alongside them), so the table is wiped on upgrade — spent
470
+ // markers are short-lived (5 minute cleanup window) so wiping is
471
+ // equivalent to letting them age out.
472
+ name: "Re-key spent outputs by (prev_tx_hash, prev_tx_vout)",
473
+ sql: [
474
+ `DROP TABLE IF EXISTS brz_token_spent_outputs`,
475
+ `CREATE TABLE brz_token_spent_outputs (
476
+ user_id VARBINARY(33) NOT NULL,
477
+ prev_tx_hash VARCHAR(255) NOT NULL,
478
+ prev_tx_vout INT NOT NULL,
479
+ spent_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6)),
480
+ PRIMARY KEY (user_id, prev_tx_hash, prev_tx_vout)
481
+ )`,
482
+ ],
483
+ },
484
+ {
485
+ // Mirrors Rust migration 5 in spark-mysql/src/token_store.rs.
486
+ // Re-key brz_token_outputs by (prev_tx_hash, prev_tx_vout) and drop the
487
+ // legacy id column. id already held "{prev_tx_hash}:{vout}", so the
488
+ // outpoint is the natural key. Dedup any duplicate-outpoint rows
489
+ // (possible from pre-outpoint code) before adding the composite PK,
490
+ // preferring rows that hold a reservation.
491
+ name: "Re-key token outputs by (prev_tx_hash, prev_tx_vout), drop legacy id",
492
+ sql: [
493
+ `DELETE a FROM brz_token_outputs a
494
+ JOIN brz_token_outputs b
495
+ ON a.user_id = b.user_id
496
+ AND a.prev_tx_hash = b.prev_tx_hash
497
+ AND a.prev_tx_vout = b.prev_tx_vout
498
+ AND ((b.reservation_id IS NOT NULL) > (a.reservation_id IS NOT NULL)
499
+ OR ((b.reservation_id IS NOT NULL) = (a.reservation_id IS NOT NULL)
500
+ AND b.id > a.id))`,
501
+ `ALTER TABLE brz_token_outputs
502
+ DROP PRIMARY KEY,
503
+ ADD PRIMARY KEY (user_id, prev_tx_hash, prev_tx_vout)`,
504
+ `ALTER TABLE brz_token_outputs DROP COLUMN id`,
505
+ ],
506
+ },
450
507
  ];
451
508
  }
452
509
  }
@@ -430,7 +430,7 @@ class MysqlTreeStore {
430
430
  // (and thus has no row) gets one created lazily.
431
431
  if (isSwap && newLeaves && newLeaves.length > 0) {
432
432
  await conn.query(
433
- `INSERT INTO brz_tree_swap_status (user_id, last_completed_at) VALUES (?, NOW(6))
433
+ `INSERT INTO brz_tree_swap_status (user_id, last_completed_at) VALUES (?, UTC_TIMESTAMP(6))
434
434
  ON DUPLICATE KEY UPDATE last_completed_at = VALUES(last_completed_at)`,
435
435
  [this.identity]
436
436
  );
@@ -581,7 +581,7 @@ class MysqlTreeStore {
581
581
 
582
582
  async now() {
583
583
  try {
584
- const [rows] = await this.pool.query("SELECT NOW(6) AS now");
584
+ const [rows] = await this.pool.query("SELECT UTC_TIMESTAMP(6) AS now");
585
585
  const value = rows[0].now;
586
586
  // mysql2 typically returns DATETIME as a JS Date when dateStrings is false (default).
587
587
  if (value instanceof Date) return value.getTime();
@@ -809,7 +809,7 @@ class MysqlTreeStore {
809
809
 
810
810
  async _createReservation(conn, reservationId, leaves, purpose, pendingChange) {
811
811
  await conn.query(
812
- "INSERT INTO brz_tree_reservations (user_id, id, purpose, pending_change_amount) VALUES (?, ?, ?, ?)",
812
+ "INSERT INTO brz_tree_reservations (user_id, id, purpose, pending_change_amount, created_at) VALUES (?, ?, ?, ?, UTC_TIMESTAMP(6))",
813
813
  [this.identity, reservationId, purpose, pendingChange]
814
814
  );
815
815
 
@@ -827,7 +827,7 @@ class MysqlTreeStore {
827
827
  if (filtered.length === 0) return;
828
828
 
829
829
  const valueClauses = new Array(filtered.length)
830
- .fill("(?, ?, ?, ?, ?, ?, NOW(6))")
830
+ .fill("(?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(6))")
831
831
  .join(", ");
832
832
  const params = [];
833
833
  for (const leaf of filtered) {
@@ -849,7 +849,7 @@ class MysqlTreeStore {
849
849
  is_missing_from_operators = VALUES(is_missing_from_operators),
850
850
  data = VALUES(data),
851
851
  value = VALUES(value),
852
- added_at = NOW(6)`,
852
+ added_at = UTC_TIMESTAMP(6)`,
853
853
  params
854
854
  );
855
855
  }
@@ -867,7 +867,9 @@ class MysqlTreeStore {
867
867
  async _batchInsertSpentLeaves(conn, leafIds) {
868
868
  if (leafIds.length === 0) return;
869
869
 
870
- const valueClauses = new Array(leafIds.length).fill("(?, ?)").join(", ");
870
+ const valueClauses = new Array(leafIds.length)
871
+ .fill("(?, ?, UTC_TIMESTAMP(6))")
872
+ .join(", ");
871
873
  const params = [];
872
874
  for (const id of leafIds) {
873
875
  params.push(this.identity, id);
@@ -876,7 +878,7 @@ class MysqlTreeStore {
876
878
  // problems (FK violations, NOT NULL violations, type errors) still
877
879
  // propagate.
878
880
  await conn.query(
879
- `INSERT INTO brz_tree_spent_leaves (user_id, leaf_id) VALUES ${valueClauses}
881
+ `INSERT INTO brz_tree_spent_leaves (user_id, leaf_id, spent_at) VALUES ${valueClauses}
880
882
  ON DUPLICATE KEY UPDATE leaf_id = leaf_id`,
881
883
  params
882
884
  );
@@ -904,14 +906,14 @@ class MysqlTreeStore {
904
906
  SELECT id FROM (
905
907
  SELECT id FROM brz_tree_reservations
906
908
  WHERE user_id = ?
907
- AND created_at < DATE_SUB(NOW(6), INTERVAL ? SECOND)
909
+ AND created_at < DATE_SUB(UTC_TIMESTAMP(6), INTERVAL ? SECOND)
908
910
  ) AS stale
909
911
  )`,
910
912
  [this.identity, this.identity, RESERVATION_TIMEOUT_SECS]
911
913
  );
912
914
  await conn.query(
913
915
  `DELETE FROM brz_tree_reservations
914
- WHERE user_id = ? AND created_at < DATE_SUB(NOW(6), INTERVAL ? SECOND)`,
916
+ WHERE user_id = ? AND created_at < DATE_SUB(UTC_TIMESTAMP(6), INTERVAL ? SECOND)`,
915
917
  [this.identity, RESERVATION_TIMEOUT_SECS]
916
918
  );
917
919
  }
@@ -936,6 +938,10 @@ function createMysqlPool(config) {
936
938
  connectTimeout: (config.createTimeoutSecs || 0) * 1000 || 10000,
937
939
  idleTimeout: (config.recycleTimeoutSecs || 0) * 1000 || 10000,
938
940
  waitForConnections: true,
941
+ // Serialize JS `Date` parameters as UTC strings rather than host-local
942
+ // time. Paired with explicit `UTC_TIMESTAMP(6)` on the server side, this
943
+ // keeps timestamp comparisons consistent regardless of the host TZ.
944
+ timezone: "Z",
939
945
  });
940
946
  }
941
947
 
@@ -98,7 +98,7 @@ class MysqlTreeStoreMigrationManager {
98
98
  await conn.query(`
99
99
  CREATE TABLE IF NOT EXISTS \`${TREE_MIGRATIONS_TABLE}\` (
100
100
  version INT PRIMARY KEY,
101
- applied_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
101
+ applied_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))
102
102
  )
103
103
  `);
104
104
 
@@ -121,7 +121,7 @@ class MysqlTreeStoreMigrationManager {
121
121
  await runMigrationStep(conn, step);
122
122
  }
123
123
  await conn.query(
124
- `INSERT INTO \`${TREE_MIGRATIONS_TABLE}\` (version) VALUES (?)`,
124
+ `INSERT INTO \`${TREE_MIGRATIONS_TABLE}\` (version, applied_at) VALUES (?, UTC_TIMESTAMP(6))`,
125
125
  [version]
126
126
  );
127
127
  }
@@ -367,6 +367,20 @@ class MysqlTreeStoreMigrationManager {
367
367
  `ALTER TABLE brz_tree_swap_status ADD PRIMARY KEY (user_id)`,
368
368
  ],
369
369
  },
370
+ {
371
+ // Pin DATETIME defaults to UTC. Server-side INSERTs already pass
372
+ // `UTC_TIMESTAMP(6)` explicitly; this migration makes the column
373
+ // default match, so any future callsite that omits the column also
374
+ // gets a UTC value rather than a session-TZ-dependent one.
375
+ name: "Pin DATETIME defaults to UTC",
376
+ sql: [
377
+ `ALTER TABLE brz_tree_reservations MODIFY COLUMN created_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
378
+ `ALTER TABLE brz_tree_leaves MODIFY COLUMN created_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
379
+ `ALTER TABLE brz_tree_leaves MODIFY COLUMN added_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
380
+ `ALTER TABLE brz_tree_spent_leaves MODIFY COLUMN spent_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
381
+ `ALTER TABLE brz_tree_schema_migrations MODIFY COLUMN applied_at DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP(6))`,
382
+ ],
383
+ },
370
384
  ];
371
385
  }
372
386
  }
@@ -7,11 +7,11 @@
7
7
  "postgres-storage/",
8
8
  "postgres-tree-store/",
9
9
  "postgres-token-store/",
10
- "postgres-session-manager/",
10
+ "postgres-session-store/",
11
11
  "mysql-storage/",
12
12
  "mysql-tree-store/",
13
13
  "mysql-token-store/",
14
- "mysql-session-manager/",
14
+ "mysql-session-store/",
15
15
  "index.js",
16
16
  "index.mjs"
17
17
  ],
@@ -0,0 +1,13 @@
1
+ // errors.cjs - Session store error wrapper with cause chain support
2
+ class SessionStoreError extends Error {
3
+ constructor(message, cause = null) {
4
+ super(message);
5
+ this.name = 'SessionStoreError';
6
+ this.cause = cause;
7
+ if (Error.captureStackTrace) {
8
+ Error.captureStackTrace(this, SessionStoreError);
9
+ }
10
+ }
11
+ }
12
+
13
+ module.exports = { SessionStoreError };