@powersync/service-core 1.19.0 → 1.19.1
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 +13 -0
- package/dist/api/RouteAPI.d.ts +2 -2
- package/dist/api/diagnostics.js +4 -3
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/JwtPayload.d.ts +7 -8
- package/dist/auth/JwtPayload.js +19 -1
- package/dist/auth/JwtPayload.js.map +1 -1
- package/dist/auth/KeyStore.js +2 -1
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/routes/auth.d.ts +0 -1
- package/dist/routes/auth.js +2 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/configure-fastify.js +1 -1
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/admin.d.ts +3 -0
- package/dist/routes/endpoints/admin.js +5 -2
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +3 -3
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +2 -2
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +5 -5
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +2 -2
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/router.d.ts +0 -1
- package/dist/routes/router.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +2 -2
- package/dist/storage/SyncRulesBucketStorage.d.ts +8 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +4 -3
- package/dist/sync/BucketChecksumState.js +3 -3
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.js +5 -7
- package/dist/sync/sync.js.map +1 -1
- package/package.json +4 -4
- package/src/api/RouteAPI.ts +2 -2
- package/src/api/diagnostics.ts +5 -4
- package/src/auth/JwtPayload.ts +16 -8
- package/src/auth/KeyStore.ts +1 -1
- package/src/routes/auth.ts +2 -4
- package/src/routes/configure-fastify.ts +1 -1
- package/src/routes/endpoints/admin.ts +5 -2
- package/src/routes/endpoints/checkpointing.ts +5 -3
- package/src/routes/endpoints/socket-route.ts +2 -2
- package/src/routes/endpoints/sync-rules.ts +5 -5
- package/src/routes/endpoints/sync-stream.ts +2 -2
- package/src/routes/router.ts +0 -2
- package/src/storage/PersistedSyncRulesContent.ts +2 -2
- package/src/storage/SyncRulesBucketStorage.ts +9 -0
- package/src/sync/BucketChecksumState.ts +6 -7
- package/src/sync/sync.ts +9 -11
- package/test/src/auth.test.ts +76 -20
- package/test/src/routes/stream.test.ts +7 -7
- package/test/src/sync/BucketChecksumState.test.ts +7 -6
- package/tsconfig.tsbuildinfo +1 -1
package/src/sync/sync.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
|
|
2
|
-
import { BucketDescription, BucketPriority,
|
|
2
|
+
import { BucketDescription, BucketPriority, HydratedSyncRules, SqliteJsonValue } from '@powersync/service-sync-rules';
|
|
3
3
|
|
|
4
4
|
import { AbortError } from 'ix/aborterror.js';
|
|
5
5
|
|
|
@@ -96,16 +96,13 @@ async function* streamResponseInner(
|
|
|
96
96
|
bucketStorage: storage.SyncRulesBucketStorage,
|
|
97
97
|
syncRules: HydratedSyncRules,
|
|
98
98
|
params: util.StreamingSyncRequest,
|
|
99
|
-
tokenPayload:
|
|
99
|
+
tokenPayload: auth.JwtPayload,
|
|
100
100
|
tracker: RequestTracker,
|
|
101
101
|
signal: AbortSignal,
|
|
102
102
|
logger: Logger,
|
|
103
103
|
isEncodingAsBson: boolean
|
|
104
104
|
): AsyncGenerator<util.StreamingSyncLine | string | null> {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const userId = tokenPayload.sub;
|
|
108
|
-
const checkpointUserId = util.checkpointUserId(userId as string, params.client_id);
|
|
105
|
+
const checkpointUserId = util.checkpointUserId(tokenPayload.userIdString, params.client_id);
|
|
109
106
|
|
|
110
107
|
const checksumState = new BucketChecksumState({
|
|
111
108
|
syncContext,
|
|
@@ -236,7 +233,7 @@ async function* streamResponseInner(
|
|
|
236
233
|
onRowsSent: markOperationsSent,
|
|
237
234
|
abort_connection: signal,
|
|
238
235
|
abort_batch: abortCheckpointSignal,
|
|
239
|
-
|
|
236
|
+
userIdForLogs: tokenPayload.userIdJson,
|
|
240
237
|
// Passing null here will emit a full sync complete message at the end. If we pass a priority, we'll emit a partial
|
|
241
238
|
// sync complete message instead.
|
|
242
239
|
forPriority: !isLast ? priority : null,
|
|
@@ -270,7 +267,8 @@ interface BucketDataRequest {
|
|
|
270
267
|
* This signal also fires when abort_connection fires.
|
|
271
268
|
*/
|
|
272
269
|
abort_batch: AbortSignal;
|
|
273
|
-
|
|
270
|
+
/** User id for debug purposes, not for sync rules. */
|
|
271
|
+
userIdForLogs?: SqliteJsonValue;
|
|
274
272
|
forPriority: BucketPriority | null;
|
|
275
273
|
onRowsSent: (stats: OperationsSentStats) => void;
|
|
276
274
|
logger: Logger;
|
|
@@ -333,7 +331,7 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
|
|
|
333
331
|
let checkpointInvalidated = false;
|
|
334
332
|
|
|
335
333
|
if (syncContext.syncSemaphore.isLocked()) {
|
|
336
|
-
logger.info('Sync concurrency limit reached, waiting for lock', { user_id: request.
|
|
334
|
+
logger.info('Sync concurrency limit reached, waiting for lock', { user_id: request.userIdForLogs });
|
|
337
335
|
}
|
|
338
336
|
const acquired = await acquireSemaphoreAbortable(syncContext.syncSemaphore, AbortSignal.any([abort_batch]));
|
|
339
337
|
if (acquired === 'aborted') {
|
|
@@ -346,7 +344,7 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
|
|
|
346
344
|
// This can be noisy, so we only log when we get close to the
|
|
347
345
|
// concurrency limit.
|
|
348
346
|
logger.info(`Got sync lock. Slots available: ${value - 1}`, {
|
|
349
|
-
user_id: request.
|
|
347
|
+
user_id: request.userIdForLogs,
|
|
350
348
|
sync_data_slots: value - 1
|
|
351
349
|
});
|
|
352
350
|
}
|
|
@@ -429,7 +427,7 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
|
|
|
429
427
|
// This can be noisy, so we only log when we get close to the
|
|
430
428
|
// concurrency limit.
|
|
431
429
|
logger.info(`Releasing sync lock`, {
|
|
432
|
-
user_id: request.
|
|
430
|
+
user_id: request.userIdForLogs
|
|
433
431
|
});
|
|
434
432
|
}
|
|
435
433
|
release();
|
package/test/src/auth.test.ts
CHANGED
|
@@ -70,7 +70,7 @@ describe('JWT Auth', () => {
|
|
|
70
70
|
defaultAudiences: ['tests'],
|
|
71
71
|
maxAge: '6m'
|
|
72
72
|
});
|
|
73
|
-
expect(verified.
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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.
|
|
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 =
|
|
469
|
+
const verified = await store.verifyJwt(signedJwt, {
|
|
414
470
|
defaultAudiences: ['tests'],
|
|
415
471
|
maxAge: '6m'
|
|
416
|
-
})
|
|
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 =
|
|
491
|
+
const verified = await store.verifyJwt(signedJwt, {
|
|
436
492
|
defaultAudiences: ['tests'],
|
|
437
493
|
maxAge: '6m'
|
|
438
|
-
})
|
|
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', () => {
|
|
@@ -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';
|
|
@@ -15,11 +15,11 @@ describe('Stream Route', () => {
|
|
|
15
15
|
const context: Context = {
|
|
16
16
|
logger: logger,
|
|
17
17
|
service_context: mockServiceContext(null),
|
|
18
|
-
token_payload: {
|
|
18
|
+
token_payload: new JwtPayload({
|
|
19
19
|
sub: '',
|
|
20
20
|
exp: 0,
|
|
21
21
|
iat: 0
|
|
22
|
-
}
|
|
22
|
+
})
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const request: BasicRouterRequest = {
|
|
@@ -56,11 +56,11 @@ describe('Stream Route', () => {
|
|
|
56
56
|
const context: Context = {
|
|
57
57
|
logger: logger,
|
|
58
58
|
service_context: serviceContext,
|
|
59
|
-
token_payload: {
|
|
59
|
+
token_payload: new JwtPayload({
|
|
60
60
|
exp: new Date().getTime() / 1000 + 10000,
|
|
61
61
|
iat: new Date().getTime() / 1000 - 10000,
|
|
62
62
|
sub: 'test-user'
|
|
63
|
-
}
|
|
63
|
+
})
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
// It may be worth eventually doing this via Fastify to test the full stack
|
|
@@ -108,11 +108,11 @@ describe('Stream Route', () => {
|
|
|
108
108
|
const context: Context = {
|
|
109
109
|
logger: testLogger,
|
|
110
110
|
service_context: serviceContext,
|
|
111
|
-
token_payload: {
|
|
111
|
+
token_payload: new JwtPayload({
|
|
112
112
|
exp: new Date().getTime() / 1000 + 10000,
|
|
113
113
|
iat: new Date().getTime() / 1000 - 10000,
|
|
114
114
|
sub: 'test-user'
|
|
115
|
-
}
|
|
115
|
+
})
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
const request: BasicRouterRequest = {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CHECKPOINT_INVALIDATE_ALL,
|
|
7
7
|
ChecksumMap,
|
|
8
8
|
InternalOpId,
|
|
9
|
+
JwtPayload,
|
|
9
10
|
ReplicationCheckpoint,
|
|
10
11
|
StreamingSyncRequest,
|
|
11
12
|
SyncContext,
|
|
@@ -26,7 +27,7 @@ bucket_definitions:
|
|
|
26
27
|
data: []
|
|
27
28
|
`,
|
|
28
29
|
{ defaultSchema: 'public' }
|
|
29
|
-
).hydrate({ hydrationState: versionedHydrationState(1) });
|
|
30
|
+
).config.hydrate({ hydrationState: versionedHydrationState(1) });
|
|
30
31
|
|
|
31
32
|
// global[1] and global[2]
|
|
32
33
|
const SYNC_RULES_GLOBAL_TWO = SqlSyncRules.fromYaml(
|
|
@@ -39,7 +40,7 @@ bucket_definitions:
|
|
|
39
40
|
data: []
|
|
40
41
|
`,
|
|
41
42
|
{ defaultSchema: 'public' }
|
|
42
|
-
).hydrate({ hydrationState: versionedHydrationState(2) });
|
|
43
|
+
).config.hydrate({ hydrationState: versionedHydrationState(2) });
|
|
43
44
|
|
|
44
45
|
// by_project[n]
|
|
45
46
|
const SYNC_RULES_DYNAMIC = SqlSyncRules.fromYaml(
|
|
@@ -50,7 +51,7 @@ bucket_definitions:
|
|
|
50
51
|
data: []
|
|
51
52
|
`,
|
|
52
53
|
{ defaultSchema: 'public' }
|
|
53
|
-
).hydrate({ hydrationState: versionedHydrationState(3) });
|
|
54
|
+
).config.hydrate({ hydrationState: versionedHydrationState(3) });
|
|
54
55
|
|
|
55
56
|
const syncContext = new SyncContext({
|
|
56
57
|
maxBuckets: 100,
|
|
@@ -59,7 +60,7 @@ bucket_definitions:
|
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
const syncRequest: StreamingSyncRequest = {};
|
|
62
|
-
const tokenPayload
|
|
63
|
+
const tokenPayload = new JwtPayload({ sub: '' });
|
|
63
64
|
|
|
64
65
|
test('global bucket with update', async () => {
|
|
65
66
|
const storage = new MockBucketChecksumStateStorage();
|
|
@@ -497,7 +498,7 @@ bucket_definitions:
|
|
|
497
498
|
|
|
498
499
|
const state = new BucketChecksumState({
|
|
499
500
|
syncContext,
|
|
500
|
-
tokenPayload: { sub: 'u1' },
|
|
501
|
+
tokenPayload: new JwtPayload({ sub: 'u1' }),
|
|
501
502
|
syncRequest,
|
|
502
503
|
syncRules: SYNC_RULES_DYNAMIC,
|
|
503
504
|
bucketStorage: storage
|
|
@@ -615,7 +616,7 @@ config:
|
|
|
615
616
|
|
|
616
617
|
const rules = SqlSyncRules.fromYaml(source, {
|
|
617
618
|
defaultSchema: 'public'
|
|
618
|
-
}).hydrate({ hydrationState: versionedHydrationState(1) });
|
|
619
|
+
}).config.hydrate({ hydrationState: versionedHydrationState(1) });
|
|
619
620
|
|
|
620
621
|
return new BucketChecksumState({
|
|
621
622
|
syncContext,
|