@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.
- package/CHANGELOG.md +43 -0
- package/dist/api/diagnostics.js +31 -1
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/CachedKeyCollector.js +26 -2
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/KeySpec.d.ts +1 -0
- package/dist/auth/KeySpec.js +12 -0
- package/dist/auth/KeySpec.js.map +1 -1
- package/dist/auth/KeyStore.d.ts +19 -0
- package/dist/auth/KeyStore.js +17 -5
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.d.ts +3 -0
- package/dist/auth/RemoteJWKSCollector.js +9 -3
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/StaticSupabaseKeyCollector.d.ts +2 -1
- package/dist/auth/StaticSupabaseKeyCollector.js +1 -1
- package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
- package/dist/auth/utils.d.ts +19 -0
- package/dist/auth/utils.js +106 -3
- package/dist/auth/utils.js.map +1 -1
- package/dist/entry/commands/compact-action.js +10 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/metrics/open-telemetry/util.js +3 -1
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/replication/AbstractReplicator.js +2 -2
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/auth.d.ts +1 -21
- package/dist/routes/auth.js +1 -97
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/configure-fastify.js +2 -1
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +1 -8
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +17 -4
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.d.ts +4 -0
- package/dist/routes/route-register.js +29 -15
- package/dist/routes/route-register.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +12 -2
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/SourceEntity.d.ts +5 -4
- package/dist/storage/SourceTable.d.ts +22 -20
- package/dist/storage/SourceTable.js +34 -30
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +11 -5
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +1 -1
- package/dist/sync/BucketChecksumState.js +1 -1
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/util.d.ts +3 -1
- package/dist/sync/util.js +29 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +22 -12
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +0 -12
- package/dist/util/lsn.d.ts +4 -0
- package/dist/util/lsn.js +11 -0
- package/dist/util/lsn.js.map +1 -0
- package/dist/util/util-index.d.ts +2 -0
- package/dist/util/util-index.js +2 -0
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/version.d.ts +1 -0
- package/dist/util/version.js +3 -0
- package/dist/util/version.js.map +1 -0
- package/package.json +7 -5
- package/src/api/diagnostics.ts +33 -1
- package/src/auth/CachedKeyCollector.ts +25 -3
- package/src/auth/KeySpec.ts +14 -0
- package/src/auth/KeyStore.ts +29 -5
- package/src/auth/RemoteJWKSCollector.ts +11 -4
- package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
- package/src/auth/utils.ts +123 -3
- package/src/entry/commands/compact-action.ts +9 -1
- package/src/metrics/open-telemetry/util.ts +4 -1
- package/src/replication/AbstractReplicator.ts +2 -2
- package/src/routes/auth.ts +1 -124
- package/src/routes/configure-fastify.ts +3 -1
- package/src/routes/endpoints/socket-route.ts +1 -7
- package/src/routes/endpoints/sync-stream.ts +29 -21
- package/src/routes/route-register.ts +41 -15
- package/src/storage/BucketStorageBatch.ts +13 -2
- package/src/storage/SourceEntity.ts +5 -5
- package/src/storage/SourceTable.ts +48 -34
- package/src/storage/SyncRulesBucketStorage.ts +14 -7
- package/src/sync/BucketChecksumState.ts +2 -2
- package/src/sync/util.ts +31 -2
- package/src/util/config/compound-config-collector.ts +23 -15
- package/src/util/config/types.ts +0 -11
- package/src/util/lsn.ts +8 -0
- package/src/util/util-index.ts +2 -0
- package/src/util/version.ts +3 -0
- package/test/src/auth.test.ts +323 -1
- package/test/src/sync/BucketChecksumState.test.ts +36 -35
- package/tsconfig.tsbuildinfo +1 -1
package/test/src/auth.test.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
599
|
-
|
|
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
|
}
|