@breeztech/breez-sdk-spark 0.13.10-dev → 0.13.12-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 (49) hide show
  1. package/breez-sdk-spark.tgz +0 -0
  2. package/bundler/breez_sdk_spark_wasm.d.ts +157 -0
  3. package/bundler/breez_sdk_spark_wasm.js +1 -1
  4. package/bundler/breez_sdk_spark_wasm_bg.js +419 -41
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
  7. package/deno/breez_sdk_spark_wasm.d.ts +157 -0
  8. package/deno/breez_sdk_spark_wasm.js +419 -41
  9. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  10. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
  11. package/nodejs/breez_sdk_spark_wasm.d.ts +157 -0
  12. package/nodejs/breez_sdk_spark_wasm.js +428 -41
  13. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  14. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
  15. package/nodejs/index.js +56 -0
  16. package/nodejs/index.mjs +9 -0
  17. package/nodejs/mysql-session-manager/errors.cjs +13 -0
  18. package/nodejs/mysql-session-manager/index.cjs +144 -0
  19. package/nodejs/mysql-session-manager/migrations.cjs +102 -0
  20. package/nodejs/mysql-session-manager/package.json +9 -0
  21. package/nodejs/mysql-storage/errors.cjs +19 -0
  22. package/nodejs/mysql-storage/index.cjs +1367 -0
  23. package/nodejs/mysql-storage/migrations.cjs +387 -0
  24. package/nodejs/mysql-storage/package.json +9 -0
  25. package/nodejs/mysql-token-store/errors.cjs +9 -0
  26. package/nodejs/mysql-token-store/index.cjs +980 -0
  27. package/nodejs/mysql-token-store/migrations.cjs +255 -0
  28. package/nodejs/mysql-token-store/package.json +9 -0
  29. package/nodejs/mysql-tree-store/errors.cjs +9 -0
  30. package/nodejs/mysql-tree-store/index.cjs +941 -0
  31. package/nodejs/mysql-tree-store/migrations.cjs +221 -0
  32. package/nodejs/mysql-tree-store/package.json +9 -0
  33. package/nodejs/package.json +5 -0
  34. package/nodejs/postgres-session-manager/errors.cjs +13 -0
  35. package/nodejs/postgres-session-manager/index.cjs +165 -0
  36. package/nodejs/postgres-session-manager/migrations.cjs +126 -0
  37. package/nodejs/postgres-session-manager/package.json +9 -0
  38. package/nodejs/postgres-storage/index.cjs +147 -92
  39. package/nodejs/postgres-storage/migrations.cjs +85 -4
  40. package/nodejs/postgres-token-store/index.cjs +178 -102
  41. package/nodejs/postgres-token-store/migrations.cjs +92 -3
  42. package/nodejs/postgres-tree-store/index.cjs +168 -83
  43. package/nodejs/postgres-tree-store/migrations.cjs +80 -3
  44. package/package.json +1 -1
  45. package/ssr/index.js +53 -0
  46. package/web/breez_sdk_spark_wasm.d.ts +184 -7
  47. package/web/breez_sdk_spark_wasm.js +419 -41
  48. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  49. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
@@ -63,15 +63,26 @@ const SELECT_PAYMENT_SQL = `
63
63
  lrm.payment_hash AS lnurl_payment_hash,
64
64
  pm.parent_payment_id
65
65
  FROM payments p
66
- LEFT JOIN payment_details_lightning l ON p.id = l.payment_id
67
- LEFT JOIN payment_details_token t ON p.id = t.payment_id
68
- LEFT JOIN payment_details_spark s ON p.id = s.payment_id
69
- LEFT JOIN payment_metadata pm ON p.id = pm.payment_id
70
- LEFT JOIN lnurl_receive_metadata lrm ON l.payment_hash = lrm.payment_hash`;
66
+ LEFT JOIN payment_details_lightning l ON p.id = l.payment_id AND p.user_id = l.user_id
67
+ LEFT JOIN payment_details_token t ON p.id = t.payment_id AND p.user_id = t.user_id
68
+ LEFT JOIN payment_details_spark s ON p.id = s.payment_id AND p.user_id = s.user_id
69
+ LEFT JOIN payment_metadata pm ON p.id = pm.payment_id AND p.user_id = pm.user_id
70
+ LEFT JOIN lnurl_receive_metadata lrm ON l.payment_hash = lrm.payment_hash AND l.user_id = lrm.user_id`;
71
71
 
72
72
  class PostgresStorage {
73
- constructor(pool, logger = null) {
73
+ /**
74
+ * @param {import('pg').Pool} pool - Connection pool (may be shared with other tenants).
75
+ * @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey
76
+ * uniquely identifying this tenant. All reads and writes are scoped to it
77
+ * so that multiple instances with distinct identities can share one DB.
78
+ * @param {object} [logger]
79
+ */
80
+ constructor(pool, identity, logger = null) {
81
+ if (!identity) {
82
+ throw new StorageError("PostgresStorage requires a tenant identity");
83
+ }
74
84
  this.pool = pool;
85
+ this.identity = Buffer.from(identity);
75
86
  this.logger = logger;
76
87
  }
77
88
 
@@ -81,7 +92,7 @@ class PostgresStorage {
81
92
  async initialize() {
82
93
  try {
83
94
  const migrationManager = new PostgresMigrationManager(this.logger);
84
- await migrationManager.migrate(this.pool);
95
+ await migrationManager.migrate(this.pool, this.identity);
85
96
  return this;
86
97
  } catch (error) {
87
98
  throw new StorageError(
@@ -127,8 +138,8 @@ class PostgresStorage {
127
138
  async getCachedItem(key) {
128
139
  try {
129
140
  const result = await this.pool.query(
130
- "SELECT value FROM settings WHERE key = $1",
131
- [key]
141
+ "SELECT value FROM settings WHERE user_id = $1 AND key = $2",
142
+ [this.identity, key]
132
143
  );
133
144
  return result.rows.length > 0 ? result.rows[0].value : null;
134
145
  } catch (error) {
@@ -142,9 +153,9 @@ class PostgresStorage {
142
153
  async setCachedItem(key, value) {
143
154
  try {
144
155
  await this.pool.query(
145
- `INSERT INTO settings (key, value) VALUES ($1, $2)
146
- ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
147
- [key, value]
156
+ `INSERT INTO settings (user_id, key, value) VALUES ($1, $2, $3)
157
+ ON CONFLICT(user_id, key) DO UPDATE SET value = EXCLUDED.value`,
158
+ [this.identity, key, value]
148
159
  );
149
160
  } catch (error) {
150
161
  throw new StorageError(
@@ -156,7 +167,10 @@ class PostgresStorage {
156
167
 
157
168
  async deleteCachedItem(key) {
158
169
  try {
159
- await this.pool.query("DELETE FROM settings WHERE key = $1", [key]);
170
+ await this.pool.query(
171
+ "DELETE FROM settings WHERE user_id = $1 AND key = $2",
172
+ [this.identity, key]
173
+ );
160
174
  } catch (error) {
161
175
  throw new StorageError(
162
176
  `Failed to delete cached item '${key}': ${error.message}`,
@@ -173,9 +187,10 @@ class PostgresStorage {
173
187
  const actualLimit =
174
188
  request.limit != null ? request.limit : 4294967295;
175
189
 
176
- const whereClauses = [];
177
- const params = [];
178
- let paramIdx = 1;
190
+ // Tenant scoping is always $1; dynamic filters use $2 onwards.
191
+ const whereClauses = ["p.user_id = $1"];
192
+ const params = [this.identity];
193
+ let paramIdx = 2;
179
194
 
180
195
  // Filter by payment type
181
196
  if (request.typeFilter && request.typeFilter.length > 0) {
@@ -349,9 +364,9 @@ class PostgresStorage {
349
364
  const spark = payment.details?.type === "spark" ? true : null;
350
365
 
351
366
  await client.query(
352
- `INSERT INTO payments (id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, deposit_tx_id, spark)
353
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
354
- ON CONFLICT(id) DO UPDATE SET
367
+ `INSERT INTO payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, deposit_tx_id, spark)
368
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
369
+ ON CONFLICT(user_id, id) DO UPDATE SET
355
370
  payment_type=EXCLUDED.payment_type,
356
371
  status=EXCLUDED.status,
357
372
  amount=EXCLUDED.amount,
@@ -362,6 +377,7 @@ class PostgresStorage {
362
377
  deposit_tx_id=EXCLUDED.deposit_tx_id,
363
378
  spark=EXCLUDED.spark`,
364
379
  [
380
+ this.identity,
365
381
  payment.id,
366
382
  payment.paymentType,
367
383
  payment.status,
@@ -381,12 +397,13 @@ class PostgresStorage {
381
397
  payment.details.htlcDetails != null)
382
398
  ) {
383
399
  await client.query(
384
- `INSERT INTO payment_details_spark (payment_id, invoice_details, htlc_details)
385
- VALUES ($1, $2, $3)
386
- ON CONFLICT(payment_id) DO UPDATE SET
400
+ `INSERT INTO payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
401
+ VALUES ($1, $2, $3, $4)
402
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
387
403
  invoice_details=COALESCE(EXCLUDED.invoice_details, payment_details_spark.invoice_details),
388
404
  htlc_details=COALESCE(EXCLUDED.htlc_details, payment_details_spark.htlc_details)`,
389
405
  [
406
+ this.identity,
390
407
  payment.id,
391
408
  payment.details.invoiceDetails
392
409
  ? JSON.stringify(payment.details.invoiceDetails)
@@ -401,9 +418,9 @@ class PostgresStorage {
401
418
  if (payment.details?.type === "lightning") {
402
419
  await client.query(
403
420
  `INSERT INTO payment_details_lightning
404
- (payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
405
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
406
- ON CONFLICT(payment_id) DO UPDATE SET
421
+ (user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
422
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
423
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
407
424
  invoice=EXCLUDED.invoice,
408
425
  payment_hash=EXCLUDED.payment_hash,
409
426
  destination_pubkey=EXCLUDED.destination_pubkey,
@@ -412,6 +429,7 @@ class PostgresStorage {
412
429
  htlc_status=COALESCE(EXCLUDED.htlc_status, payment_details_lightning.htlc_status),
413
430
  htlc_expiry_time=COALESCE(EXCLUDED.htlc_expiry_time, payment_details_lightning.htlc_expiry_time)`,
414
431
  [
432
+ this.identity,
415
433
  payment.id,
416
434
  payment.details.invoice,
417
435
  payment.details.htlcDetails.paymentHash,
@@ -427,14 +445,15 @@ class PostgresStorage {
427
445
  if (payment.details?.type === "token") {
428
446
  await client.query(
429
447
  `INSERT INTO payment_details_token
430
- (payment_id, metadata, tx_hash, tx_type, invoice_details)
431
- VALUES ($1, $2, $3, $4, $5)
432
- ON CONFLICT(payment_id) DO UPDATE SET
448
+ (user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
449
+ VALUES ($1, $2, $3, $4, $5, $6)
450
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
433
451
  metadata=EXCLUDED.metadata,
434
452
  tx_hash=EXCLUDED.tx_hash,
435
453
  tx_type=EXCLUDED.tx_type,
436
454
  invoice_details=COALESCE(EXCLUDED.invoice_details, payment_details_token.invoice_details)`,
437
455
  [
456
+ this.identity,
438
457
  payment.id,
439
458
  JSON.stringify(payment.details.metadata),
440
459
  payment.details.txHash,
@@ -462,8 +481,8 @@ class PostgresStorage {
462
481
  }
463
482
 
464
483
  const result = await this.pool.query(
465
- `${SELECT_PAYMENT_SQL} WHERE p.id = $1`,
466
- [id]
484
+ `${SELECT_PAYMENT_SQL} WHERE p.user_id = $1 AND p.id = $2`,
485
+ [this.identity, id]
467
486
  );
468
487
 
469
488
  if (result.rows.length === 0) {
@@ -487,8 +506,8 @@ class PostgresStorage {
487
506
  }
488
507
 
489
508
  const result = await this.pool.query(
490
- `${SELECT_PAYMENT_SQL} WHERE l.invoice = $1`,
491
- [invoice]
509
+ `${SELECT_PAYMENT_SQL} WHERE p.user_id = $1 AND l.invoice = $2`,
510
+ [this.identity, invoice]
492
511
  );
493
512
 
494
513
  if (result.rows.length === 0) {
@@ -511,22 +530,24 @@ class PostgresStorage {
511
530
  return {};
512
531
  }
513
532
 
514
- // Early exit if no related payments exist
533
+ // Early exit if no related payments exist for this tenant
515
534
  const hasRelatedResult = await this.pool.query(
516
- "SELECT EXISTS(SELECT 1 FROM payment_metadata WHERE parent_payment_id IS NOT NULL LIMIT 1)"
535
+ "SELECT EXISTS(SELECT 1 FROM payment_metadata WHERE user_id = $1 AND parent_payment_id IS NOT NULL LIMIT 1)",
536
+ [this.identity]
517
537
  );
518
538
  if (!hasRelatedResult.rows[0].exists) {
519
539
  return {};
520
540
  }
521
541
 
542
+ // $1 is reserved for user_id; parent ids start at $2.
522
543
  const placeholders = parentPaymentIds.map(
523
- (_, i) => `$${i + 1}`
544
+ (_, i) => `$${i + 2}`
524
545
  );
525
- const query = `${SELECT_PAYMENT_SQL} WHERE pm.parent_payment_id IN (${placeholders.join(", ")}) ORDER BY p.timestamp ASC`;
546
+ const query = `${SELECT_PAYMENT_SQL} WHERE p.user_id = $1 AND pm.parent_payment_id IN (${placeholders.join(", ")}) ORDER BY p.timestamp ASC`;
526
547
 
527
548
  const queryResult = await this.pool.query(
528
549
  query,
529
- parentPaymentIds
550
+ [this.identity, ...parentPaymentIds]
530
551
  );
531
552
 
532
553
  const result = {};
@@ -551,9 +572,9 @@ class PostgresStorage {
551
572
  async insertPaymentMetadata(paymentId, metadata) {
552
573
  try {
553
574
  await this.pool.query(
554
- `INSERT INTO payment_metadata (payment_id, parent_payment_id, lnurl_pay_info, lnurl_withdraw_info, lnurl_description, conversion_info, conversion_status)
555
- VALUES ($1, $2, $3, $4, $5, $6, $7)
556
- ON CONFLICT(payment_id) DO UPDATE SET
575
+ `INSERT INTO payment_metadata (user_id, payment_id, parent_payment_id, lnurl_pay_info, lnurl_withdraw_info, lnurl_description, conversion_info, conversion_status)
576
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
577
+ ON CONFLICT(user_id, payment_id) DO UPDATE SET
557
578
  parent_payment_id = COALESCE(EXCLUDED.parent_payment_id, payment_metadata.parent_payment_id),
558
579
  lnurl_pay_info = COALESCE(EXCLUDED.lnurl_pay_info, payment_metadata.lnurl_pay_info),
559
580
  lnurl_withdraw_info = COALESCE(EXCLUDED.lnurl_withdraw_info, payment_metadata.lnurl_withdraw_info),
@@ -561,6 +582,7 @@ class PostgresStorage {
561
582
  conversion_info = COALESCE(EXCLUDED.conversion_info, payment_metadata.conversion_info),
562
583
  conversion_status = COALESCE(EXCLUDED.conversion_status, payment_metadata.conversion_status)`,
563
584
  [
585
+ this.identity,
564
586
  paymentId,
565
587
  metadata.parentPaymentId,
566
588
  metadata.lnurlPayInfo
@@ -589,10 +611,10 @@ class PostgresStorage {
589
611
  async addDeposit(txid, vout, amountSats, isMature) {
590
612
  try {
591
613
  await this.pool.query(
592
- `INSERT INTO unclaimed_deposits (txid, vout, amount_sats, is_mature)
593
- VALUES ($1, $2, $3, $4)
594
- ON CONFLICT(txid, vout) DO UPDATE SET is_mature = EXCLUDED.is_mature, amount_sats = EXCLUDED.amount_sats`,
595
- [txid, vout, amountSats, isMature]
614
+ `INSERT INTO unclaimed_deposits (user_id, txid, vout, amount_sats, is_mature)
615
+ VALUES ($1, $2, $3, $4, $5)
616
+ ON CONFLICT(user_id, txid, vout) DO UPDATE SET is_mature = EXCLUDED.is_mature, amount_sats = EXCLUDED.amount_sats`,
617
+ [this.identity, txid, vout, amountSats, isMature]
596
618
  );
597
619
  } catch (error) {
598
620
  throw new StorageError(
@@ -605,8 +627,8 @@ class PostgresStorage {
605
627
  async deleteDeposit(txid, vout) {
606
628
  try {
607
629
  await this.pool.query(
608
- "DELETE FROM unclaimed_deposits WHERE txid = $1 AND vout = $2",
609
- [txid, vout]
630
+ "DELETE FROM unclaimed_deposits WHERE user_id = $1 AND txid = $2 AND vout = $3",
631
+ [this.identity, txid, vout]
610
632
  );
611
633
  } catch (error) {
612
634
  throw new StorageError(
@@ -619,7 +641,8 @@ class PostgresStorage {
619
641
  async listDeposits() {
620
642
  try {
621
643
  const result = await this.pool.query(
622
- "SELECT txid, vout, amount_sats, is_mature, claim_error, refund_tx, refund_tx_id FROM unclaimed_deposits"
644
+ "SELECT txid, vout, amount_sats, is_mature, claim_error, refund_tx, refund_tx_id FROM unclaimed_deposits WHERE user_id = $1",
645
+ [this.identity]
623
646
  );
624
647
 
625
648
  return result.rows.map((row) => ({
@@ -645,15 +668,15 @@ class PostgresStorage {
645
668
  await this.pool.query(
646
669
  `UPDATE unclaimed_deposits
647
670
  SET claim_error = $1, refund_tx = NULL, refund_tx_id = NULL
648
- WHERE txid = $2 AND vout = $3`,
649
- [JSON.stringify(payload.error), txid, vout]
671
+ WHERE user_id = $2 AND txid = $3 AND vout = $4`,
672
+ [JSON.stringify(payload.error), this.identity, txid, vout]
650
673
  );
651
674
  } else if (payload.type === "refund") {
652
675
  await this.pool.query(
653
676
  `UPDATE unclaimed_deposits
654
677
  SET refund_tx = $1, refund_tx_id = $2, claim_error = NULL
655
- WHERE txid = $3 AND vout = $4`,
656
- [payload.refundTx, payload.refundTxid, txid, vout]
678
+ WHERE user_id = $3 AND txid = $4 AND vout = $5`,
679
+ [payload.refundTx, payload.refundTxid, this.identity, txid, vout]
657
680
  );
658
681
  } else {
659
682
  throw new StorageError(`Unknown payload type: ${payload.type}`);
@@ -672,13 +695,14 @@ class PostgresStorage {
672
695
  await this._withTransaction(async (client) => {
673
696
  for (const item of metadata) {
674
697
  await client.query(
675
- `INSERT INTO lnurl_receive_metadata (payment_hash, nostr_zap_request, nostr_zap_receipt, sender_comment)
676
- VALUES ($1, $2, $3, $4)
677
- ON CONFLICT(payment_hash) DO UPDATE SET
698
+ `INSERT INTO lnurl_receive_metadata (user_id, payment_hash, nostr_zap_request, nostr_zap_receipt, sender_comment)
699
+ VALUES ($1, $2, $3, $4, $5)
700
+ ON CONFLICT(user_id, payment_hash) DO UPDATE SET
678
701
  nostr_zap_request = EXCLUDED.nostr_zap_request,
679
702
  nostr_zap_receipt = EXCLUDED.nostr_zap_receipt,
680
703
  sender_comment = EXCLUDED.sender_comment`,
681
704
  [
705
+ this.identity,
682
706
  item.paymentHash,
683
707
  item.nostrZapRequest || null,
684
708
  item.nostrZapReceipt || null,
@@ -833,9 +857,10 @@ class PostgresStorage {
833
857
  const result = await this.pool.query(
834
858
  `SELECT id, name, payment_identifier, created_at, updated_at
835
859
  FROM contacts
860
+ WHERE user_id = $1
836
861
  ORDER BY name ASC
837
- LIMIT $1 OFFSET $2`,
838
- [limit, offset]
862
+ LIMIT $2 OFFSET $3`,
863
+ [this.identity, limit, offset]
839
864
  );
840
865
 
841
866
  return result.rows.map((row) => ({
@@ -858,8 +883,8 @@ class PostgresStorage {
858
883
  const result = await this.pool.query(
859
884
  `SELECT id, name, payment_identifier, created_at, updated_at
860
885
  FROM contacts
861
- WHERE id = $1`,
862
- [id]
886
+ WHERE user_id = $1 AND id = $2`,
887
+ [this.identity, id]
863
888
  );
864
889
 
865
890
  if (result.rows.length === 0) {
@@ -885,13 +910,14 @@ class PostgresStorage {
885
910
  async insertContact(contact) {
886
911
  try {
887
912
  await this.pool.query(
888
- `INSERT INTO contacts (id, name, payment_identifier, created_at, updated_at)
889
- VALUES ($1, $2, $3, $4, $5)
890
- ON CONFLICT(id) DO UPDATE SET
913
+ `INSERT INTO contacts (user_id, id, name, payment_identifier, created_at, updated_at)
914
+ VALUES ($1, $2, $3, $4, $5, $6)
915
+ ON CONFLICT(user_id, id) DO UPDATE SET
891
916
  name = EXCLUDED.name,
892
917
  payment_identifier = EXCLUDED.payment_identifier,
893
918
  updated_at = EXCLUDED.updated_at`,
894
919
  [
920
+ this.identity,
895
921
  contact.id,
896
922
  contact.name,
897
923
  contact.paymentIdentifier,
@@ -909,7 +935,10 @@ class PostgresStorage {
909
935
 
910
936
  async deleteContact(id) {
911
937
  try {
912
- await this.pool.query("DELETE FROM contacts WHERE id = $1", [id]);
938
+ await this.pool.query(
939
+ "DELETE FROM contacts WHERE user_id = $1 AND id = $2",
940
+ [this.identity, id]
941
+ );
913
942
  } catch (error) {
914
943
  throw new StorageError(
915
944
  `Failed to delete contact: ${error.message}`,
@@ -923,21 +952,25 @@ class PostgresStorage {
923
952
  async syncAddOutgoingChange(record) {
924
953
  try {
925
954
  return await this._withTransaction(async (client) => {
955
+ // Local queue revision is per-tenant — two tenants don't share a queue.
926
956
  const revisionResult = await client.query(
927
- "SELECT COALESCE(MAX(revision), 0) + 1 AS revision FROM sync_outgoing"
957
+ "SELECT COALESCE(MAX(revision), 0) + 1 AS revision FROM sync_outgoing WHERE user_id = $1",
958
+ [this.identity]
928
959
  );
929
960
  const revision = BigInt(revisionResult.rows[0].revision);
930
961
 
931
962
  await client.query(
932
963
  `INSERT INTO sync_outgoing (
964
+ user_id,
933
965
  record_type,
934
966
  data_id,
935
967
  schema_version,
936
968
  commit_time,
937
969
  updated_fields_json,
938
970
  revision
939
- ) VALUES ($1, $2, $3, $4, $5, $6)`,
971
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
940
972
  [
973
+ this.identity,
941
974
  record.id.type,
942
975
  record.id.dataId,
943
976
  record.schemaVersion,
@@ -962,8 +995,9 @@ class PostgresStorage {
962
995
  try {
963
996
  await this._withTransaction(async (client) => {
964
997
  const deleteResult = await client.query(
965
- "DELETE FROM sync_outgoing WHERE record_type = $1 AND data_id = $2 AND revision = $3",
998
+ "DELETE FROM sync_outgoing WHERE user_id = $1 AND record_type = $2 AND data_id = $3 AND revision = $4",
966
999
  [
1000
+ this.identity,
967
1001
  record.id.type,
968
1002
  record.id.dataId,
969
1003
  localRevision.toString(),
@@ -981,19 +1015,21 @@ class PostgresStorage {
981
1015
 
982
1016
  await client.query(
983
1017
  `INSERT INTO sync_state (
1018
+ user_id,
984
1019
  record_type,
985
1020
  data_id,
986
1021
  revision,
987
1022
  schema_version,
988
1023
  commit_time,
989
1024
  data
990
- ) VALUES ($1, $2, $3, $4, $5, $6)
991
- ON CONFLICT(record_type, data_id) DO UPDATE SET
1025
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1026
+ ON CONFLICT(user_id, record_type, data_id) DO UPDATE SET
992
1027
  schema_version = EXCLUDED.schema_version,
993
1028
  commit_time = EXCLUDED.commit_time,
994
1029
  data = EXCLUDED.data,
995
1030
  revision = EXCLUDED.revision`,
996
1031
  [
1032
+ this.identity,
997
1033
  record.id.type,
998
1034
  record.id.dataId,
999
1035
  record.revision.toString(),
@@ -1003,9 +1039,11 @@ class PostgresStorage {
1003
1039
  ]
1004
1040
  );
1005
1041
 
1042
+ // Upsert this tenant's revision row; fresh tenants without a row get one.
1006
1043
  await client.query(
1007
- "UPDATE sync_revision SET revision = GREATEST(revision, $1)",
1008
- [record.revision.toString()]
1044
+ `INSERT INTO sync_revision (user_id, revision) VALUES ($1, $2)
1045
+ ON CONFLICT (user_id) DO UPDATE SET revision = GREATEST(sync_revision.revision, EXCLUDED.revision)`,
1046
+ [this.identity, record.revision.toString()]
1009
1047
  );
1010
1048
  });
1011
1049
  } catch (error) {
@@ -1034,10 +1072,12 @@ class PostgresStorage {
1034
1072
  FROM sync_outgoing o
1035
1073
  LEFT JOIN sync_state e ON
1036
1074
  o.record_type = e.record_type AND
1037
- o.data_id = e.data_id
1075
+ o.data_id = e.data_id AND
1076
+ o.user_id = e.user_id
1077
+ WHERE o.user_id = $1
1038
1078
  ORDER BY o.revision ASC
1039
- LIMIT $1`,
1040
- [limit]
1079
+ LIMIT $2`,
1080
+ [this.identity, limit]
1041
1081
  );
1042
1082
 
1043
1083
  return result.rows.map((row) => {
@@ -1082,8 +1122,10 @@ class PostgresStorage {
1082
1122
 
1083
1123
  async syncGetLastRevision() {
1084
1124
  try {
1125
+ // A tenant that hasn't synced anything yet may have no row; treat as 0.
1085
1126
  const result = await this.pool.query(
1086
- "SELECT revision FROM sync_revision"
1127
+ "SELECT revision FROM sync_revision WHERE user_id = $1",
1128
+ [this.identity]
1087
1129
  );
1088
1130
  return result.rows.length > 0
1089
1131
  ? BigInt(result.rows[0].revision)
@@ -1106,18 +1148,20 @@ class PostgresStorage {
1106
1148
  for (const record of records) {
1107
1149
  await client.query(
1108
1150
  `INSERT INTO sync_incoming (
1151
+ user_id,
1109
1152
  record_type,
1110
1153
  data_id,
1111
1154
  schema_version,
1112
1155
  commit_time,
1113
1156
  data,
1114
1157
  revision
1115
- ) VALUES ($1, $2, $3, $4, $5, $6)
1116
- ON CONFLICT(record_type, data_id, revision) DO UPDATE SET
1158
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1159
+ ON CONFLICT(user_id, record_type, data_id, revision) DO UPDATE SET
1117
1160
  schema_version = EXCLUDED.schema_version,
1118
1161
  commit_time = EXCLUDED.commit_time,
1119
1162
  data = EXCLUDED.data`,
1120
1163
  [
1164
+ this.identity,
1121
1165
  record.id.type,
1122
1166
  record.id.dataId,
1123
1167
  record.schemaVersion,
@@ -1141,10 +1185,11 @@ class PostgresStorage {
1141
1185
  try {
1142
1186
  await this.pool.query(
1143
1187
  `DELETE FROM sync_incoming
1144
- WHERE record_type = $1
1145
- AND data_id = $2
1146
- AND revision = $3`,
1147
- [record.id.type, record.id.dataId, record.revision.toString()]
1188
+ WHERE user_id = $1
1189
+ AND record_type = $2
1190
+ AND data_id = $3
1191
+ AND revision = $4`,
1192
+ [this.identity, record.id.type, record.id.dataId, record.revision.toString()]
1148
1193
  );
1149
1194
  } catch (error) {
1150
1195
  throw new StorageError(
@@ -1167,10 +1212,11 @@ class PostgresStorage {
1167
1212
  e.data AS existing_data,
1168
1213
  e.revision AS existing_revision
1169
1214
  FROM sync_incoming i
1170
- LEFT JOIN sync_state e ON i.record_type = e.record_type AND i.data_id = e.data_id
1215
+ LEFT JOIN sync_state e ON i.record_type = e.record_type AND i.data_id = e.data_id AND i.user_id = e.user_id
1216
+ WHERE i.user_id = $1
1171
1217
  ORDER BY i.revision ASC
1172
- LIMIT $1`,
1173
- [limit]
1218
+ LIMIT $2`,
1219
+ [this.identity, limit]
1174
1220
  );
1175
1221
 
1176
1222
  return result.rows.map((row) => {
@@ -1230,9 +1276,12 @@ class PostgresStorage {
1230
1276
  FROM sync_outgoing o
1231
1277
  LEFT JOIN sync_state e ON
1232
1278
  o.record_type = e.record_type AND
1233
- o.data_id = e.data_id
1279
+ o.data_id = e.data_id AND
1280
+ o.user_id = e.user_id
1281
+ WHERE o.user_id = $1
1234
1282
  ORDER BY o.revision DESC
1235
- LIMIT 1`
1283
+ LIMIT 1`,
1284
+ [this.identity]
1236
1285
  );
1237
1286
 
1238
1287
  if (result.rows.length === 0) {
@@ -1284,19 +1333,21 @@ class PostgresStorage {
1284
1333
  await this._withTransaction(async (client) => {
1285
1334
  await client.query(
1286
1335
  `INSERT INTO sync_state (
1336
+ user_id,
1287
1337
  record_type,
1288
1338
  data_id,
1289
1339
  revision,
1290
1340
  schema_version,
1291
1341
  commit_time,
1292
1342
  data
1293
- ) VALUES ($1, $2, $3, $4, $5, $6)
1294
- ON CONFLICT(record_type, data_id) DO UPDATE SET
1343
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1344
+ ON CONFLICT(user_id, record_type, data_id) DO UPDATE SET
1295
1345
  schema_version = EXCLUDED.schema_version,
1296
1346
  commit_time = EXCLUDED.commit_time,
1297
1347
  data = EXCLUDED.data,
1298
1348
  revision = EXCLUDED.revision`,
1299
1349
  [
1350
+ this.identity,
1300
1351
  record.id.type,
1301
1352
  record.id.dataId,
1302
1353
  record.revision.toString(),
@@ -1307,8 +1358,9 @@ class PostgresStorage {
1307
1358
  );
1308
1359
 
1309
1360
  await client.query(
1310
- "UPDATE sync_revision SET revision = GREATEST(revision, $1)",
1311
- [record.revision.toString()]
1361
+ `INSERT INTO sync_revision (user_id, revision) VALUES ($1, $2)
1362
+ ON CONFLICT (user_id) DO UPDATE SET revision = GREATEST(sync_revision.revision, EXCLUDED.revision)`,
1363
+ [this.identity, record.revision.toString()]
1312
1364
  );
1313
1365
  });
1314
1366
  } catch (error) {
@@ -1350,12 +1402,14 @@ function defaultPostgresStorageConfig(connectionString) {
1350
1402
  * @param {number} config.maxPoolSize - Maximum number of connections in the pool
1351
1403
  * @param {number} config.createTimeoutSecs - Timeout in seconds for establishing a new connection
1352
1404
  * @param {number} config.recycleTimeoutSecs - Timeout in seconds before recycling an idle connection
1405
+ * @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey
1406
+ * uniquely identifying this tenant.
1353
1407
  * @param {object} [logger] - Optional logger
1354
1408
  * @returns {Promise<PostgresStorage>}
1355
1409
  */
1356
- async function createPostgresStorage(config, logger = null) {
1410
+ async function createPostgresStorage(config, identity, logger = null) {
1357
1411
  const pool = createPostgresPool(config);
1358
- return createPostgresStorageWithPool(pool, logger);
1412
+ return createPostgresStorageWithPool(pool, identity, logger);
1359
1413
  }
1360
1414
 
1361
1415
  /**
@@ -1378,11 +1432,12 @@ function createPostgresPool(config) {
1378
1432
  * Create a PostgresStorage instance from an existing pg.Pool.
1379
1433
  *
1380
1434
  * @param {pg.Pool} pool - An existing connection pool
1435
+ * @param {Buffer|Uint8Array} identity - 33-byte tenant identity (secp256k1 pubkey).
1381
1436
  * @param {object} [logger] - Optional logger
1382
1437
  * @returns {Promise<PostgresStorage>}
1383
1438
  */
1384
- async function createPostgresStorageWithPool(pool, logger = null) {
1385
- const storage = new PostgresStorage(pool, logger);
1439
+ async function createPostgresStorageWithPool(pool, identity, logger = null) {
1440
+ const storage = new PostgresStorage(pool, identity, logger);
1386
1441
  await storage.initialize();
1387
1442
  return storage;
1388
1443
  }