@powersync/service-core 1.13.3 → 1.14.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 (94) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/api/diagnostics.js +31 -1
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/auth/CachedKeyCollector.js +26 -2
  5. package/dist/auth/CachedKeyCollector.js.map +1 -1
  6. package/dist/auth/KeySpec.d.ts +1 -0
  7. package/dist/auth/KeySpec.js +12 -0
  8. package/dist/auth/KeySpec.js.map +1 -1
  9. package/dist/auth/KeyStore.d.ts +19 -0
  10. package/dist/auth/KeyStore.js +17 -5
  11. package/dist/auth/KeyStore.js.map +1 -1
  12. package/dist/auth/RemoteJWKSCollector.d.ts +3 -0
  13. package/dist/auth/RemoteJWKSCollector.js +9 -3
  14. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  15. package/dist/auth/StaticSupabaseKeyCollector.d.ts +2 -1
  16. package/dist/auth/StaticSupabaseKeyCollector.js +1 -1
  17. package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
  18. package/dist/auth/utils.d.ts +19 -0
  19. package/dist/auth/utils.js +106 -3
  20. package/dist/auth/utils.js.map +1 -1
  21. package/dist/entry/commands/compact-action.js +10 -1
  22. package/dist/entry/commands/compact-action.js.map +1 -1
  23. package/dist/metrics/open-telemetry/util.js +3 -1
  24. package/dist/metrics/open-telemetry/util.js.map +1 -1
  25. package/dist/replication/AbstractReplicator.js +2 -2
  26. package/dist/replication/AbstractReplicator.js.map +1 -1
  27. package/dist/routes/auth.d.ts +1 -21
  28. package/dist/routes/auth.js +1 -97
  29. package/dist/routes/auth.js.map +1 -1
  30. package/dist/routes/configure-fastify.js +2 -1
  31. package/dist/routes/configure-fastify.js.map +1 -1
  32. package/dist/routes/endpoints/socket-route.js +1 -8
  33. package/dist/routes/endpoints/socket-route.js.map +1 -1
  34. package/dist/routes/endpoints/sync-stream.js +17 -4
  35. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  36. package/dist/routes/route-register.d.ts +4 -0
  37. package/dist/routes/route-register.js +29 -15
  38. package/dist/routes/route-register.js.map +1 -1
  39. package/dist/storage/BucketStorageBatch.d.ts +12 -2
  40. package/dist/storage/BucketStorageBatch.js.map +1 -1
  41. package/dist/storage/SourceEntity.d.ts +5 -4
  42. package/dist/storage/SourceTable.d.ts +22 -20
  43. package/dist/storage/SourceTable.js +34 -30
  44. package/dist/storage/SourceTable.js.map +1 -1
  45. package/dist/storage/SyncRulesBucketStorage.d.ts +11 -5
  46. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  47. package/dist/sync/BucketChecksumState.d.ts +1 -1
  48. package/dist/sync/BucketChecksumState.js +1 -1
  49. package/dist/sync/BucketChecksumState.js.map +1 -1
  50. package/dist/sync/util.d.ts +3 -1
  51. package/dist/sync/util.js +29 -1
  52. package/dist/sync/util.js.map +1 -1
  53. package/dist/util/config/compound-config-collector.js +22 -12
  54. package/dist/util/config/compound-config-collector.js.map +1 -1
  55. package/dist/util/config/types.d.ts +0 -12
  56. package/dist/util/lsn.d.ts +4 -0
  57. package/dist/util/lsn.js +11 -0
  58. package/dist/util/lsn.js.map +1 -0
  59. package/dist/util/util-index.d.ts +2 -0
  60. package/dist/util/util-index.js +2 -0
  61. package/dist/util/util-index.js.map +1 -1
  62. package/dist/util/version.d.ts +1 -0
  63. package/dist/util/version.js +3 -0
  64. package/dist/util/version.js.map +1 -0
  65. package/package.json +7 -5
  66. package/src/api/diagnostics.ts +33 -1
  67. package/src/auth/CachedKeyCollector.ts +25 -3
  68. package/src/auth/KeySpec.ts +14 -0
  69. package/src/auth/KeyStore.ts +29 -5
  70. package/src/auth/RemoteJWKSCollector.ts +11 -4
  71. package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
  72. package/src/auth/utils.ts +123 -3
  73. package/src/entry/commands/compact-action.ts +9 -1
  74. package/src/metrics/open-telemetry/util.ts +4 -1
  75. package/src/replication/AbstractReplicator.ts +2 -2
  76. package/src/routes/auth.ts +1 -124
  77. package/src/routes/configure-fastify.ts +3 -1
  78. package/src/routes/endpoints/socket-route.ts +1 -7
  79. package/src/routes/endpoints/sync-stream.ts +29 -21
  80. package/src/routes/route-register.ts +41 -15
  81. package/src/storage/BucketStorageBatch.ts +13 -2
  82. package/src/storage/SourceEntity.ts +5 -5
  83. package/src/storage/SourceTable.ts +48 -34
  84. package/src/storage/SyncRulesBucketStorage.ts +14 -7
  85. package/src/sync/BucketChecksumState.ts +2 -2
  86. package/src/sync/util.ts +31 -2
  87. package/src/util/config/compound-config-collector.ts +23 -15
  88. package/src/util/config/types.ts +0 -11
  89. package/src/util/lsn.ts +8 -0
  90. package/src/util/util-index.ts +2 -0
  91. package/src/util/version.ts +3 -0
  92. package/test/src/auth.test.ts +323 -1
  93. package/test/src/sync/BucketChecksumState.test.ts +36 -35
  94. package/tsconfig.tsbuildinfo +1 -1
@@ -6,7 +6,8 @@ import { KeySpec } from '../../src/auth/KeySpec.js';
6
6
  import { RemoteJWKSCollector } from '../../src/auth/RemoteJWKSCollector.js';
7
7
  import { KeyResult } from '../../src/auth/KeyCollector.js';
8
8
  import { CachedKeyCollector } from '../../src/auth/CachedKeyCollector.js';
9
- import { JwtPayload } from '@/index.js';
9
+ import { JwtPayload, StaticSupabaseKeyCollector } from '@/index.js';
10
+ import { debugKeyNotFound } from '../../src/auth/utils.js';
10
11
 
11
12
  const publicKeyRSA: jose.JWK = {
12
13
  use: 'sig',
@@ -438,4 +439,325 @@ describe('JWT Auth', () => {
438
439
 
439
440
  expect(verified.claim).toEqual('test-claim-2');
440
441
  });
442
+
443
+ describe('debugKeyNotFound', () => {
444
+ test('Supabase token with legacy auth not configured', async () => {
445
+ const keys = await StaticSupabaseKeyCollector.importKeys([]);
446
+ const store = new KeyStore(keys);
447
+
448
+ // Mock Supabase debug info - legacy not enabled
449
+ store.supabaseAuthDebug = {
450
+ jwksDetails: null,
451
+ jwksEnabled: false,
452
+ sharedSecretEnabled: false
453
+ };
454
+
455
+ // Create a legacy Supabase token (HS256)
456
+ const token = await new jose.SignJWT({})
457
+ .setProtectedHeader({ alg: 'HS256', kid: 'test' })
458
+ .setSubject('test')
459
+ .setIssuer('https://abc123.supabase.co/auth/v1')
460
+ .setAudience('authenticated')
461
+ .setExpirationTime('1h')
462
+ .setIssuedAt()
463
+ .sign(Buffer.from('secret'));
464
+
465
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
466
+ expect(err.configurationDetails).toMatch(
467
+ 'Token is a Supabase Legacy HS256 (Shared Secret) token, but Supabase JWT secret is not configured'
468
+ );
469
+ });
470
+
471
+ test('Legacy Supabase token with wrong secret', async () => {
472
+ const keys = await StaticSupabaseKeyCollector.importKeys([sharedKey]);
473
+ const store = new KeyStore(keys);
474
+
475
+ // Mock Supabase debug info - legacy enabled
476
+ store.supabaseAuthDebug = {
477
+ jwksDetails: null,
478
+ jwksEnabled: false,
479
+ sharedSecretEnabled: true
480
+ };
481
+
482
+ // Create a legacy Supabase token (HS256)
483
+ const token = await new jose.SignJWT({})
484
+ .setProtectedHeader({ alg: 'HS256', kid: sharedKey2.kid })
485
+ .setSubject('test')
486
+ .setIssuer('https://abc123.supabase.co/auth/v1')
487
+ .setAudience('authenticated')
488
+ .setExpirationTime('1h')
489
+ .setIssuedAt()
490
+ .sign(await jose.importJWK(sharedKey2));
491
+
492
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
493
+ expect(err.configurationDetails).toMatch(
494
+ 'Token is a Supabase Legacy HS256 (Shared Secret) token, but configured Supabase JWT secret does not match'
495
+ );
496
+ });
497
+
498
+ test('New HS256 Supabase token with wrong secret', async () => {
499
+ const keys = await StaticSupabaseKeyCollector.importKeys([sharedKey]);
500
+ const store = new KeyStore(keys);
501
+
502
+ // Mock Supabase debug info - legacy enabled
503
+ store.supabaseAuthDebug = {
504
+ jwksDetails: null,
505
+ jwksEnabled: false,
506
+ sharedSecretEnabled: true
507
+ };
508
+
509
+ // Create a new HS256 Supabase token.
510
+ // The only real difference here is that the kid is a UUID
511
+ const token = await new jose.SignJWT({})
512
+ .setProtectedHeader({ alg: 'HS256', kid: '2fc01f1d-90fb-4c8b-b646-1c06ed86be46' })
513
+ .setSubject('test')
514
+ .setIssuer('https://abc123.supabase.co/auth/v1')
515
+ .setAudience('authenticated')
516
+ .setExpirationTime('1h')
517
+ .setIssuedAt()
518
+ .sign(await jose.importJWK(sharedKey2));
519
+
520
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
521
+ expect(err.configurationDetails).toMatch(
522
+ 'Token is a Supabase HS256 (Shared Secret) token, but configured Supabase JWT secret does not match'
523
+ );
524
+ });
525
+
526
+ test('Supabase signing key token with no Supabase connection', async () => {
527
+ const keys = await StaticSupabaseKeyCollector.importKeys([]);
528
+ const store = new KeyStore(keys);
529
+
530
+ // Mock Supabase debug info - no Supabase connection
531
+ store.supabaseAuthDebug = {
532
+ jwksDetails: null,
533
+ jwksEnabled: false,
534
+ sharedSecretEnabled: false
535
+ };
536
+
537
+ const signKey = await jose.importJWK(privateKeyECDSA);
538
+ const token = await new jose.SignJWT({})
539
+ .setProtectedHeader({ alg: 'ES256', kid: 'test-kid' })
540
+ .setSubject('test')
541
+ .setIssuer('https://abc123.supabase.co/auth/v1')
542
+ .setAudience('authenticated')
543
+ .setExpirationTime('1h')
544
+ .setIssuedAt()
545
+ .sign(signKey);
546
+
547
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
548
+ expect(err.configurationDetails).toMatch(
549
+ 'Token uses Supabase JWT Signing Keys, but no Supabase connection is configured'
550
+ );
551
+ });
552
+
553
+ test('Supabase signing key token with Supabase auth disabled', async () => {
554
+ const keys = await StaticSupabaseKeyCollector.importKeys([]);
555
+ const store = new KeyStore(keys);
556
+
557
+ // Mock Supabase debug info - Supabase project, but Supabase auth not enabled
558
+ store.supabaseAuthDebug = {
559
+ jwksDetails: {
560
+ projectId: 'abc123',
561
+ hostname: 'db.abc123.supabase.co',
562
+ url: 'https://abc123.supabase.co/auth/v1/.well-known/jwks.json'
563
+ },
564
+ jwksEnabled: false,
565
+ sharedSecretEnabled: false
566
+ };
567
+
568
+ const signKey = await jose.importJWK(privateKeyECDSA);
569
+ const token = await new jose.SignJWT({})
570
+ .setProtectedHeader({ alg: 'ES256', kid: 'test-kid' })
571
+ .setSubject('test')
572
+ .setIssuer('https://abc123.supabase.co/auth/v1')
573
+ .setAudience('authenticated')
574
+ .setExpirationTime('1h')
575
+ .setIssuedAt()
576
+ .sign(signKey);
577
+
578
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
579
+ expect(err.configurationDetails).toMatch(
580
+ 'Token uses Supabase JWT Signing Keys, but Supabase Auth is not enabled'
581
+ );
582
+ });
583
+
584
+ test('Supabase project ID mismatch', async () => {
585
+ const keys = await StaticSupabaseKeyCollector.importKeys([publicKeyRSA]);
586
+ const store = new KeyStore(keys);
587
+
588
+ // Mock Supabase debug info - JWKS enabled with different project ID
589
+ store.supabaseAuthDebug = {
590
+ jwksDetails: {
591
+ projectId: 'expected123',
592
+ hostname: 'db.expected123.supabase.co',
593
+ url: 'https://expected123.supabase.co/auth/v1/.well-known/jwks.json'
594
+ },
595
+ jwksEnabled: true,
596
+ sharedSecretEnabled: false
597
+ };
598
+
599
+ // Create a modern Supabase token with different project ID
600
+ const token = await new jose.SignJWT({})
601
+ .setProtectedHeader({ alg: 'ES256', kid: privateKeyECDSA.kid })
602
+ .setSubject('test')
603
+ .setIssuer('https://different456.supabase.co/auth/v1')
604
+ .setAudience('authenticated')
605
+ .setExpirationTime('1h')
606
+ .setIssuedAt()
607
+ .sign(await jose.importJWK(privateKeyECDSA));
608
+
609
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
610
+ expect(err.configurationDetails).toMatch(
611
+ 'Supabase project id mismatch. Expected project: expected123, got issuer: https://different456.supabase.co/auth/v1'
612
+ );
613
+ });
614
+
615
+ test('Supabase signing keys configured but no matching keys', async () => {
616
+ const keys = await StaticSupabaseKeyCollector.importKeys([publicKeyRSA]);
617
+ const store = new KeyStore(keys);
618
+
619
+ // Mock Supabase debug info - JWKS enabled with matching project ID
620
+ store.supabaseAuthDebug = {
621
+ jwksDetails: {
622
+ projectId: 'abc123',
623
+ hostname: 'db.abc123.supabase.co',
624
+ url: 'https://abc123.supabase.co/auth/v1/.well-known/jwks.json'
625
+ },
626
+ jwksEnabled: true,
627
+ sharedSecretEnabled: false
628
+ };
629
+
630
+ // Create a modern Supabase token with matching project ID
631
+ const token = await new jose.SignJWT({})
632
+ .setProtectedHeader({ alg: 'ES256', kid: privateKeyECDSA.kid })
633
+ .setSubject('test')
634
+ .setIssuer('https://abc123.supabase.co/auth/v1')
635
+ .setAudience('authenticated')
636
+ .setExpirationTime('1h')
637
+ .setIssuedAt()
638
+ .sign(await jose.importJWK(privateKeyECDSA));
639
+
640
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
641
+ expect(err.configurationDetails).toMatch(
642
+ 'Supabase signing keys configured, but no matching keys found. Known keys: '
643
+ );
644
+ });
645
+
646
+ test('non-Supabase token', async () => {
647
+ const keys = await StaticSupabaseKeyCollector.importKeys([sharedKey]);
648
+ const store = new KeyStore(keys);
649
+
650
+ // Create a regular JWT token (not Supabase)
651
+ const token = await new jose.SignJWT({})
652
+ .setProtectedHeader({ alg: 'ES256', kid: privateKeyECDSA.kid })
653
+ .setSubject('test')
654
+ .setIssuer('https://regular-issuer.com')
655
+ .setAudience('my-audience')
656
+ .setExpirationTime('1h')
657
+ .setIssuedAt()
658
+ .sign(await jose.importJWK(privateKeyECDSA));
659
+
660
+ // Treated as just a generic unknown key
661
+ const err = await store.verifyJwt(token, { defaultAudiences: ['my-audience'], maxAge: '1d' }).catch((e) => e);
662
+ expect(err.configurationDetails).toMatch('Known keys:');
663
+ });
664
+
665
+ test('Valid legacy Supabase token', async () => {
666
+ const keys = await StaticSupabaseKeyCollector.importKeys([sharedKey]);
667
+ const store = new KeyStore(keys);
668
+
669
+ // Mock Supabase debug info - legacy enabled
670
+ store.supabaseAuthDebug = {
671
+ jwksDetails: null,
672
+ jwksEnabled: false,
673
+ sharedSecretEnabled: true
674
+ };
675
+
676
+ // Create a legacy Supabase token (HS256)
677
+ const token = await new jose.SignJWT({})
678
+ .setProtectedHeader({ alg: 'HS256', kid: sharedKey.kid })
679
+ .setSubject('test')
680
+ .setIssuer('https://abc123.supabase.co/auth/v1')
681
+ .setAudience('authenticated')
682
+ .setExpirationTime('1h')
683
+ .setIssuedAt()
684
+ .sign(await jose.importJWK(sharedKey));
685
+
686
+ await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' });
687
+ });
688
+
689
+ test('Valid Supabase signing key', async () => {
690
+ const keys = await StaticSupabaseKeyCollector.importKeys([privateKeyECDSA]);
691
+ const store = new KeyStore(keys);
692
+
693
+ // Mock Supabase debug info - JWKS enabled, legacy disabled
694
+ store.supabaseAuthDebug = {
695
+ jwksDetails: null,
696
+ jwksEnabled: true,
697
+ sharedSecretEnabled: false
698
+ };
699
+
700
+ // Create a modern Supabase signing key token (ES256)
701
+ const token = await new jose.SignJWT({})
702
+ .setProtectedHeader({ alg: 'ES256', kid: privateKeyECDSA.kid })
703
+ .setSubject('test')
704
+ .setIssuer('https://abc123.supabase.co/auth/v1')
705
+ .setAudience('authenticated')
706
+ .setExpirationTime('1h')
707
+ .setIssuedAt()
708
+ .sign(await jose.importJWK(privateKeyECDSA));
709
+
710
+ await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' });
711
+ });
712
+
713
+ test('Legacy Supabase anon token', async () => {
714
+ const keys = await StaticSupabaseKeyCollector.importKeys([sharedKey]);
715
+ const store = new KeyStore(keys);
716
+
717
+ // Mock Supabase debug info - legacy enabled
718
+ store.supabaseAuthDebug = {
719
+ jwksDetails: null,
720
+ jwksEnabled: false,
721
+ sharedSecretEnabled: true
722
+ };
723
+
724
+ // Create a legacy Supabase token (HS256)
725
+ const token = await new jose.SignJWT({})
726
+ .setProtectedHeader({ alg: 'HS256', kid: sharedKey.kid })
727
+ .setSubject('test')
728
+ .setIssuer('https://abc123.supabase.co/auth/v1')
729
+ .setAudience('anon')
730
+ .setExpirationTime('1h')
731
+ .setIssuedAt()
732
+ .sign(await jose.importJWK(sharedKey));
733
+
734
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
735
+ expect(err.message).toMatch('[PSYNC_S2105] Unexpected "aud" claim value: "anon"');
736
+ });
737
+
738
+ test('Supabase signing key anon token', async () => {
739
+ const keys = await StaticSupabaseKeyCollector.importKeys([privateKeyECDSA]);
740
+ const store = new KeyStore(keys);
741
+
742
+ // Mock Supabase debug info - JWKS enabled
743
+ store.supabaseAuthDebug = {
744
+ jwksDetails: null,
745
+ jwksEnabled: true,
746
+ sharedSecretEnabled: false
747
+ };
748
+
749
+ // Create a modern Supabase signing key token (ES256)
750
+ const token = await new jose.SignJWT({})
751
+ .setProtectedHeader({ alg: 'ES256', kid: privateKeyECDSA.kid })
752
+ .setSubject('test')
753
+ .setIssuer('https://abc123.supabase.co/auth/v1')
754
+ .setAudience('anon')
755
+ .setExpirationTime('1h')
756
+ .setIssuedAt()
757
+ .sign(await jose.importJWK(privateKeyECDSA));
758
+
759
+ const err = await store.verifyJwt(token, { defaultAudiences: [], maxAge: '1d' }).catch((e) => e);
760
+ expect(err.message).toMatch('[PSYNC_S2105] Unexpected "aud" claim value: "anon"');
761
+ });
762
+ });
441
763
  });
@@ -5,11 +5,12 @@ import {
5
5
  CHECKPOINT_INVALIDATE_ALL,
6
6
  ChecksumMap,
7
7
  InternalOpId,
8
+ ReplicationCheckpoint,
8
9
  SyncContext,
9
10
  WatchFilterEvent
10
11
  } from '@/index.js';
11
12
  import { JSONBig } from '@powersync/service-jsonbig';
12
- import { RequestParameters, SqliteJsonRow, ParameterLookup, SqlSyncRules } from '@powersync/service-sync-rules';
13
+ import { ParameterLookup, RequestParameters, SqliteJsonRow, SqlSyncRules } from '@powersync/service-sync-rules';
13
14
  import { describe, expect, test } from 'vitest';
14
15
 
15
16
  describe('BucketChecksumState', () => {
@@ -67,7 +68,7 @@ bucket_definitions:
67
68
  });
68
69
 
69
70
  const line = (await state.buildNextCheckpointLine({
70
- base: { checkpoint: 1n, lsn: '1' },
71
+ base: storage.makeCheckpoint(1n),
71
72
  writeCheckpoint: null,
72
73
  update: CHECKPOINT_INVALIDATE_ALL
73
74
  }))!;
@@ -97,7 +98,7 @@ bucket_definitions:
97
98
 
98
99
  // Now we get a new line
99
100
  const line2 = (await state.buildNextCheckpointLine({
100
- base: { checkpoint: 2n, lsn: '2' },
101
+ base: storage.makeCheckpoint(2n),
101
102
  writeCheckpoint: null,
102
103
  update: {
103
104
  updatedDataBuckets: new Set(['global[]']),
@@ -136,7 +137,7 @@ bucket_definitions:
136
137
  });
137
138
 
138
139
  const line = (await state.buildNextCheckpointLine({
139
- base: { checkpoint: 1n, lsn: '1' },
140
+ base: storage.makeCheckpoint(1n),
140
141
  writeCheckpoint: null,
141
142
  update: CHECKPOINT_INVALIDATE_ALL
142
143
  }))!;
@@ -172,7 +173,7 @@ bucket_definitions:
172
173
  });
173
174
 
174
175
  const line = (await state.buildNextCheckpointLine({
175
- base: { checkpoint: 1n, lsn: '1' },
176
+ base: storage.makeCheckpoint(1n),
176
177
  writeCheckpoint: null,
177
178
  update: CHECKPOINT_INVALIDATE_ALL
178
179
  }))!;
@@ -202,7 +203,7 @@ bucket_definitions:
202
203
  storage.updateTestChecksum({ bucket: 'global[2]', checksum: 2, count: 2 });
203
204
 
204
205
  const line2 = (await state.buildNextCheckpointLine({
205
- base: { checkpoint: 2n, lsn: '2' },
206
+ base: storage.makeCheckpoint(2n),
206
207
  writeCheckpoint: null,
207
208
  update: {
208
209
  ...CHECKPOINT_INVALIDATE_ALL,
@@ -241,7 +242,7 @@ bucket_definitions:
241
242
  storage.updateTestChecksum({ bucket: 'global[]', checksum: 1, count: 1 });
242
243
 
243
244
  const line = (await state.buildNextCheckpointLine({
244
- base: { checkpoint: 1n, lsn: '1' },
245
+ base: storage.makeCheckpoint(1n),
245
246
  writeCheckpoint: null,
246
247
  update: CHECKPOINT_INVALIDATE_ALL
247
248
  }))!;
@@ -281,7 +282,7 @@ bucket_definitions:
281
282
  // storage.filter = state.checkpointFilter;
282
283
 
283
284
  const line = await state.buildNextCheckpointLine({
284
- base: { checkpoint: 1n, lsn: '1' },
285
+ base: storage.makeCheckpoint(1n),
285
286
  writeCheckpoint: null,
286
287
  update: CHECKPOINT_INVALIDATE_ALL
287
288
  });
@@ -293,7 +294,7 @@ bucket_definitions:
293
294
  storage.updateTestChecksum({ bucket: 'global[2]', checksum: 2, count: 2 });
294
295
 
295
296
  const line2 = (await state.buildNextCheckpointLine({
296
- base: { checkpoint: 2n, lsn: '2' },
297
+ base: storage.makeCheckpoint(2n),
297
298
  writeCheckpoint: null,
298
299
  update: {
299
300
  ...CHECKPOINT_INVALIDATE_ALL,
@@ -337,7 +338,7 @@ bucket_definitions:
337
338
  storage.updateTestChecksum({ bucket: 'global[2]', checksum: 1, count: 1 });
338
339
 
339
340
  const line = await state.buildNextCheckpointLine({
340
- base: { checkpoint: 1n, lsn: '1' },
341
+ base: storage.makeCheckpoint(1n),
341
342
  writeCheckpoint: null,
342
343
  update: CHECKPOINT_INVALIDATE_ALL
343
344
  });
@@ -348,7 +349,7 @@ bucket_definitions:
348
349
  storage.updateTestChecksum({ bucket: 'global[2]', checksum: 2, count: 2 });
349
350
 
350
351
  const line2 = (await state.buildNextCheckpointLine({
351
- base: { checkpoint: 2n, lsn: '2' },
352
+ base: storage.makeCheckpoint(2n),
352
353
  writeCheckpoint: null,
353
354
  // Invalidate the state - will re-check all buckets
354
355
  update: CHECKPOINT_INVALIDATE_ALL
@@ -384,7 +385,7 @@ bucket_definitions:
384
385
  });
385
386
 
386
387
  const line = (await state.buildNextCheckpointLine({
387
- base: { checkpoint: 3n, lsn: '3' },
388
+ base: storage.makeCheckpoint(3n),
388
389
  writeCheckpoint: null,
389
390
  update: CHECKPOINT_INVALIDATE_ALL
390
391
  }))!;
@@ -426,7 +427,7 @@ bucket_definitions:
426
427
  storage.updateTestChecksum({ bucket: 'global[1]', checksum: 4, count: 4 });
427
428
 
428
429
  const line2 = (await state.buildNextCheckpointLine({
429
- base: { checkpoint: 4n, lsn: '4' },
430
+ base: storage.makeCheckpoint(4n),
430
431
  writeCheckpoint: null,
431
432
  update: {
432
433
  ...CHECKPOINT_INVALIDATE_ALL,
@@ -484,17 +485,11 @@ bucket_definitions:
484
485
  bucketStorage: storage
485
486
  });
486
487
 
487
- storage.getParameterSets = async (
488
- checkpoint: InternalOpId,
489
- lookups: ParameterLookup[]
490
- ): Promise<SqliteJsonRow[]> => {
491
- expect(checkpoint).toEqual(1n);
492
- expect(lookups).toEqual([ParameterLookup.normalized('by_project', '1', ['u1'])]);
493
- return [{ id: 1 }, { id: 2 }];
494
- };
495
-
496
488
  const line = (await state.buildNextCheckpointLine({
497
- base: { checkpoint: 1n, lsn: '1' },
489
+ base: storage.makeCheckpoint(1n, (lookups) => {
490
+ expect(lookups).toEqual([ParameterLookup.normalized('by_project', '1', ['u1'])]);
491
+ return [{ id: 1 }, { id: 2 }];
492
+ }),
498
493
  writeCheckpoint: null,
499
494
  update: CHECKPOINT_INVALIDATE_ALL
500
495
  }))!;
@@ -531,18 +526,12 @@ bucket_definitions:
531
526
  line.updateBucketPosition({ bucket: 'by_project[1]', nextAfter: 1n, hasMore: false });
532
527
  line.updateBucketPosition({ bucket: 'by_project[2]', nextAfter: 1n, hasMore: false });
533
528
 
534
- storage.getParameterSets = async (
535
- checkpoint: InternalOpId,
536
- lookups: ParameterLookup[]
537
- ): Promise<SqliteJsonRow[]> => {
538
- expect(checkpoint).toEqual(2n);
539
- expect(lookups).toEqual([ParameterLookup.normalized('by_project', '1', ['u1'])]);
540
- return [{ id: 1 }, { id: 2 }, { id: 3 }];
541
- };
542
-
543
529
  // Now we get a new line
544
530
  const line2 = (await state.buildNextCheckpointLine({
545
- base: { checkpoint: 2n, lsn: '2' },
531
+ base: storage.makeCheckpoint(2n, (lookups) => {
532
+ expect(lookups).toEqual([ParameterLookup.normalized('by_project', '1', ['u1'])]);
533
+ return [{ id: 1 }, { id: 2 }, { id: 3 }];
534
+ }),
546
535
  writeCheckpoint: null,
547
536
  update: {
548
537
  invalidateDataBuckets: false,
@@ -595,7 +584,19 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
595
584
  );
596
585
  }
597
586
 
598
- async getParameterSets(checkpoint: InternalOpId, lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
599
- throw new Error('Method not implemented.');
587
+ makeCheckpoint(
588
+ opId: InternalOpId,
589
+ parameters?: (lookups: ParameterLookup[]) => SqliteJsonRow[]
590
+ ): ReplicationCheckpoint {
591
+ return {
592
+ checkpoint: opId,
593
+ lsn: String(opId),
594
+ getParameterSets: async (lookups: ParameterLookup[]) => {
595
+ if (parameters == null) {
596
+ throw new Error(`getParametersSets not defined for checkpoint ${opId}`);
597
+ }
598
+ return parameters(lookups);
599
+ }
600
+ };
600
601
  }
601
602
  }