@powersync/service-core 0.0.0-dev-20260203155513 → 0.0.0-dev-20260223080959

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 (87) hide show
  1. package/CHANGELOG.md +46 -8
  2. package/dist/api/RouteAPI.d.ts +2 -2
  3. package/dist/api/diagnostics.js +14 -6
  4. package/dist/api/diagnostics.js.map +1 -1
  5. package/dist/auth/JwtPayload.d.ts +7 -8
  6. package/dist/auth/JwtPayload.js +19 -1
  7. package/dist/auth/JwtPayload.js.map +1 -1
  8. package/dist/auth/KeyStore.js +2 -1
  9. package/dist/auth/KeyStore.js.map +1 -1
  10. package/dist/replication/AbstractReplicator.js +2 -5
  11. package/dist/replication/AbstractReplicator.js.map +1 -1
  12. package/dist/routes/auth.d.ts +0 -1
  13. package/dist/routes/auth.js +2 -4
  14. package/dist/routes/auth.js.map +1 -1
  15. package/dist/routes/configure-fastify.d.ts +84 -0
  16. package/dist/routes/configure-fastify.js +0 -1
  17. package/dist/routes/configure-fastify.js.map +1 -1
  18. package/dist/routes/endpoints/admin.d.ts +171 -0
  19. package/dist/routes/endpoints/admin.js +36 -21
  20. package/dist/routes/endpoints/admin.js.map +1 -1
  21. package/dist/routes/endpoints/checkpointing.js +3 -3
  22. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  23. package/dist/routes/endpoints/socket-route.js +4 -10
  24. package/dist/routes/endpoints/socket-route.js.map +1 -1
  25. package/dist/routes/endpoints/sync-rules.js +10 -13
  26. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  27. package/dist/routes/endpoints/sync-stream.js +3 -8
  28. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  29. package/dist/routes/router.d.ts +0 -1
  30. package/dist/routes/router.js.map +1 -1
  31. package/dist/storage/BucketStorageFactory.d.ts +29 -15
  32. package/dist/storage/BucketStorageFactory.js +58 -1
  33. package/dist/storage/BucketStorageFactory.js.map +1 -1
  34. package/dist/storage/PersistedSyncRulesContent.d.ts +28 -4
  35. package/dist/storage/PersistedSyncRulesContent.js +56 -1
  36. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  37. package/dist/storage/ReportStorage.d.ts +1 -8
  38. package/dist/storage/StorageVersionConfig.d.ts +20 -0
  39. package/dist/storage/StorageVersionConfig.js +20 -0
  40. package/dist/storage/StorageVersionConfig.js.map +1 -0
  41. package/dist/storage/SyncRulesBucketStorage.d.ts +8 -0
  42. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  43. package/dist/storage/storage-index.d.ts +1 -0
  44. package/dist/storage/storage-index.js +1 -0
  45. package/dist/storage/storage-index.js.map +1 -1
  46. package/dist/sync/BucketChecksumState.d.ts +4 -6
  47. package/dist/sync/BucketChecksumState.js +4 -9
  48. package/dist/sync/BucketChecksumState.js.map +1 -1
  49. package/dist/sync/sync.d.ts +0 -8
  50. package/dist/sync/sync.js +9 -19
  51. package/dist/sync/sync.js.map +1 -1
  52. package/dist/sync/util.d.ts +0 -22
  53. package/dist/sync/util.js +0 -24
  54. package/dist/sync/util.js.map +1 -1
  55. package/dist/util/config.js +4 -1
  56. package/dist/util/config.js.map +1 -1
  57. package/package.json +5 -5
  58. package/src/api/RouteAPI.ts +2 -2
  59. package/src/api/diagnostics.ts +16 -7
  60. package/src/auth/JwtPayload.ts +16 -8
  61. package/src/auth/KeyStore.ts +1 -1
  62. package/src/replication/AbstractReplicator.ts +3 -5
  63. package/src/routes/auth.ts +2 -4
  64. package/src/routes/configure-fastify.ts +0 -1
  65. package/src/routes/endpoints/admin.ts +45 -26
  66. package/src/routes/endpoints/checkpointing.ts +5 -3
  67. package/src/routes/endpoints/socket-route.ts +4 -11
  68. package/src/routes/endpoints/sync-rules.ts +18 -17
  69. package/src/routes/endpoints/sync-stream.ts +4 -8
  70. package/src/routes/router.ts +0 -2
  71. package/src/storage/BucketStorageFactory.ts +67 -19
  72. package/src/storage/PersistedSyncRulesContent.ts +82 -5
  73. package/src/storage/ReportStorage.ts +3 -9
  74. package/src/storage/StorageVersionConfig.ts +30 -0
  75. package/src/storage/SyncRulesBucketStorage.ts +9 -0
  76. package/src/storage/storage-index.ts +1 -0
  77. package/src/sync/BucketChecksumState.ts +10 -13
  78. package/src/sync/sync.ts +15 -42
  79. package/src/sync/util.ts +0 -25
  80. package/src/util/config.ts +7 -2
  81. package/test/src/auth.test.ts +76 -20
  82. package/test/src/routes/admin.test.ts +48 -0
  83. package/test/src/routes/mocks.ts +22 -1
  84. package/test/src/routes/stream.test.ts +10 -9
  85. package/test/src/sync/BucketChecksumState.test.ts +92 -84
  86. package/test/tsconfig.json +3 -6
  87. package/tsconfig.tsbuildinfo +1 -1
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  BucketDescription,
3
+ BucketParameterQuerier,
3
4
  BucketPriority,
4
5
  BucketSource,
5
6
  HydratedSyncRules,
7
+ QuerierError,
6
8
  RequestedStream,
7
- RequestJwtPayload,
8
9
  RequestParameters,
9
10
  ResolvedBucket
10
11
  } from '@powersync/service-sync-rules';
@@ -13,13 +14,13 @@ import * as storage from '../storage/storage-index.js';
13
14
  import * as util from '../util/util-index.js';
14
15
 
15
16
  import {
16
- ErrorCode,
17
17
  logger as defaultLogger,
18
+ ErrorCode,
18
19
  Logger,
19
20
  ServiceAssertionError,
20
21
  ServiceError
21
22
  } from '@powersync/lib-services-framework';
22
- import { BucketParameterQuerier, QuerierError } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
23
+ import { JwtPayload } from '../auth/JwtPayload.js';
23
24
  import { SyncContext } from './SyncContext.js';
24
25
  import { getIntersection, hasIntersection } from './util.js';
25
26
 
@@ -27,7 +28,7 @@ export interface BucketChecksumStateOptions {
27
28
  syncContext: SyncContext;
28
29
  bucketStorage: BucketChecksumStateStorage;
29
30
  syncRules: HydratedSyncRules;
30
- tokenPayload: RequestJwtPayload;
31
+ tokenPayload: JwtPayload;
31
32
  syncRequest: util.StreamingSyncRequest;
32
33
  logger?: Logger;
33
34
  }
@@ -79,7 +80,6 @@ export class BucketChecksumState {
79
80
  private pendingBucketDownloads = new Set<string>();
80
81
 
81
82
  private readonly logger: Logger;
82
- private resync = false;
83
83
 
84
84
  constructor(options: BucketChecksumStateOptions) {
85
85
  this.context = options.syncContext;
@@ -113,13 +113,12 @@ export class BucketChecksumState {
113
113
  */
114
114
  async buildNextCheckpointLine(next: storage.StorageCheckpointUpdate): Promise<CheckpointLine | null> {
115
115
  const { writeCheckpoint, base } = next;
116
- const user_id = this.parameterState.syncParams.userId;
116
+ const userIdForLogs = this.parameterState.syncParams.userId;
117
117
 
118
118
  const storage = this.bucketStorage;
119
119
 
120
120
  const update = await this.parameterState.getCheckpointUpdate(next);
121
121
  const { buckets: allBuckets, updatedBuckets } = update;
122
- this.resync = updatedBuckets === INVALIDATE_ALL_BUCKETS;
123
122
 
124
123
  /** Set of all buckets in this checkpoint. */
125
124
  const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
@@ -164,7 +163,6 @@ export class BucketChecksumState {
164
163
  checksumMap = newChecksums;
165
164
  } else {
166
165
  // Re-check all buckets
167
- this.resync = true;
168
166
  const bucketList = [...bucketDescriptionMap.keys()];
169
167
  checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
170
168
  }
@@ -189,7 +187,6 @@ export class BucketChecksumState {
189
187
  diff.updatedBuckets.length == 0
190
188
  ) {
191
189
  // No changes - don't send anything to the client
192
- this.resync = false;
193
190
  return null;
194
191
  }
195
192
 
@@ -224,7 +221,7 @@ export class BucketChecksumState {
224
221
  message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
225
222
  this.logger.info(message, {
226
223
  checkpoint: base.checkpoint,
227
- user_id: user_id,
224
+ user_id: userIdForLogs,
228
225
  buckets: allBuckets.length,
229
226
  updated: diff.updatedBuckets.length,
230
227
  removed: diff.removedBuckets.length
@@ -243,7 +240,7 @@ export class BucketChecksumState {
243
240
  deferredLog = () => {
244
241
  let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
245
242
  message += `buckets: ${allBuckets.length} ${limitedBuckets(allBuckets, 20)}`;
246
- this.logger.info(message, { checkpoint: base.checkpoint, user_id: user_id, buckets: allBuckets.length });
243
+ this.logger.info(message, { checkpoint: base.checkpoint, user_id: userIdForLogs, buckets: allBuckets.length });
247
244
  };
248
245
  bucketsToFetch = allBuckets.map((b) => ({ bucket: b.bucket, priority: b.priority }));
249
246
 
@@ -398,12 +395,12 @@ export class BucketParameterState {
398
395
  private cachedDynamicBucketSet: Set<string> | null = null;
399
396
 
400
397
  private lookupsFromPreviousCheckpoint: Set<string> | null = null;
401
- private resyncBucket = false;
398
+
402
399
  constructor(
403
400
  context: SyncContext,
404
401
  bucketStorage: BucketChecksumStateStorage,
405
402
  syncRules: HydratedSyncRules,
406
- tokenPayload: RequestJwtPayload,
403
+ tokenPayload: JwtPayload,
407
404
  request: util.StreamingSyncRequest,
408
405
  logger: Logger
409
406
  ) {
package/src/sync/sync.ts CHANGED
@@ -1,31 +1,19 @@
1
1
  import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
2
- import { BucketDescription, BucketPriority, HydratedSyncRules, RequestJwtPayload } from '@powersync/service-sync-rules';
2
+ import { BucketDescription, BucketPriority, HydratedSyncRules, SqliteJsonValue } from '@powersync/service-sync-rules';
3
3
 
4
4
  import { AbortError } from 'ix/aborterror.js';
5
5
 
6
6
  import * as auth from '../auth/auth-index.js';
7
7
  import * as storage from '../storage/storage-index.js';
8
8
  import * as util from '../util/util-index.js';
9
+
9
10
  import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
10
11
  import { mergeAsyncIterables } from '../streams/streams-index.js';
11
12
  import { BucketChecksumState, CheckpointLine } from './BucketChecksumState.js';
12
13
  import { OperationsSentStats, RequestTracker, statsForBatch } from './RequestTracker.js';
13
14
  import { SyncContext } from './SyncContext.js';
14
- import {
15
- acquireSemaphoreAbortable,
16
- parseCheckpointLineForEvent,
17
- settledPromise,
18
- tokenStream,
19
- TokenStreamOptions
20
- } from './util.js';
21
- import { EventsEngine } from '../events/EventsEngine.js';
22
- import { event_types } from '@powersync/service-types';
23
-
24
- type Event = {
25
- engine?: EventsEngine;
26
- user_id: string;
27
- client_id: string;
28
- };
15
+ import { TokenStreamOptions, acquireSemaphoreAbortable, settledPromise, tokenStream } from './util.js';
16
+
29
17
  export interface SyncStreamParameters {
30
18
  syncContext: SyncContext;
31
19
  bucketStorage: storage.SyncRulesBucketStorage;
@@ -41,7 +29,6 @@ export interface SyncStreamParameters {
41
29
  tokenStreamOptions?: Partial<TokenStreamOptions>;
42
30
 
43
31
  tracker: RequestTracker;
44
- event?: Event;
45
32
  }
46
33
 
47
34
  export async function* streamResponse(
@@ -56,8 +43,7 @@ export async function* streamResponse(
56
43
  tokenStreamOptions,
57
44
  tracker,
58
45
  signal,
59
- isEncodingAsBson,
60
- event
46
+ isEncodingAsBson
61
47
  } = options;
62
48
  const logger = options.logger ?? defaultLogger;
63
49
 
@@ -85,8 +71,7 @@ export async function* streamResponse(
85
71
  tracker,
86
72
  controller.signal,
87
73
  logger,
88
- isEncodingAsBson,
89
- event
74
+ isEncodingAsBson
90
75
  );
91
76
  // Merge the two streams, and abort as soon as one of the streams end.
92
77
  const merged = mergeAsyncIterables([stream, ki], controller.signal);
@@ -111,17 +96,13 @@ async function* streamResponseInner(
111
96
  bucketStorage: storage.SyncRulesBucketStorage,
112
97
  syncRules: HydratedSyncRules,
113
98
  params: util.StreamingSyncRequest,
114
- tokenPayload: RequestJwtPayload,
99
+ tokenPayload: auth.JwtPayload,
115
100
  tracker: RequestTracker,
116
101
  signal: AbortSignal,
117
102
  logger: Logger,
118
- isEncodingAsBson: boolean,
119
- event?: Event
103
+ isEncodingAsBson: boolean
120
104
  ): AsyncGenerator<util.StreamingSyncLine | string | null> {
121
- const { raw_data } = params;
122
-
123
- const userId = tokenPayload.sub;
124
- const checkpointUserId = util.checkpointUserId(userId as string, params.client_id);
105
+ const checkpointUserId = util.checkpointUserId(tokenPayload.userIdString, params.client_id);
125
106
 
126
107
  const checksumState = new BucketChecksumState({
127
108
  syncContext,
@@ -179,15 +160,6 @@ async function* streamResponseInner(
179
160
 
180
161
  // Since yielding can block, we update the state just before yielding the line.
181
162
  line.advance();
182
- parseCheckpointLineForEvent(checkpointLine);
183
-
184
- event?.engine?.emit(event_types.EventsEngineEventType.SYNC_ANALYTICS_EVENT, {
185
- user_id: event?.user_id,
186
- client_id: event?.client_id,
187
- request_streams: params.streams,
188
- data: parseCheckpointLineForEvent(checkpointLine)
189
- });
190
-
191
163
  yield checkpointLine;
192
164
 
193
165
  // Start syncing data for buckets up to the checkpoint. As soon as we have completed at least one priority and
@@ -261,7 +233,7 @@ async function* streamResponseInner(
261
233
  onRowsSent: markOperationsSent,
262
234
  abort_connection: signal,
263
235
  abort_batch: abortCheckpointSignal,
264
- user_id: userId,
236
+ userIdForLogs: tokenPayload.userIdJson,
265
237
  // Passing null here will emit a full sync complete message at the end. If we pass a priority, we'll emit a partial
266
238
  // sync complete message instead.
267
239
  forPriority: !isLast ? priority : null,
@@ -295,7 +267,8 @@ interface BucketDataRequest {
295
267
  * This signal also fires when abort_connection fires.
296
268
  */
297
269
  abort_batch: AbortSignal;
298
- user_id?: string;
270
+ /** User id for debug purposes, not for sync rules. */
271
+ userIdForLogs?: SqliteJsonValue;
299
272
  forPriority: BucketPriority | null;
300
273
  onRowsSent: (stats: OperationsSentStats) => void;
301
274
  logger: Logger;
@@ -358,7 +331,7 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
358
331
  let checkpointInvalidated = false;
359
332
 
360
333
  if (syncContext.syncSemaphore.isLocked()) {
361
- logger.info('Sync concurrency limit reached, waiting for lock', { user_id: request.user_id });
334
+ logger.info('Sync concurrency limit reached, waiting for lock', { user_id: request.userIdForLogs });
362
335
  }
363
336
  const acquired = await acquireSemaphoreAbortable(syncContext.syncSemaphore, AbortSignal.any([abort_batch]));
364
337
  if (acquired === 'aborted') {
@@ -371,7 +344,7 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
371
344
  // This can be noisy, so we only log when we get close to the
372
345
  // concurrency limit.
373
346
  logger.info(`Got sync lock. Slots available: ${value - 1}`, {
374
- user_id: request.user_id,
347
+ user_id: request.userIdForLogs,
375
348
  sync_data_slots: value - 1
376
349
  });
377
350
  }
@@ -454,7 +427,7 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
454
427
  // This can be noisy, so we only log when we get close to the
455
428
  // concurrency limit.
456
429
  logger.info(`Releasing sync lock`, {
457
- user_id: request.user_id
430
+ user_id: request.userIdForLogs
458
431
  });
459
432
  }
460
433
  release();
package/src/sync/util.ts CHANGED
@@ -2,11 +2,8 @@ import * as timers from 'timers/promises';
2
2
 
3
3
  import { SemaphoreInterface } from 'async-mutex';
4
4
  import * as util from '../util/util-index.js';
5
- import { StreamingSyncCheckpoint, StreamingSyncCheckpointDiff } from '../util/util-index.js';
6
5
  import { RequestTracker } from './RequestTracker.js';
7
- import * as bson from 'bson';
8
6
  import { serialize } from 'bson';
9
- import { SyncEventCheckpointType } from '@powersync/service-types/dist/reports.js';
10
7
 
11
8
  export type TokenStreamOptions = {
12
9
  /**
@@ -218,25 +215,3 @@ export function* getIntersection<T>(a: MapOrSet<T>, b: MapOrSet<T>): IterableIte
218
215
  }
219
216
  }
220
217
  }
221
-
222
- export function parseCheckpointLineForEvent(line: StreamingSyncCheckpoint | StreamingSyncCheckpointDiff) {
223
- if ('checkpoint' in line) {
224
- return {
225
- id: new bson.ObjectId(),
226
- type: SyncEventCheckpointType.FULL,
227
- last_op_id: line.checkpoint.last_op_id,
228
- buckets: line.checkpoint.buckets,
229
- streams: line.checkpoint.streams,
230
- date: new Date()
231
- };
232
- } else {
233
- return {
234
- id: new bson.ObjectId(),
235
- type: SyncEventCheckpointType.DIFF,
236
- last_op_id: line.checkpoint_diff.last_op_id,
237
- updated_buckets: line.checkpoint_diff.updated_buckets,
238
- removed_buckets: line.checkpoint_diff.removed_buckets,
239
- date: new Date()
240
- };
241
- }
242
- }
@@ -8,11 +8,16 @@ import { CompoundConfigCollector } from './util-index.js';
8
8
 
9
9
  export function configureLogger(config?: configFile.LoggingConfig): void {
10
10
  const level = process.env.PS_LOG_LEVEL ?? config?.level ?? DEFAULT_LOG_LEVEL;
11
- const format = (process.env.PS_LOG_FORMAT as configFile.LoggingConfig['format']) ?? config?.format ?? DEFAULT_LOG_FORMAT;
11
+ const format =
12
+ (process.env.PS_LOG_FORMAT as configFile.LoggingConfig['format']) ?? config?.format ?? DEFAULT_LOG_FORMAT;
12
13
  const winstonFormat = format === 'json' ? LogFormat.production : LogFormat.development;
13
14
 
15
+ // We want the user to always be aware that a log level was configured (they might forget they set it in the config and wonder why they aren't seeing logs)
16
+ // We log this using the configured format, but before we configure the level.
17
+ logger.configure({ level: DEFAULT_LOG_LEVEL, format: winstonFormat, transports: [new winston.transports.Console()] });
18
+ logger.info(`Configured logger with level "${level}" and format "${format}"`);
19
+
14
20
  logger.configure({ level, format: winstonFormat, transports: [new winston.transports.Console()] });
15
- console.log(`[PREFLIGHT] Configured logger with level "${level}" and format "${format}"`); // we want the user to always be aware that a log level was configured (they might forget they set it in the config and wonder why they aren't seeing logs)
16
21
  }
17
22
 
18
23
  /**
@@ -70,7 +70,7 @@ describe('JWT Auth', () => {
70
70
  defaultAudiences: ['tests'],
71
71
  maxAge: '6m'
72
72
  });
73
- expect(verified.sub).toEqual('f1');
73
+ expect(verified.userIdJson).toEqual('f1');
74
74
  await expect(
75
75
  store.verifyJwt(signedJwt, {
76
76
  defaultAudiences: ['other'],
@@ -126,6 +126,58 @@ describe('JWT Auth', () => {
126
126
  ).rejects.toThrow('[PSYNC_S2103] JWT has expired');
127
127
  });
128
128
 
129
+ test('KeyStore normalizes sub claim values', async () => {
130
+ const keys = await StaticKeyCollector.importKeys([sharedKey]);
131
+ const store = new KeyStore(keys);
132
+ const signKey = (await jose.importJWK(sharedKey)) as jose.KeyLike;
133
+
134
+ const signWithSub = async (sub: any) => {
135
+ return new jose.SignJWT({ sub })
136
+ .setProtectedHeader({ alg: 'HS256', kid: 'k1' })
137
+ .setIssuedAt()
138
+ .setIssuer('tester')
139
+ .setAudience('tests')
140
+ .setExpirationTime('5m')
141
+ .sign(signKey);
142
+ };
143
+
144
+ const stringToken = await signWithSub('user-1');
145
+ const stringVerified = await store.verifyJwt(stringToken, {
146
+ defaultAudiences: ['tests'],
147
+ maxAge: '6m'
148
+ });
149
+ expect(stringVerified.userIdJson).toEqual('user-1');
150
+ expect(stringVerified.userIdString).toEqual('user-1');
151
+
152
+ const booleanToken = await signWithSub(true);
153
+ const booleanVerified = await store.verifyJwt(booleanToken, {
154
+ defaultAudiences: ['tests'],
155
+ maxAge: '6m'
156
+ });
157
+ expect(booleanVerified.userIdJson).toEqual(1n);
158
+ expect(booleanVerified.userIdString).toEqual('1');
159
+
160
+ const objectSub = { id: 1, name: 'test' };
161
+ const objectToken = await signWithSub(objectSub);
162
+ const objectVerified = await store.verifyJwt(objectToken, {
163
+ defaultAudiences: ['tests'],
164
+ maxAge: '6m'
165
+ });
166
+ // The exact JSON serialization here may change in the future
167
+ expect(objectVerified.userIdJson).toEqual(`{"id":1.0,"name":"test"}`);
168
+ expect(objectVerified.userIdString).toEqual(`{"id":1.0,"name":"test"}`);
169
+
170
+ const arraySub = [1, 'a'];
171
+ const arrayToken = await signWithSub(arraySub);
172
+ const arrayVerified = await store.verifyJwt(arrayToken, {
173
+ defaultAudiences: ['tests'],
174
+ maxAge: '6m'
175
+ });
176
+ // The exact JSON serialization here may change in the future
177
+ expect(arrayVerified.userIdJson).toEqual(`[1.0,"a"]`);
178
+ expect(arrayVerified.userIdString).toEqual(`[1.0,"a"]`);
179
+ });
180
+
129
181
  test('Algorithm validation', async () => {
130
182
  const keys = await StaticKeyCollector.importKeys([publicKeyRSA]);
131
183
  const store = new KeyStore(keys);
@@ -210,12 +262,14 @@ describe('JWT Auth', () => {
210
262
  .setExpirationTime('5m')
211
263
  .sign(signKey2);
212
264
 
213
- await expect(
214
- store.verifyJwt(signedJwt3, {
215
- defaultAudiences: ['tests'],
216
- maxAge: '6m'
217
- })
218
- ).resolves.toMatchObject({ sub: 'f1' });
265
+ expect(
266
+ (
267
+ await store.verifyJwt(signedJwt3, {
268
+ defaultAudiences: ['tests'],
269
+ maxAge: '6m'
270
+ })
271
+ ).parsedPayload
272
+ ).toMatchObject({ sub: 'f1' });
219
273
 
220
274
  // Random kid, matches sharedKey2
221
275
  const signedJwt4 = await new jose.SignJWT({})
@@ -227,12 +281,14 @@ describe('JWT Auth', () => {
227
281
  .setExpirationTime('5m')
228
282
  .sign(signKey2);
229
283
 
230
- await expect(
231
- store.verifyJwt(signedJwt4, {
232
- defaultAudiences: ['tests'],
233
- maxAge: '6m'
234
- })
235
- ).resolves.toMatchObject({ sub: 'f1' });
284
+ expect(
285
+ (
286
+ await store.verifyJwt(signedJwt4, {
287
+ defaultAudiences: ['tests'],
288
+ maxAge: '6m'
289
+ })
290
+ ).parsedPayload
291
+ ).toMatchObject({ sub: 'f1' });
236
292
  });
237
293
 
238
294
  test('KeyOptions', async () => {
@@ -258,7 +314,7 @@ describe('JWT Auth', () => {
258
314
  defaultAudiences: ['tests'],
259
315
  maxAge: '6m'
260
316
  });
261
- expect(verified.sub).toEqual('f1');
317
+ expect(verified.userIdJson).toEqual('f1');
262
318
 
263
319
  const signedJwt2 = await new jose.SignJWT({})
264
320
  .setProtectedHeader({ alg: 'HS256', kid: 'k1' })
@@ -410,12 +466,12 @@ describe('JWT Auth', () => {
410
466
  .setExpirationTime('5m')
411
467
  .sign(signKey);
412
468
 
413
- const verified = (await store.verifyJwt(signedJwt, {
469
+ const verified = await store.verifyJwt(signedJwt, {
414
470
  defaultAudiences: ['tests'],
415
471
  maxAge: '6m'
416
- })) as JwtPayload & { claim: string };
472
+ });
417
473
 
418
- expect(verified.claim).toEqual('test-claim');
474
+ expect(verified.parsedPayload.claim).toEqual('test-claim');
419
475
  });
420
476
 
421
477
  test('signing with ECDSA', async () => {
@@ -432,12 +488,12 @@ describe('JWT Auth', () => {
432
488
  .setExpirationTime('5m')
433
489
  .sign(signKey);
434
490
 
435
- const verified = (await store.verifyJwt(signedJwt, {
491
+ const verified = await store.verifyJwt(signedJwt, {
436
492
  defaultAudiences: ['tests'],
437
493
  maxAge: '6m'
438
- })) as JwtPayload & { claim: string };
494
+ });
439
495
 
440
- expect(verified.claim).toEqual('test-claim-2');
496
+ expect(verified.parsedPayload.claim).toEqual('test-claim-2');
441
497
  });
442
498
 
443
499
  describe('debugKeyNotFound', () => {
@@ -0,0 +1,48 @@
1
+ import { BasicRouterRequest, Context, JwtPayload } from '@/index.js';
2
+ import { logger } from '@powersync/lib-services-framework';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { validate } from '../../../src/routes/endpoints/admin.js';
5
+ import { mockServiceContext } from './mocks.js';
6
+
7
+ describe('admin routes', () => {
8
+ describe('validate', () => {
9
+ it('reports errors with source location', async () => {
10
+ const context: Context = {
11
+ logger: logger,
12
+ service_context: mockServiceContext(null),
13
+ token_payload: new JwtPayload({
14
+ sub: '',
15
+ exp: 0,
16
+ iat: 0
17
+ })
18
+ };
19
+
20
+ const request: BasicRouterRequest = {
21
+ headers: {},
22
+ hostname: '',
23
+ protocol: 'http'
24
+ };
25
+
26
+ const response = await validate.handler({
27
+ context,
28
+ params: {
29
+ sync_rules: `
30
+ bucket_definitions:
31
+ missing_table:
32
+ data:
33
+ - SELECT * FROM missing_table
34
+ `
35
+ },
36
+ request
37
+ });
38
+
39
+ expect(response.errors).toEqual([
40
+ expect.objectContaining({
41
+ level: 'warning',
42
+ location: { start_offset: 70, end_offset: 83 },
43
+ message: 'Table public.missing_table not found'
44
+ })
45
+ ]);
46
+ });
47
+ });
48
+ });
@@ -41,8 +41,29 @@ export function mockServiceContext(storage: Partial<SyncRulesBucketStorage> | nu
41
41
  return {
42
42
  getParseSyncRulesOptions() {
43
43
  return { defaultSchema: 'public' };
44
+ },
45
+ async getSourceConfig() {
46
+ return {
47
+ tag: 'test_tag',
48
+ id: 'test_id',
49
+ type: 'test_type'
50
+ };
51
+ },
52
+ async getConnectionSchema() {
53
+ return [];
54
+ },
55
+ async getConnectionStatus() {
56
+ return {
57
+ id: 'test_id',
58
+ uri: 'http://example.org/',
59
+ connected: true,
60
+ errors: []
61
+ };
62
+ },
63
+ async getDebugTablesInfo() {
64
+ return [];
44
65
  }
45
- } as Partial<RouteAPI>;
66
+ } satisfies Partial<RouteAPI> as unknown as RouteAPI;
46
67
  },
47
68
  addStopHandler() {
48
69
  return () => {};
@@ -1,4 +1,4 @@
1
- import { BasicRouterRequest, Context, SyncRulesBucketStorage } from '@/index.js';
1
+ import { BasicRouterRequest, Context, JwtPayload, SyncRulesBucketStorage } from '@/index.js';
2
2
  import { RouterResponse, ServiceError, logger } from '@powersync/lib-services-framework';
3
3
  import { SqlSyncRules } from '@powersync/service-sync-rules';
4
4
  import { Readable, Writable } from 'stream';
@@ -8,6 +8,7 @@ import winston from 'winston';
8
8
  import { syncStreamed } from '../../../src/routes/endpoints/sync-stream.js';
9
9
  import { DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS, limitParamsForLogging } from '../../../src/util/param-logging.js';
10
10
  import { mockServiceContext } from './mocks.js';
11
+ import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
11
12
 
12
13
  describe('Stream Route', () => {
13
14
  describe('compressed stream', () => {
@@ -15,11 +16,11 @@ describe('Stream Route', () => {
15
16
  const context: Context = {
16
17
  logger: logger,
17
18
  service_context: mockServiceContext(null),
18
- token_payload: {
19
+ token_payload: new JwtPayload({
19
20
  sub: '',
20
21
  exp: 0,
21
22
  iat: 0
22
- }
23
+ })
23
24
  };
24
25
 
25
26
  const request: BasicRouterRequest = {
@@ -45,7 +46,7 @@ describe('Stream Route', () => {
45
46
 
46
47
  const storage = {
47
48
  getParsedSyncRules() {
48
- return new SqlSyncRules('bucket_definitions: {}').hydrate();
49
+ return new SqlSyncRules('bucket_definitions: {}').hydrate({ hydrationState: DEFAULT_HYDRATION_STATE });
49
50
  },
50
51
  watchCheckpointChanges: async function* (options) {
51
52
  throw new Error('Simulated storage error');
@@ -56,11 +57,11 @@ describe('Stream Route', () => {
56
57
  const context: Context = {
57
58
  logger: logger,
58
59
  service_context: serviceContext,
59
- token_payload: {
60
+ token_payload: new JwtPayload({
60
61
  exp: new Date().getTime() / 1000 + 10000,
61
62
  iat: new Date().getTime() / 1000 - 10000,
62
63
  sub: 'test-user'
63
- }
64
+ })
64
65
  };
65
66
 
66
67
  // It may be worth eventually doing this via Fastify to test the full stack
@@ -83,7 +84,7 @@ describe('Stream Route', () => {
83
84
  it('logs the application metadata', async () => {
84
85
  const storage = {
85
86
  getParsedSyncRules() {
86
- return new SqlSyncRules('bucket_definitions: {}').hydrate();
87
+ return new SqlSyncRules('bucket_definitions: {}').hydrate({ hydrationState: DEFAULT_HYDRATION_STATE });
87
88
  },
88
89
  watchCheckpointChanges: async function* (options) {
89
90
  throw new Error('Simulated storage error');
@@ -108,11 +109,11 @@ describe('Stream Route', () => {
108
109
  const context: Context = {
109
110
  logger: testLogger,
110
111
  service_context: serviceContext,
111
- token_payload: {
112
+ token_payload: new JwtPayload({
112
113
  exp: new Date().getTime() / 1000 + 10000,
113
114
  iat: new Date().getTime() / 1000 - 10000,
114
115
  sub: 'test-user'
115
- }
116
+ })
116
117
  };
117
118
 
118
119
  const request: BasicRouterRequest = {