@powersync/service-core 0.0.0-dev-20250724093011 → 0.0.0-dev-20250729101933
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 +7 -4
- package/dist/api/diagnostics.js +2 -2
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/KeyStore.d.ts +19 -0
- package/dist/auth/KeyStore.js +16 -4
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.d.ts +3 -0
- package/dist/auth/RemoteJWKSCollector.js +3 -1
- 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/emitters/EmitterEngine.js +0 -3
- package/dist/emitters/EmitterEngine.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +12 -2
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +0 -1
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +23 -0
- package/dist/util/config/compound-config-collector.js.map +1 -1
- 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 +1 -0
- package/dist/util/util-index.js +1 -0
- package/dist/util/util-index.js.map +1 -1
- package/package.json +4 -4
- package/src/api/diagnostics.ts +2 -2
- package/src/auth/KeyStore.ts +28 -4
- package/src/auth/RemoteJWKSCollector.ts +5 -2
- package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
- package/src/auth/utils.ts +123 -3
- package/src/emitters/EmitterEngine.ts +0 -3
- package/src/storage/BucketStorageBatch.ts +13 -2
- package/src/storage/SyncRulesBucketStorage.ts +0 -2
- package/src/util/config/compound-config-collector.ts +24 -0
- package/src/util/lsn.ts +8 -0
- package/src/util/util-index.ts +1 -0
- package/test/src/auth.test.ts +323 -1
- 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
|
});
|