@powersync/service-core 0.14.0 → 0.16.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 (59) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/auth/RemoteJWKSCollector.d.ts +9 -7
  3. package/dist/auth/RemoteJWKSCollector.js +16 -29
  4. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  5. package/dist/entry/cli-entry.js +2 -0
  6. package/dist/entry/cli-entry.js.map +1 -1
  7. package/dist/entry/commands/teardown-action.js +2 -1
  8. package/dist/entry/commands/teardown-action.js.map +1 -1
  9. package/dist/entry/commands/test-connection-action.d.ts +2 -0
  10. package/dist/entry/commands/test-connection-action.js +32 -0
  11. package/dist/entry/commands/test-connection-action.js.map +1 -0
  12. package/dist/metrics/Metrics.js +2 -2
  13. package/dist/metrics/Metrics.js.map +1 -1
  14. package/dist/replication/AbstractReplicator.d.ts +2 -0
  15. package/dist/replication/AbstractReplicator.js.map +1 -1
  16. package/dist/replication/ReplicationEngine.d.ts +2 -0
  17. package/dist/replication/ReplicationEngine.js +3 -0
  18. package/dist/replication/ReplicationEngine.js.map +1 -1
  19. package/dist/replication/ReplicationModule.d.ts +8 -2
  20. package/dist/replication/ReplicationModule.js +6 -11
  21. package/dist/replication/ReplicationModule.js.map +1 -1
  22. package/dist/routes/endpoints/admin.js +3 -3
  23. package/dist/routes/endpoints/admin.js.map +1 -1
  24. package/dist/routes/endpoints/socket-route.js +5 -5
  25. package/dist/routes/endpoints/socket-route.js.map +1 -1
  26. package/dist/routes/endpoints/sync-rules.js +9 -9
  27. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  28. package/dist/routes/endpoints/sync-stream.js +5 -5
  29. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  30. package/dist/routes/route-register.js +4 -4
  31. package/dist/routes/route-register.js.map +1 -1
  32. package/dist/storage/BucketStorage.d.ts +2 -2
  33. package/dist/storage/BucketStorage.js.map +1 -1
  34. package/dist/util/config/compound-config-collector.js +14 -4
  35. package/dist/util/config/compound-config-collector.js.map +1 -1
  36. package/dist/util/config/types.d.ts +0 -3
  37. package/dist/util/utils.d.ts +4 -0
  38. package/dist/util/utils.js +31 -1
  39. package/dist/util/utils.js.map +1 -1
  40. package/package.json +5 -5
  41. package/src/auth/RemoteJWKSCollector.ts +31 -35
  42. package/src/entry/cli-entry.ts +2 -0
  43. package/src/entry/commands/teardown-action.ts +2 -1
  44. package/src/entry/commands/test-connection-action.ts +41 -0
  45. package/src/metrics/Metrics.ts +2 -2
  46. package/src/replication/AbstractReplicator.ts +3 -0
  47. package/src/replication/ReplicationEngine.ts +5 -0
  48. package/src/replication/ReplicationModule.ts +15 -13
  49. package/src/routes/endpoints/admin.ts +3 -3
  50. package/src/routes/endpoints/socket-route.ts +5 -5
  51. package/src/routes/endpoints/sync-rules.ts +9 -9
  52. package/src/routes/endpoints/sync-stream.ts +5 -5
  53. package/src/routes/route-register.ts +4 -4
  54. package/src/storage/BucketStorage.ts +6 -2
  55. package/src/util/config/compound-config-collector.ts +17 -9
  56. package/src/util/config/types.ts +0 -3
  57. package/src/util/utils.ts +28 -1
  58. package/test/src/auth.test.ts +20 -5
  59. package/tsconfig.tsbuildinfo +1 -1
@@ -1,13 +1,19 @@
1
1
  import { DataSourceConfig } from '@powersync/service-types/dist/config/PowerSyncConfig.js';
2
2
  import * as t from 'ts-codec';
3
3
 
4
+ import { schema } from '@powersync/lib-services-framework';
4
5
  import * as types from '@powersync/service-types';
5
6
  import * as api from '../api/api-index.js';
6
7
  import * as modules from '../modules/modules-index.js';
7
8
  import * as system from '../system/system-index.js';
8
- import { schema } from '@powersync/lib-services-framework';
9
9
  import { AbstractReplicator } from './AbstractReplicator.js';
10
- import { TearDownOptions } from '../modules/modules-index.js';
10
+
11
+ export interface ConnectionTestResult {
12
+ /**
13
+ * Connection URI or hostname.
14
+ */
15
+ connectionDescription: string;
16
+ }
11
17
 
12
18
  /**
13
19
  * Provides a common interface for testing the connection to a DataSource.
@@ -17,7 +23,7 @@ export interface ConnectionTester<TConfig extends DataSourceConfig> {
17
23
  * Confirm if a connection can be established to the datasource for the provided datasource configuration
18
24
  * @param config
19
25
  */
20
- testConnection(config: TConfig): Promise<void>;
26
+ testConnection(config: TConfig): Promise<ConnectionTestResult>;
21
27
  }
22
28
 
23
29
  export interface ReplicationModuleOptions extends modules.AbstractModuleOptions {
@@ -58,7 +64,7 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
58
64
  */
59
65
  protected abstract createReplicator(context: system.ServiceContext): AbstractReplicator;
60
66
 
61
- public abstract testConnection(config: TConfig): Promise<void>;
67
+ public abstract testConnection(config: TConfig): Promise<ConnectionTestResult>;
62
68
 
63
69
  /**
64
70
  * Register this module's Replicators and RouteAPI adapters if the required configuration is present.
@@ -81,16 +87,12 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
81
87
  );
82
88
  }
83
89
 
84
- try {
85
- const baseMatchingConfig = matchingConfig[0] as TConfig;
86
- // If decoding fails, log the error and continue, no replication will happen for this data source
87
- this.decodeConfig(baseMatchingConfig);
90
+ const baseMatchingConfig = matchingConfig[0] as TConfig;
91
+ // If decoding fails, this will raise a hard error, and stop the service.
92
+ this.decodeConfig(baseMatchingConfig);
88
93
 
89
- context.replicationEngine?.register(this.createReplicator(context));
90
- context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
91
- } catch (e) {
92
- this.logger.error('Failed to initialize.', e);
93
- }
94
+ context.replicationEngine?.register(this.createReplicator(context));
95
+ context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
94
96
  }
95
97
 
96
98
  protected decodeConfig(config: TConfig): void {
@@ -1,4 +1,4 @@
1
- import { errors, router, schema } from '@powersync/lib-services-framework';
1
+ import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
2
2
  import { SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
3
3
  import { internal_routes } from '@powersync/service-types';
4
4
 
@@ -120,9 +120,9 @@ export const reprocess = routeDefinition({
120
120
 
121
121
  const active = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
122
122
  if (active == null) {
123
- throw new errors.JourneyError({
123
+ throw new errors.ServiceError({
124
124
  status: 422,
125
- code: 'NO_SYNC_RULES',
125
+ code: ErrorCode.PSYNC_S4104,
126
126
  description: 'No active sync rules'
127
127
  });
128
128
  }
@@ -1,4 +1,4 @@
1
- import { errors, logger, schema } from '@powersync/lib-services-framework';
1
+ import { ErrorCode, errors, logger, schema } from '@powersync/lib-services-framework';
2
2
  import { RequestParameters } from '@powersync/service-sync-rules';
3
3
  import { serialize } from 'bson';
4
4
 
@@ -34,9 +34,9 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
34
34
 
35
35
  if (routerEngine!.closed) {
36
36
  responder.onError(
37
- new errors.JourneyError({
37
+ new errors.ServiceError({
38
38
  status: 503,
39
- code: 'SERVICE_UNAVAILABLE',
39
+ code: ErrorCode.PSYNC_S2003,
40
40
  description: 'Service temporarily unavailable'
41
41
  })
42
42
  );
@@ -53,9 +53,9 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
53
53
  const cp = await activeBucketStorage.getActiveCheckpoint();
54
54
  if (!cp.hasSyncRules()) {
55
55
  responder.onError(
56
- new errors.JourneyError({
56
+ new errors.ServiceError({
57
57
  status: 500,
58
- code: 'NO_SYNC_RULES',
58
+ code: ErrorCode.PSYNC_S2302,
59
59
  description: 'No sync rules available'
60
60
  })
61
61
  );
@@ -1,4 +1,4 @@
1
- import { errors, router, schema } from '@powersync/lib-services-framework';
1
+ import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
2
2
  import { SqlSyncRules, SyncRulesErrors } from '@powersync/service-sync-rules';
3
3
  import type { FastifyPluginAsync } from 'fastify';
4
4
  import * as t from 'ts-codec';
@@ -43,9 +43,9 @@ export const deploySyncRules = routeDefinition({
43
43
 
44
44
  if (service_context.configuration.sync_rules.present) {
45
45
  // If sync rules are configured via the config, disable deploy via the API.
46
- throw new errors.JourneyError({
46
+ throw new errors.ServiceError({
47
47
  status: 422,
48
- code: 'API_DISABLED',
48
+ code: ErrorCode.PSYNC_S4105,
49
49
  description: 'Sync rules API disabled',
50
50
  details: 'Use the management API to deploy sync rules'
51
51
  });
@@ -60,9 +60,9 @@ export const deploySyncRules = routeDefinition({
60
60
  schema: undefined
61
61
  });
62
62
  } catch (e) {
63
- throw new errors.JourneyError({
63
+ throw new errors.ServiceError({
64
64
  status: 422,
65
- code: 'INVALID_SYNC_RULES',
65
+ code: ErrorCode.PSYNC_R0001,
66
66
  description: 'Sync rules parsing failed',
67
67
  details: e.message
68
68
  });
@@ -112,9 +112,9 @@ export const currentSyncRules = routeDefinition({
112
112
 
113
113
  const sync_rules = await activeBucketStorage.getActiveSyncRulesContent();
114
114
  if (!sync_rules) {
115
- throw new errors.JourneyError({
115
+ throw new errors.ServiceError({
116
116
  status: 422,
117
- code: 'NO_SYNC_RULES',
117
+ code: ErrorCode.PSYNC_S4104,
118
118
  description: 'No active sync rules'
119
119
  });
120
120
  }
@@ -159,9 +159,9 @@ export const reprocessSyncRules = routeDefinition({
159
159
  const apiHandler = payload.context.service_context.routerEngine!.getAPI();
160
160
  const sync_rules = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
161
161
  if (sync_rules == null) {
162
- throw new errors.JourneyError({
162
+ throw new errors.ServiceError({
163
163
  status: 422,
164
- code: 'NO_SYNC_RULES',
164
+ code: ErrorCode.PSYNC_S4104,
165
165
  description: 'No active sync rules'
166
166
  });
167
167
  }
@@ -1,4 +1,4 @@
1
- import { errors, logger, router, schema } from '@powersync/lib-services-framework';
1
+ import { ErrorCode, errors, logger, router, schema } from '@powersync/lib-services-framework';
2
2
  import { RequestParameters } from '@powersync/service-sync-rules';
3
3
  import { Readable } from 'stream';
4
4
 
@@ -26,9 +26,9 @@ export const syncStreamed = routeDefinition({
26
26
  const clientId = payload.params.client_id;
27
27
 
28
28
  if (routerEngine!.closed) {
29
- throw new errors.JourneyError({
29
+ throw new errors.ServiceError({
30
30
  status: 503,
31
- code: 'SERVICE_UNAVAILABLE',
31
+ code: ErrorCode.PSYNC_S2003,
32
32
  description: 'Service temporarily unavailable'
33
33
  });
34
34
  }
@@ -39,9 +39,9 @@ export const syncStreamed = routeDefinition({
39
39
  // Sanity check before we start the stream
40
40
  const cp = await storageEngine.activeBucketStorage.getActiveCheckpoint();
41
41
  if (!cp.hasSyncRules()) {
42
- throw new errors.JourneyError({
42
+ throw new errors.ServiceError({
43
43
  status: 500,
44
- code: 'NO_SYNC_RULES',
44
+ code: ErrorCode.PSYNC_S2302,
45
45
  description: 'No sync rules available'
46
46
  });
47
47
  }
@@ -62,16 +62,16 @@ export function registerFastifyRoutes(
62
62
  });
63
63
  }
64
64
  } catch (ex) {
65
- const journeyError = errors.JourneyError.isJourneyError(ex) ? ex : new errors.InternalServerError(ex);
66
- logger.error(`Request failed`, journeyError);
65
+ const serviceError = errors.asServiceError(ex);
66
+ logger.error(`Request failed`, serviceError);
67
67
 
68
68
  response = new router.RouterResponse({
69
- status: journeyError.errorData.status || 500,
69
+ status: serviceError.errorData.status || 500,
70
70
  headers: {
71
71
  'Content-Type': 'application/json'
72
72
  },
73
73
  data: {
74
- error: journeyError.errorData
74
+ error: serviceError.errorData
75
75
  }
76
76
  });
77
77
  }
@@ -1,4 +1,8 @@
1
- import { DisposableListener, DisposableObserverClient } from '@powersync/lib-services-framework';
1
+ import {
2
+ AsyncDisposableObserverClient,
3
+ DisposableListener,
4
+ DisposableObserverClient
5
+ } from '@powersync/lib-services-framework';
2
6
  import {
3
7
  EvaluatedParameters,
4
8
  EvaluatedRow,
@@ -56,7 +60,7 @@ export interface BucketStorageFactoryListener extends DisposableListener {
56
60
  replicationEvent: (event: ReplicationEventPayload) => void;
57
61
  }
58
62
 
59
- export interface BucketStorageFactory extends DisposableObserverClient<BucketStorageFactoryListener> {
63
+ export interface BucketStorageFactory extends AsyncDisposableObserverClient<BucketStorageFactoryListener> {
60
64
  /**
61
65
  * Update sync rules from configuration, if changed.
62
66
  */
@@ -1,4 +1,4 @@
1
- import { logger } from '@powersync/lib-services-framework';
1
+ import { logger, LookupOptions } from '@powersync/lib-services-framework';
2
2
  import { configFile } from '@powersync/service-types';
3
3
  import * as auth from '../../auth/auth-index.js';
4
4
  import { ConfigCollector } from './collectors/config-collector.js';
@@ -91,12 +91,23 @@ export class CompoundConfigCollector {
91
91
  jwks_uris = [jwks_uris];
92
92
  }
93
93
 
94
+ let jwksLookup: LookupOptions = {
95
+ reject_ip_ranges: []
96
+ };
97
+
98
+ if (baseConfig.client_auth?.jwks_reject_ip_ranges != null) {
99
+ jwksLookup = {
100
+ reject_ip_ranges: baseConfig.client_auth?.jwks_reject_ip_ranges
101
+ };
102
+ }
103
+ if (baseConfig.client_auth?.block_local_jwks) {
104
+ // Deprecated - recommend method is to use jwks_reject_ip_ranges
105
+ jwksLookup.reject_ip_ranges.push('local');
106
+ jwksLookup.reject_ipv6 = true;
107
+ }
108
+
94
109
  for (let uri of jwks_uris) {
95
- collectors.add(
96
- new auth.CachedKeyCollector(
97
- new auth.RemoteJWKSCollector(uri, { block_local_ip: !!baseConfig.client_auth?.block_local_jwks })
98
- )
99
- );
110
+ collectors.add(new auth.CachedKeyCollector(new auth.RemoteJWKSCollector(uri, { lookupOptions: jwksLookup })));
100
111
  }
101
112
 
102
113
  const baseDevKey = (baseConfig.client_auth?.jwks?.keys ?? []).find((key) => key.kid == POWERSYNC_DEV_KID);
@@ -121,9 +132,6 @@ export class CompoundConfigCollector {
121
132
  api_tokens: baseConfig.api?.tokens ?? [],
122
133
  dev: {
123
134
  demo_auth: baseConfig.dev?.demo_auth ?? false,
124
- demo_client: baseConfig.dev?.demo_client ?? false,
125
- demo_password: baseConfig.dev?.demo_password,
126
- crud_api: baseConfig.dev?.crud_api ?? false,
127
135
  dev_key: devKey
128
136
  },
129
137
  port: baseConfig.port ?? 8080,
@@ -34,9 +34,6 @@ export type ResolvedPowerSyncConfig = {
34
34
  storage: configFile.GenericStorageConfig;
35
35
  dev: {
36
36
  demo_auth: boolean;
37
- demo_password?: string;
38
- crud_api: boolean;
39
- demo_client: boolean;
40
37
  /**
41
38
  * Only present when demo_auth == true
42
39
  */
package/src/util/utils.ts CHANGED
@@ -7,6 +7,7 @@ import { BucketChecksum, OpId, OplogEntry } from './protocol-types.js';
7
7
  import * as storage from '../storage/storage-index.js';
8
8
 
9
9
  import { PartialChecksum } from '../storage/ChecksumCache.js';
10
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
10
11
 
11
12
  export type ChecksumMap = Map<string, BucketChecksum>;
12
13
 
@@ -34,7 +35,7 @@ export function timestampToOpId(ts: bigint): OpId {
34
35
  // Dynamic values are passed in in some cases, so we make extra sure that the
35
36
  // number is a bigint and not number or Long.
36
37
  if (typeof ts != 'bigint') {
37
- throw new Error(`bigint expected, got: ${ts} (${typeof ts})`);
38
+ throw new ServiceAssertionError(`bigint expected, got: ${ts} (${typeof ts})`);
38
39
  }
39
40
  return ts.toString(10);
40
41
  }
@@ -222,3 +223,29 @@ export function flatstr(s: string) {
222
223
  function rowKey(entry: OplogEntry) {
223
224
  return `${entry.object_type}/${entry.object_id}/${entry.subkey}`;
224
225
  }
226
+
227
+ /**
228
+ * Estimate in-memory size of row.
229
+ */
230
+ export function estimateRowSize(record: sync_rules.ToastableSqliteRow | undefined) {
231
+ if (record == null) {
232
+ return 12;
233
+ }
234
+ let size = 0;
235
+ for (let [key, value] of Object.entries(record)) {
236
+ size += 12 + key.length;
237
+ // number | string | null | bigint | Uint8Array
238
+ if (value == null) {
239
+ size += 4;
240
+ } else if (typeof value == 'number') {
241
+ size += 8;
242
+ } else if (typeof value == 'bigint') {
243
+ size += 8;
244
+ } else if (typeof value == 'string') {
245
+ size += value.length;
246
+ } else if (value instanceof Uint8Array) {
247
+ size += value.byteLength;
248
+ }
249
+ }
250
+ return size;
251
+ }
@@ -284,11 +284,23 @@ describe('JWT Auth', () => {
284
284
  expect(errors).toEqual([]);
285
285
  expect(keys.length).toBeGreaterThanOrEqual(1);
286
286
 
287
- // The localhost hostname fails to resolve correctly on MacOS https://github.com/nodejs/help/issues/2163
288
- const invalid = new RemoteJWKSCollector('https://127.0.0.1/.well-known/jwks.json', {
289
- block_local_ip: true
287
+ // Domain names are resolved when retrieving keys
288
+ const invalid = new RemoteJWKSCollector('https://localhost/.well-known/jwks.json', {
289
+ lookupOptions: {
290
+ reject_ip_ranges: ['local']
291
+ }
290
292
  });
291
293
  expect(invalid.getKeys()).rejects.toThrow('IPs in this range are not supported');
294
+
295
+ // IPS throw an error immediately
296
+ expect(
297
+ () =>
298
+ new RemoteJWKSCollector('https://127.0.0.1/.well-known/jwks.json', {
299
+ lookupOptions: {
300
+ reject_ip_ranges: ['local']
301
+ }
302
+ })
303
+ ).toThrowError('IPs in this range are not supported');
292
304
  });
293
305
 
294
306
  test('http not blocking local IPs', async () => {
@@ -301,10 +313,13 @@ describe('JWT Auth', () => {
301
313
  expect(errors).toEqual([]);
302
314
  expect(keys.length).toBeGreaterThanOrEqual(1);
303
315
 
304
- // The localhost hostname fails to resolve correctly on MacOS https://github.com/nodejs/help/issues/2163
305
316
  const invalid = new RemoteJWKSCollector('https://127.0.0.1/.well-known/jwks.json');
306
317
  // Should try and fetch
307
- expect(invalid.getKeys()).rejects.toThrow('ECONNREFUSED');
318
+ expect(invalid.getKeys()).rejects.toThrow();
319
+
320
+ const invalid2 = new RemoteJWKSCollector('https://localhost/.well-known/jwks.json');
321
+ // Should try and fetch
322
+ expect(invalid2.getKeys()).rejects.toThrow();
308
323
  });
309
324
 
310
325
  test('caching', async () => {