@powersync/service-core 0.0.0-dev-20250618131818 → 0.0.0-dev-20250714115156

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 (78) hide show
  1. package/CHANGELOG.md +36 -4
  2. package/dist/auth/CachedKeyCollector.js +26 -2
  3. package/dist/auth/CachedKeyCollector.js.map +1 -1
  4. package/dist/auth/KeySpec.d.ts +1 -0
  5. package/dist/auth/KeySpec.js +12 -0
  6. package/dist/auth/KeySpec.js.map +1 -1
  7. package/dist/auth/KeyStore.js +2 -2
  8. package/dist/auth/KeyStore.js.map +1 -1
  9. package/dist/auth/RemoteJWKSCollector.js +6 -2
  10. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  11. package/dist/emitters/AbstractEmitterEngine.d.ts +13 -0
  12. package/dist/emitters/AbstractEmitterEngine.js +57 -0
  13. package/dist/emitters/AbstractEmitterEngine.js.map +1 -0
  14. package/dist/emitters/EmitterEngine.d.ts +12 -0
  15. package/dist/emitters/EmitterEngine.js +35 -0
  16. package/dist/emitters/EmitterEngine.js.map +1 -0
  17. package/dist/emitters/emitter-interfaces.d.ts +37 -0
  18. package/dist/emitters/emitter-interfaces.js +10 -0
  19. package/dist/emitters/emitter-interfaces.js.map +1 -0
  20. package/dist/emitters/event-error.d.ts +5 -0
  21. package/dist/emitters/event-error.js +8 -0
  22. package/dist/emitters/event-error.js.map +1 -0
  23. package/dist/emitters/events/connect-event.d.ts +10 -0
  24. package/dist/emitters/events/connect-event.js +32 -0
  25. package/dist/emitters/events/connect-event.js.map +1 -0
  26. package/dist/emitters/events/disconnect-event.d.ts +10 -0
  27. package/dist/emitters/events/disconnect-event.js +27 -0
  28. package/dist/emitters/events/disconnect-event.js.map +1 -0
  29. package/dist/emitters/events/index.d.ts +1 -0
  30. package/dist/emitters/events/index.js +4 -0
  31. package/dist/emitters/events/index.js.map +1 -0
  32. package/dist/replication/AbstractReplicator.d.ts +8 -2
  33. package/dist/replication/AbstractReplicator.js +25 -9
  34. package/dist/replication/AbstractReplicator.js.map +1 -1
  35. package/dist/routes/auth.d.ts +1 -21
  36. package/dist/routes/auth.js +1 -97
  37. package/dist/routes/auth.js.map +1 -1
  38. package/dist/routes/configure-rsocket.js +4 -2
  39. package/dist/routes/configure-rsocket.js.map +1 -1
  40. package/dist/routes/endpoints/sync-stream.js +23 -1
  41. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  42. package/dist/routes/router.d.ts +1 -2
  43. package/dist/routes/router.js.map +1 -1
  44. package/dist/system/ServiceContext.d.ts +3 -0
  45. package/dist/system/ServiceContext.js +8 -1
  46. package/dist/system/ServiceContext.js.map +1 -1
  47. package/dist/util/config/compound-config-collector.js +0 -13
  48. package/dist/util/config/compound-config-collector.js.map +1 -1
  49. package/dist/util/config/types.d.ts +0 -12
  50. package/dist/util/util-index.d.ts +1 -0
  51. package/dist/util/util-index.js +1 -0
  52. package/dist/util/util-index.js.map +1 -1
  53. package/dist/util/version.d.ts +1 -0
  54. package/dist/util/version.js +3 -0
  55. package/dist/util/version.js.map +1 -0
  56. package/package.json +4 -4
  57. package/src/auth/CachedKeyCollector.ts +25 -3
  58. package/src/auth/KeySpec.ts +14 -0
  59. package/src/auth/KeyStore.ts +2 -2
  60. package/src/auth/RemoteJWKSCollector.ts +6 -2
  61. package/src/emitters/AbstractEmitterEngine.ts +64 -0
  62. package/src/emitters/EmitterEngine.ts +41 -0
  63. package/src/emitters/emitter-interfaces.ts +47 -0
  64. package/src/emitters/event-error.ts +10 -0
  65. package/src/emitters/events/connect-event.ts +34 -0
  66. package/src/emitters/events/disconnect-event.ts +36 -0
  67. package/src/emitters/events/index.ts +4 -0
  68. package/src/replication/AbstractReplicator.ts +30 -10
  69. package/src/routes/auth.ts +1 -124
  70. package/src/routes/configure-rsocket.ts +3 -2
  71. package/src/routes/endpoints/sync-stream.ts +25 -2
  72. package/src/routes/router.ts +0 -1
  73. package/src/system/ServiceContext.ts +11 -1
  74. package/src/util/config/compound-config-collector.ts +0 -16
  75. package/src/util/config/types.ts +0 -11
  76. package/src/util/util-index.ts +1 -0
  77. package/src/util/version.ts +3 -0
  78. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "version": "0.0.0-dev-20250618131818",
8
+ "version": "0.0.0-dev-20250714115156",
9
9
  "main": "dist/index.js",
10
10
  "license": "FSL-1.1-Apache-2.0",
11
11
  "type": "module",
@@ -32,11 +32,11 @@
32
32
  "uuid": "^11.1.0",
33
33
  "winston": "^3.13.0",
34
34
  "yaml": "^2.3.2",
35
- "@powersync/lib-services-framework": "0.0.0-dev-20250618131818",
35
+ "@powersync/lib-services-framework": "0.7.0",
36
36
  "@powersync/service-jsonbig": "0.17.10",
37
- "@powersync/service-rsocket-router": "0.0.0-dev-20250618131818",
37
+ "@powersync/service-rsocket-router": "0.1.1",
38
38
  "@powersync/service-sync-rules": "0.27.0",
39
- "@powersync/service-types": "0.0.0-dev-20250618131818"
39
+ "@powersync/service-types": "0.0.0-dev-20250714115156"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/async": "^3.2.24",
@@ -3,7 +3,7 @@ import timers from 'timers/promises';
3
3
  import { KeySpec } from './KeySpec.js';
4
4
  import { LeakyBucket } from './LeakyBucket.js';
5
5
  import { KeyCollector, KeyResult } from './KeyCollector.js';
6
- import { AuthorizationError } from '@powersync/lib-services-framework';
6
+ import { AuthorizationError, ErrorCode, logger } from '@powersync/lib-services-framework';
7
7
  import { mapAuthConfigError } from './utils.js';
8
8
 
9
9
  /**
@@ -70,8 +70,21 @@ export class CachedKeyCollector implements KeyCollector {
70
70
  // e.g. in the case of waiting for error retries.
71
71
  // In the case of very slow requests, we don't wait for it to complete, but the
72
72
  // request can still complete in the background.
73
- const timeout = timers.setTimeout(3000);
74
- await Promise.race([this.refreshPromise, timeout]);
73
+ const WAIT_TIMEOUT_SECONDS = 3;
74
+ const timeout = timers.setTimeout(WAIT_TIMEOUT_SECONDS * 1000).then(() => {
75
+ throw new AuthorizationError(ErrorCode.PSYNC_S2204, `JWKS request failed`, {
76
+ cause: { message: `Key request timed out in ${WAIT_TIMEOUT_SECONDS}s`, name: 'AbortError' }
77
+ });
78
+ });
79
+ try {
80
+ await Promise.race([this.refreshPromise, timeout]);
81
+ } catch (e) {
82
+ if (e instanceof AuthorizationError) {
83
+ return { keys: this.currentKeys, errors: [...this.currentErrors, e] };
84
+ } else {
85
+ throw e;
86
+ }
87
+ }
75
88
  }
76
89
 
77
90
  return { keys: this.currentKeys, errors: this.currentErrors };
@@ -102,7 +115,16 @@ export class CachedKeyCollector implements KeyCollector {
102
115
  this.currentErrors = errors;
103
116
  this.keyTimestamp = Date.now();
104
117
  this.error = false;
118
+
119
+ // Due to caching and background refresh behavior, errors are not always propagated to the request handler,
120
+ // so we log them here.
121
+ for (let error of errors) {
122
+ logger.error(`Soft key refresh error`, error);
123
+ }
105
124
  } catch (e) {
125
+ // Due to caching and background refresh behavior, errors are not always propagated to the request handler,
126
+ // so we log them here.
127
+ logger.error(`Hard key refresh error`, e);
106
128
  this.error = true;
107
129
  // No result - keep previous keys
108
130
  this.currentErrors = [mapAuthConfigError(e)];
@@ -40,6 +40,20 @@ export class KeySpec {
40
40
  return this.source.kid;
41
41
  }
42
42
 
43
+ get description(): string {
44
+ let details: string[] = [];
45
+ details.push(`kid: ${this.kid ?? '*'}`);
46
+ details.push(`kty: ${this.source.kty}`);
47
+ if (this.source.alg != null) {
48
+ details.push(`alg: ${this.source.alg}`);
49
+ }
50
+ if (this.options.requiresAudience != null) {
51
+ details.push(`aud: ${this.options.requiresAudience.join(', ')}`);
52
+ }
53
+
54
+ return `<${details.filter((x) => x != null).join(', ')}>`;
55
+ }
56
+
43
57
  matchesAlgorithm(jwtAlg: string): boolean {
44
58
  if (this.source.alg) {
45
59
  return jwtAlg === this.source.alg;
@@ -1,4 +1,4 @@
1
- import { logger, errors, AuthorizationError, ErrorCode } from '@powersync/lib-services-framework';
1
+ import { AuthorizationError, ErrorCode, logger } from '@powersync/lib-services-framework';
2
2
  import * as jose from 'jose';
3
3
  import secs from '../util/secs.js';
4
4
  import { JwtPayload } from './JwtPayload.js';
@@ -169,7 +169,7 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
169
169
  ErrorCode.PSYNC_S2101,
170
170
  'Could not find an appropriate key in the keystore. The key is missing or no key matched the token KID',
171
171
  {
172
- configurationDetails: `Known kid values: ${keys.map((key) => key.kid ?? '*').join(', ')}`
172
+ configurationDetails: `Known keys: ${keys.map((key) => key.description).join(', ')}`
173
173
  // tokenDetails automatically populated later
174
174
  }
175
175
  );
@@ -49,9 +49,10 @@ export class RemoteJWKSCollector implements KeyCollector {
49
49
 
50
50
  private async getJwksData(): Promise<any> {
51
51
  const abortController = new AbortController();
52
+ const REQUEST_TIMEOUT_SECONDS = 30;
52
53
  const timeout = setTimeout(() => {
53
54
  abortController.abort();
54
- }, 30_000);
55
+ }, REQUEST_TIMEOUT_SECONDS * 1000);
55
56
 
56
57
  try {
57
58
  const res = await fetch(this.url, {
@@ -71,11 +72,14 @@ export class RemoteJWKSCollector implements KeyCollector {
71
72
 
72
73
  return (await res.json()) as any;
73
74
  } catch (e) {
75
+ if (e instanceof Error && e.name === 'AbortError') {
76
+ e = { message: `Request timed out in ${REQUEST_TIMEOUT_SECONDS}s`, name: 'AbortError' };
77
+ }
74
78
  throw new AuthorizationError(ErrorCode.PSYNC_S2204, `JWKS request failed`, {
75
79
  configurationDetails: `JWKS URL: ${this.url}`,
76
80
  // This covers most cases of FetchError
77
81
  // `cause: e` could lose the error message
78
- cause: { message: e.message, code: e.code }
82
+ cause: { message: e.message, code: e.code, name: e.name }
79
83
  });
80
84
  } finally {
81
85
  clearTimeout(timeout);
@@ -0,0 +1,64 @@
1
+ import { EmitterEvent, EmitterEventData, EventNames, isEventError } from './emitter-interfaces.js';
2
+ import * as storage from '../storage/storage-index.js';
3
+ import EventEmitter from 'node:events';
4
+ import { logger } from '@powersync/lib-services-framework';
5
+ import { EventError } from './event-error.js';
6
+
7
+ export abstract class AbstractEmitterEngine {
8
+ private emitter: EventEmitter;
9
+ storageEngine: storage.StorageEngine;
10
+ eventsMap: Map<EventNames, EmitterEvent> = new Map();
11
+
12
+ protected constructor(storage: storage.StorageEngine) {
13
+ this.emitter = new EventEmitter({ captureRejections: true });
14
+ this.storageEngine = storage;
15
+ this.emitter.on('error', (error: Error | EventError) => {
16
+ if (isEventError(error) && this.eventsMap.has(error.eventName)) {
17
+ const event = this.eventsMap.get(error.eventName)!;
18
+ const errorHandler =
19
+ event.errorhandler ??
20
+ ((error) => {
21
+ logger.error(error.message);
22
+ });
23
+ errorHandler(error);
24
+ }
25
+ });
26
+ }
27
+
28
+ get events(): EventNames[] {
29
+ return this.emitter.eventNames() as EventNames[];
30
+ }
31
+
32
+ protected bindEvents(events: EmitterEvent[]): void {
33
+ const eventNames = this.emitter.eventNames();
34
+ for (const event of events) {
35
+ if (!eventNames.includes(event.name)) {
36
+ this.eventsMap.set(event.name, event);
37
+ this.emitter.on(event.name, event.handler(this.storageEngine).bind(event));
38
+ } else {
39
+ logger.warn(`Event ${event.name} is already registered. Skipping.`);
40
+ }
41
+ }
42
+ }
43
+
44
+ protected emit(eventName: EventNames, data: EmitterEventData): void {
45
+ if (!this.emitter.eventNames().includes(eventName)) {
46
+ throw new Error(`Event ${eventName} is not registered.`);
47
+ }
48
+ this.emitter.emit(eventName, data);
49
+ }
50
+
51
+ protected removeListeners(eventName?: EventNames): void {
52
+ if (eventName) {
53
+ this.emitter.removeAllListeners(eventName);
54
+ logger.info(`Removed all listeners for event: ${eventName}`);
55
+ } else {
56
+ this.stop();
57
+ }
58
+ }
59
+
60
+ protected stop(): void {
61
+ this.emitter.removeAllListeners();
62
+ logger.info('Emitter engine shut down and all listeners removed.');
63
+ }
64
+ }
@@ -0,0 +1,41 @@
1
+ import { BaseEmitterEngine, EmitterEvent, EmitterEventData, EventNames } from './emitter-interfaces.js';
2
+ import * as storage from '../storage/storage-index.js';
3
+ import { AbstractEmitterEngine } from './AbstractEmitterEngine.js';
4
+ import { logger } from '@powersync/lib-services-framework';
5
+
6
+ export class EmitterEngine extends AbstractEmitterEngine implements BaseEmitterEngine {
7
+ private active: boolean;
8
+ constructor(events: EmitterEvent[], storageRef: storage.StorageEngine) {
9
+ super(storageRef);
10
+ this.active = process.env.MICRO_SERVICE_NAME === 'powersync';
11
+ logger.info(`EmitterEngine initialized with active status: ${this.active}`);
12
+ this.bindEvents(events);
13
+ }
14
+
15
+ event(eventName: EventNames): EmitterEvent {
16
+ const event = this.eventsMap.get(eventName);
17
+ if (!event) {
18
+ throw new Error(`Event ${eventName} is not registered.`);
19
+ }
20
+ return event;
21
+ }
22
+
23
+ removeListeners(eventName?: EventNames): void {
24
+ if (eventName) {
25
+ this.removeListeners(eventName);
26
+ }
27
+ }
28
+ eventNames(): EventNames[] {
29
+ return this.events;
30
+ }
31
+
32
+ emitEvent(eventName: EventNames, data: EmitterEventData): void {
33
+ if (this.active) {
34
+ return this.emit(eventName, data);
35
+ }
36
+ }
37
+
38
+ async shutDown(): Promise<void> {
39
+ this.stop();
40
+ }
41
+ }
@@ -0,0 +1,47 @@
1
+ import { JwtPayload } from '../auth/JwtPayload.js';
2
+ import * as storage from '../storage/storage-index.js';
3
+ import { EventError } from './event-error.js';
4
+
5
+ export enum EventNames {
6
+ // SdK events
7
+ SDK_CONNECT_EVENT = 'sdk-connect-event',
8
+ SDK_DISCONNECT_EVENT = 'sdk-disconnect-event'
9
+ }
10
+
11
+ export type SdkEvent = {
12
+ client_id?: string;
13
+ user_id: string;
14
+ user_agent: string;
15
+ jwt_token?: JwtPayload;
16
+ };
17
+
18
+ export type EventConnectData = {
19
+ type: EventNames.SDK_CONNECT_EVENT;
20
+ connect_at: number;
21
+ } & SdkEvent;
22
+
23
+ export type EventDisconnectData = {
24
+ type: EventNames.SDK_DISCONNECT_EVENT;
25
+ disconnect_at: number;
26
+ } & SdkEvent;
27
+
28
+ export type EmitterEventData = EventConnectData | EventDisconnectData;
29
+ export type EventHandler = (data: EmitterEventData) => Promise<void>;
30
+ export type StorageHandler = (storageEngine: storage.StorageEngine) => EventHandler;
31
+ export interface EmitterEvent {
32
+ name: EventNames;
33
+ handler: StorageHandler;
34
+ errorhandler?: (error: Error | EventError) => void;
35
+ }
36
+
37
+ export interface BaseEmitterEngine {
38
+ eventNames(): EventNames[];
39
+ emitEvent(eventName: EventNames, data: EmitterEventData): void;
40
+ removeListeners(eventName?: EventNames): void;
41
+ event(eventName: EventNames): EmitterEvent;
42
+ shutDown(): Promise<void>;
43
+ }
44
+
45
+ export function isEventError(error: Error | EventError): error is EventError {
46
+ return (error as EventError).eventName !== undefined;
47
+ }
@@ -0,0 +1,10 @@
1
+ import { EventNames } from './emitter-interfaces.js';
2
+
3
+ export class EventError extends Error {
4
+ constructor(
5
+ public eventName: EventNames,
6
+ message: string
7
+ ) {
8
+ super(message);
9
+ }
10
+ }
@@ -0,0 +1,34 @@
1
+ import { EmitterEvent, EmitterEventData, EventConnectData, EventHandler, EventNames } from '../emitter-interfaces.js';
2
+ import * as storage from '../../storage/storage-index.js';
3
+ import { EventError } from '../event-error.js';
4
+ import { logger } from '@powersync/lib-services-framework';
5
+
6
+ export class ConnectEvent implements EmitterEvent {
7
+ private type = EventNames.SDK_CONNECT_EVENT;
8
+ get name(): EventNames {
9
+ return this.type;
10
+ }
11
+ errorhandler(error: Error | EventError): void {
12
+ if (error instanceof EventError) {
13
+ logger.error(`EventError in ${error.eventName}:`, error.message);
14
+ } else {
15
+ logger.error(error.message);
16
+ }
17
+ }
18
+ handler(storageEngine: storage.StorageEngine): EventHandler {
19
+ // TODO: USE STORAGE ENGINE
20
+ const storage = storageEngine.activeStorage.storage;
21
+ return async (data: EmitterEventData) => {
22
+ try {
23
+ const disconnectData = data as EventConnectData;
24
+ console.log(
25
+ `Connect event triggered for user: ${disconnectData.user_id} at ${new Date(disconnectData.connect_at).toISOString()}`
26
+ );
27
+ } catch (error) {
28
+ throw new EventError(this.type, error.message);
29
+ }
30
+ };
31
+ }
32
+ }
33
+
34
+ export const connectEvent = new ConnectEvent();
@@ -0,0 +1,36 @@
1
+ import {
2
+ EmitterEvent,
3
+ EmitterEventData,
4
+ EventDisconnectData,
5
+ EventHandler,
6
+ EventNames
7
+ } from '../emitter-interfaces.js';
8
+ import * as storage from '../../storage/storage-index.js';
9
+ import { EventError } from '../event-error.js';
10
+ import { logger } from '@powersync/lib-services-framework';
11
+
12
+ export class DisconnectEvent implements EmitterEvent {
13
+ private type = EventNames.SDK_DISCONNECT_EVENT;
14
+ get name(): EventNames {
15
+ return this.type;
16
+ }
17
+ errorhandler(error: Error | EventError): void {
18
+ if (error instanceof EventError) {
19
+ logger.error(`EventError in ${error.eventName}:`, error.message);
20
+ } else {
21
+ logger.error(error.message);
22
+ }
23
+ }
24
+ handler(storageEngine: storage.StorageEngine): EventHandler {
25
+ // TODO: USE STORAGE ENGINE
26
+ const storage = storageEngine.activeStorage.storage;
27
+ return async (data: EmitterEventData) => {
28
+ const disconnectData = data as EventDisconnectData;
29
+ console.log(
30
+ `Disconnect event triggered for user: ${disconnectData.user_id} at ${new Date(disconnectData.disconnect_at).toISOString()}`
31
+ );
32
+ };
33
+ }
34
+ }
35
+
36
+ export const disconnectEvent = new DisconnectEvent();
@@ -0,0 +1,4 @@
1
+ import { disconnectEvent } from './disconnect-event.js';
2
+ import { connectEvent } from './connect-event.js';
3
+
4
+ export const events = [disconnectEvent, connectEvent];
@@ -1,14 +1,14 @@
1
1
  import { container, logger } from '@powersync/lib-services-framework';
2
+ import { ReplicationMetric } from '@powersync/service-types';
2
3
  import { hrtime } from 'node:process';
3
4
  import winston from 'winston';
5
+ import { MetricsEngine } from '../metrics/MetricsEngine.js';
4
6
  import * as storage from '../storage/storage-index.js';
5
7
  import { StorageEngine } from '../storage/storage-index.js';
6
8
  import { SyncRulesProvider } from '../util/config/sync-rules/sync-rules-provider.js';
7
9
  import { AbstractReplicationJob } from './AbstractReplicationJob.js';
8
10
  import { ErrorRateLimiter } from './ErrorRateLimiter.js';
9
11
  import { ConnectionTestResult } from './ReplicationModule.js';
10
- import { MetricsEngine } from '../metrics/MetricsEngine.js';
11
- import { ReplicationMetric } from '@powersync/service-types';
12
12
 
13
13
  // 5 minutes
14
14
  const PING_INTERVAL = 1_000_000_000n * 300n;
@@ -40,9 +40,17 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
40
40
  /**
41
41
  * Map of replication jobs by sync rule id. Usually there is only one running job, but there could be two when
42
42
  * transitioning to a new set of sync rules.
43
- * @private
44
43
  */
45
44
  private replicationJobs = new Map<number, T>();
45
+
46
+ /**
47
+ * Map of sync rule ids to promises that are clearing the sync rule configuration.
48
+ *
49
+ * We primarily do this to keep track of what we're currently clearing, but don't currently
50
+ * use the Promise value.
51
+ */
52
+ private clearingJobs = new Map<number, Promise<void>>();
53
+
46
54
  /**
47
55
  * Used for replication lag computation.
48
56
  */
@@ -242,16 +250,26 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
242
250
  }
243
251
  }
244
252
 
245
- // Sync rules stopped previously or by a different process.
253
+ // Sync rules stopped previously, including by a different process.
246
254
  const stopped = await this.storage.getStoppedSyncRules();
247
255
  for (let syncRules of stopped) {
248
- try {
249
- // TODO: Do this in the "background", allowing the periodic refresh to continue
250
- const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
251
- await this.terminateSyncRules(syncRuleStorage);
252
- } catch (e) {
253
- this.logger.warn(`Failed clean up replication config for sync rule: ${syncRules.id}`, e);
256
+ if (this.clearingJobs.has(syncRules.id)) {
257
+ // Already in progress
258
+ continue;
254
259
  }
260
+
261
+ // We clear storage asynchronously.
262
+ // It is important to be able to continue running the refresh loop, otherwise we cannot
263
+ // retry locked sync rules, for example.
264
+ const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
265
+ const promise = this.terminateSyncRules(syncRuleStorage)
266
+ .catch((e) => {
267
+ this.logger.warn(`Failed clean up replication config for sync rule: ${syncRules.id}`, e);
268
+ })
269
+ .finally(() => {
270
+ this.clearingJobs.delete(syncRules.id);
271
+ });
272
+ this.clearingJobs.set(syncRules.id, promise);
255
273
  }
256
274
  }
257
275
 
@@ -261,6 +279,8 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
261
279
 
262
280
  protected async terminateSyncRules(syncRuleStorage: storage.SyncRulesBucketStorage) {
263
281
  this.logger.info(`Terminating sync rules: ${syncRuleStorage.group_id}...`);
282
+ // This deletes postgres replication slots - should complete quickly.
283
+ // It is safe to do before or after clearing the data in the storage.
264
284
  await this.cleanUp(syncRuleStorage);
265
285
  await syncRuleStorage.terminate({ signal: this.abortController?.signal, clearStorage: true });
266
286
  this.logger.info(`Successfully terminated sync rules: ${syncRuleStorage.group_id}`);
@@ -1,10 +1,7 @@
1
- import * as jose from 'jose';
2
-
1
+ import { AuthorizationError, AuthorizationResponse, ErrorCode } from '@powersync/lib-services-framework';
3
2
  import * as auth from '../auth/auth-index.js';
4
3
  import { ServiceContext } from '../system/ServiceContext.js';
5
- import * as util from '../util/util-index.js';
6
4
  import { BasicRouterRequest, Context, RequestEndpointHandlerPayload } from './router.js';
7
- import { AuthorizationError, AuthorizationResponse, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
8
5
 
9
6
  export function endpoint(req: BasicRouterRequest) {
10
7
  const protocol = req.headers['x-forwarded-proto'] ?? req.protocol;
@@ -12,81 +9,6 @@ export function endpoint(req: BasicRouterRequest) {
12
9
  return `${protocol}://${host}`;
13
10
  }
14
11
 
15
- function devAudience(req: BasicRouterRequest): string {
16
- return `${endpoint(req)}/dev`;
17
- }
18
-
19
- /**
20
- * @deprecated
21
- *
22
- * Will be replaced by temporary tokens issued by PowerSync Management service.
23
- */
24
- export async function issueDevToken(req: BasicRouterRequest, user_id: string, config: util.ResolvedPowerSyncConfig) {
25
- const iss = devAudience(req);
26
- const aud = devAudience(req);
27
-
28
- const key = config.dev.dev_key;
29
- if (key == null) {
30
- throw new Error('Auth disabled');
31
- }
32
-
33
- return await new jose.SignJWT({})
34
- .setProtectedHeader({ alg: key.source.alg!, kid: key.kid })
35
- .setSubject(user_id)
36
- .setIssuedAt()
37
- .setIssuer(iss)
38
- .setAudience(aud)
39
- .setExpirationTime('30d')
40
- .sign(key.key);
41
- }
42
-
43
- /** @deprecated */
44
- export async function issueLegacyDevToken(
45
- req: BasicRouterRequest,
46
- user_id: string,
47
- config: util.ResolvedPowerSyncConfig
48
- ) {
49
- const iss = devAudience(req);
50
- const aud = config.jwt_audiences[0];
51
-
52
- const key = config.dev.dev_key;
53
- if (key == null || aud == null) {
54
- throw new Error('Auth disabled');
55
- }
56
-
57
- return await new jose.SignJWT({})
58
- .setProtectedHeader({ alg: key.source.alg!, kid: key.kid })
59
- .setSubject(user_id)
60
- .setIssuedAt()
61
- .setIssuer(iss)
62
- .setAudience(aud)
63
- .setExpirationTime('60m')
64
- .sign(key.key);
65
- }
66
-
67
- export async function issuePowerSyncToken(
68
- req: BasicRouterRequest,
69
- user_id: string,
70
- config: util.ResolvedPowerSyncConfig
71
- ) {
72
- const iss = devAudience(req);
73
- const aud = config.jwt_audiences[0];
74
- const key = config.dev.dev_key;
75
- if (key == null || aud == null) {
76
- throw new Error('Auth disabled');
77
- }
78
-
79
- const jwt = await new jose.SignJWT({})
80
- .setProtectedHeader({ alg: key.source.alg!, kid: key.kid })
81
- .setSubject(user_id)
82
- .setIssuedAt()
83
- .setIssuer(iss)
84
- .setAudience(aud)
85
- .setExpirationTime('5m')
86
- .sign(key.key);
87
- return jwt;
88
- }
89
-
90
12
  export function getTokenFromHeader(authHeader: string = ''): string | null {
91
13
  const tokenMatch = /^(Token|Bearer) (\S+)$/.exec(authHeader);
92
14
  if (!tokenMatch) {
@@ -146,51 +68,6 @@ export async function generateContext(serviceContext: ServiceContext, token: str
146
68
  }
147
69
  }
148
70
 
149
- /**
150
- * @deprecated
151
- */
152
- export const authDevUser = async (payload: RequestEndpointHandlerPayload) => {
153
- const {
154
- context: {
155
- service_context: { configuration }
156
- }
157
- } = payload;
158
-
159
- const token = getTokenFromHeader(payload.request.headers.authorization as string);
160
- if (!configuration.dev.demo_auth) {
161
- return {
162
- authorized: false,
163
- errors: ['Authentication disabled']
164
- };
165
- }
166
- if (token == null) {
167
- return {
168
- authorized: false,
169
- errors: ['Authentication required']
170
- };
171
- }
172
-
173
- // Different from the configured audience.
174
- // Should also not be changed by keys
175
- const audience = [devAudience(payload.request)];
176
-
177
- let tokenPayload: auth.JwtPayload;
178
- try {
179
- tokenPayload = await configuration.dev_client_keystore.verifyJwt(token, {
180
- defaultAudiences: audience,
181
- maxAge: '31d'
182
- });
183
- } catch (err) {
184
- return {
185
- authorized: false,
186
- errors: [err.message]
187
- };
188
- }
189
-
190
- payload.context.user_id = tokenPayload.sub;
191
- return { authorized: true };
192
- };
193
-
194
71
  export const authApi = (payload: RequestEndpointHandlerPayload) => {
195
72
  const {
196
73
  context: {
@@ -38,7 +38,9 @@ export function configureRSocket(router: ReactiveSocketRouter<Context>, options:
38
38
  const extracted_token = getTokenFromHeader(token);
39
39
  if (extracted_token != null) {
40
40
  const { context, tokenError } = await generateContext(options.service_context, extracted_token);
41
- if (context?.token_payload == null) {
41
+ if (tokenError != null) {
42
+ throw tokenError;
43
+ } else if (context?.token_payload == null) {
42
44
  throw new errors.AuthorizationError(ErrorCode.PSYNC_S2106, 'Authentication required');
43
45
  }
44
46
 
@@ -46,7 +48,6 @@ export function configureRSocket(router: ReactiveSocketRouter<Context>, options:
46
48
  token,
47
49
  user_agent,
48
50
  ...context,
49
- token_error: tokenError,
50
51
  service_context: service_context as RouterServiceContext,
51
52
  logger: connectionLogger
52
53
  };