@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/dist/api/diagnostics.js +2 -2
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/auth/KeyStore.d.ts +19 -0
  5. package/dist/auth/KeyStore.js +16 -4
  6. package/dist/auth/KeyStore.js.map +1 -1
  7. package/dist/auth/RemoteJWKSCollector.d.ts +3 -0
  8. package/dist/auth/RemoteJWKSCollector.js +3 -1
  9. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  10. package/dist/auth/StaticSupabaseKeyCollector.d.ts +2 -1
  11. package/dist/auth/StaticSupabaseKeyCollector.js +1 -1
  12. package/dist/auth/StaticSupabaseKeyCollector.js.map +1 -1
  13. package/dist/auth/utils.d.ts +19 -0
  14. package/dist/auth/utils.js +106 -3
  15. package/dist/auth/utils.js.map +1 -1
  16. package/dist/emitters/EmitterEngine.js +0 -3
  17. package/dist/emitters/EmitterEngine.js.map +1 -1
  18. package/dist/storage/BucketStorageBatch.d.ts +12 -2
  19. package/dist/storage/BucketStorageBatch.js.map +1 -1
  20. package/dist/storage/SyncRulesBucketStorage.d.ts +0 -1
  21. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  22. package/dist/util/config/compound-config-collector.js +23 -0
  23. package/dist/util/config/compound-config-collector.js.map +1 -1
  24. package/dist/util/lsn.d.ts +4 -0
  25. package/dist/util/lsn.js +11 -0
  26. package/dist/util/lsn.js.map +1 -0
  27. package/dist/util/util-index.d.ts +1 -0
  28. package/dist/util/util-index.js +1 -0
  29. package/dist/util/util-index.js.map +1 -1
  30. package/package.json +4 -4
  31. package/src/api/diagnostics.ts +2 -2
  32. package/src/auth/KeyStore.ts +28 -4
  33. package/src/auth/RemoteJWKSCollector.ts +5 -2
  34. package/src/auth/StaticSupabaseKeyCollector.ts +1 -1
  35. package/src/auth/utils.ts +123 -3
  36. package/src/emitters/EmitterEngine.ts +0 -3
  37. package/src/storage/BucketStorageBatch.ts +13 -2
  38. package/src/storage/SyncRulesBucketStorage.ts +0 -2
  39. package/src/util/config/compound-config-collector.ts +24 -0
  40. package/src/util/lsn.ts +8 -0
  41. package/src/util/util-index.ts +1 -0
  42. package/test/src/auth.test.ts +323 -1
  43. 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
  });