@powersync/service-core 1.18.2 → 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 +35 -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/metrics/open-telemetry/util.js +3 -1
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +1 -0
- package/dist/replication/AbstractReplicator.js +16 -6
- package/dist/replication/AbstractReplicator.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 +8 -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 +3 -6
- 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 +3 -6
- 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 +3 -2
- package/dist/storage/SyncRulesBucketStorage.d.ts +12 -4
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/bson.d.ts +3 -3
- package/dist/storage/bson.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +7 -10
- package/dist/sync/BucketChecksumState.js +16 -15
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js +5 -7
- package/dist/sync/sync.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +5 -2
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config.d.ts +2 -0
- package/dist/util/config.js +15 -2
- package/dist/util/config.js.map +1 -1
- package/package.json +5 -5
- 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/metrics/open-telemetry/util.ts +3 -1
- package/src/replication/AbstractReplicator.ts +15 -7
- package/src/routes/auth.ts +2 -4
- package/src/routes/configure-fastify.ts +1 -1
- package/src/routes/endpoints/admin.ts +8 -2
- package/src/routes/endpoints/checkpointing.ts +5 -3
- package/src/routes/endpoints/socket-route.ts +3 -6
- package/src/routes/endpoints/sync-rules.ts +5 -5
- package/src/routes/endpoints/sync-stream.ts +3 -6
- package/src/routes/router.ts +0 -2
- package/src/storage/PersistedSyncRulesContent.ts +4 -2
- package/src/storage/SyncRulesBucketStorage.ts +13 -4
- package/src/storage/bson.ts +3 -3
- package/src/sync/BucketChecksumState.ts +26 -28
- package/src/sync/sync.ts +12 -14
- package/src/util/config/collectors/config-collector.ts +9 -2
- package/src/util/config.ts +20 -2
- package/test/src/auth.test.ts +76 -20
- package/test/src/config.test.ts +17 -0
- package/test/src/routes/stream.test.ts +9 -9
- package/test/src/sync/BucketChecksumState.test.ts +23 -52
- package/tsconfig.json +0 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +6 -7
|
@@ -2,38 +2,32 @@ import {
|
|
|
2
2
|
BucketDescription,
|
|
3
3
|
BucketPriority,
|
|
4
4
|
BucketSource,
|
|
5
|
+
HydratedSyncRules,
|
|
5
6
|
RequestedStream,
|
|
6
|
-
RequestJwtPayload,
|
|
7
7
|
RequestParameters,
|
|
8
|
-
ResolvedBucket
|
|
9
|
-
SqlSyncRules
|
|
8
|
+
ResolvedBucket
|
|
10
9
|
} from '@powersync/service-sync-rules';
|
|
11
10
|
|
|
12
11
|
import * as storage from '../storage/storage-index.js';
|
|
13
12
|
import * as util from '../util/util-index.js';
|
|
14
13
|
|
|
15
14
|
import {
|
|
15
|
+
logger as defaultLogger,
|
|
16
16
|
ErrorCode,
|
|
17
17
|
Logger,
|
|
18
18
|
ServiceAssertionError,
|
|
19
|
-
ServiceError
|
|
20
|
-
logger as defaultLogger
|
|
19
|
+
ServiceError
|
|
21
20
|
} from '@powersync/lib-services-framework';
|
|
22
|
-
import { JSONBig } from '@powersync/service-jsonbig';
|
|
23
21
|
import { BucketParameterQuerier, QuerierError } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
|
|
22
|
+
import { JwtPayload } from '../auth/JwtPayload.js';
|
|
24
23
|
import { SyncContext } from './SyncContext.js';
|
|
25
24
|
import { getIntersection, hasIntersection } from './util.js';
|
|
26
25
|
|
|
27
|
-
export interface VersionedSyncRules {
|
|
28
|
-
syncRules: SqlSyncRules;
|
|
29
|
-
version: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
export interface BucketChecksumStateOptions {
|
|
33
27
|
syncContext: SyncContext;
|
|
34
28
|
bucketStorage: BucketChecksumStateStorage;
|
|
35
|
-
syncRules:
|
|
36
|
-
tokenPayload:
|
|
29
|
+
syncRules: HydratedSyncRules;
|
|
30
|
+
tokenPayload: JwtPayload;
|
|
37
31
|
syncRequest: util.StreamingSyncRequest;
|
|
38
32
|
logger?: Logger;
|
|
39
33
|
}
|
|
@@ -118,7 +112,7 @@ export class BucketChecksumState {
|
|
|
118
112
|
*/
|
|
119
113
|
async buildNextCheckpointLine(next: storage.StorageCheckpointUpdate): Promise<CheckpointLine | null> {
|
|
120
114
|
const { writeCheckpoint, base } = next;
|
|
121
|
-
const
|
|
115
|
+
const userIdForLogs = this.parameterState.syncParams.userId;
|
|
122
116
|
|
|
123
117
|
const storage = this.bucketStorage;
|
|
124
118
|
|
|
@@ -226,7 +220,7 @@ export class BucketChecksumState {
|
|
|
226
220
|
message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
|
|
227
221
|
this.logger.info(message, {
|
|
228
222
|
checkpoint: base.checkpoint,
|
|
229
|
-
user_id:
|
|
223
|
+
user_id: userIdForLogs,
|
|
230
224
|
buckets: allBuckets.length,
|
|
231
225
|
updated: diff.updatedBuckets.length,
|
|
232
226
|
removed: diff.removedBuckets.length
|
|
@@ -245,7 +239,7 @@ export class BucketChecksumState {
|
|
|
245
239
|
deferredLog = () => {
|
|
246
240
|
let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
|
|
247
241
|
message += `buckets: ${allBuckets.length} ${limitedBuckets(allBuckets, 20)}`;
|
|
248
|
-
this.logger.info(message, { checkpoint: base.checkpoint, user_id:
|
|
242
|
+
this.logger.info(message, { checkpoint: base.checkpoint, user_id: userIdForLogs, buckets: allBuckets.length });
|
|
249
243
|
};
|
|
250
244
|
bucketsToFetch = allBuckets.map((b) => ({ bucket: b.bucket, priority: b.priority }));
|
|
251
245
|
|
|
@@ -253,7 +247,7 @@ export class BucketChecksumState {
|
|
|
253
247
|
const streamNameToIndex = new Map<string, number>();
|
|
254
248
|
this.streamNameToIndex = streamNameToIndex;
|
|
255
249
|
|
|
256
|
-
for (const source of this.parameterState.syncRules.
|
|
250
|
+
for (const source of this.parameterState.syncRules.definition.bucketSources) {
|
|
257
251
|
if (this.parameterState.isSubscribedToStream(source)) {
|
|
258
252
|
streamNameToIndex.set(source.name, subscriptions.length);
|
|
259
253
|
|
|
@@ -381,7 +375,7 @@ export interface CheckpointUpdate {
|
|
|
381
375
|
export class BucketParameterState {
|
|
382
376
|
private readonly context: SyncContext;
|
|
383
377
|
public readonly bucketStorage: BucketChecksumStateStorage;
|
|
384
|
-
public readonly syncRules:
|
|
378
|
+
public readonly syncRules: HydratedSyncRules;
|
|
385
379
|
public readonly syncParams: RequestParameters;
|
|
386
380
|
private readonly querier: BucketParameterQuerier;
|
|
387
381
|
/**
|
|
@@ -399,13 +393,13 @@ export class BucketParameterState {
|
|
|
399
393
|
private cachedDynamicBuckets: ResolvedBucket[] | null = null;
|
|
400
394
|
private cachedDynamicBucketSet: Set<string> | null = null;
|
|
401
395
|
|
|
402
|
-
private
|
|
396
|
+
private lookupsFromPreviousCheckpoint: Set<string> | null = null;
|
|
403
397
|
|
|
404
398
|
constructor(
|
|
405
399
|
context: SyncContext,
|
|
406
400
|
bucketStorage: BucketChecksumStateStorage,
|
|
407
|
-
syncRules:
|
|
408
|
-
tokenPayload:
|
|
401
|
+
syncRules: HydratedSyncRules,
|
|
402
|
+
tokenPayload: JwtPayload,
|
|
409
403
|
request: util.StreamingSyncRequest,
|
|
410
404
|
logger: Logger
|
|
411
405
|
) {
|
|
@@ -436,11 +430,10 @@ export class BucketParameterState {
|
|
|
436
430
|
this.includeDefaultStreams = subscriptions?.include_defaults ?? true;
|
|
437
431
|
this.explicitStreamSubscriptions = explicitStreamSubscriptions;
|
|
438
432
|
|
|
439
|
-
const { querier, errors } = syncRules.
|
|
433
|
+
const { querier, errors } = syncRules.getBucketParameterQuerier({
|
|
440
434
|
globalParameters: this.syncParams,
|
|
441
435
|
hasDefaultStreams: this.includeDefaultStreams,
|
|
442
|
-
streams: streamsByName
|
|
443
|
-
bucketIdTransformer: SqlSyncRules.versionedBucketIdTransformer(`${syncRules.version}`)
|
|
436
|
+
streams: streamsByName
|
|
444
437
|
});
|
|
445
438
|
this.querier = querier;
|
|
446
439
|
this.streamErrors = Object.groupBy(errors, (e) => e.descriptor) as Record<string, QuerierError[]>;
|
|
@@ -448,7 +441,6 @@ export class BucketParameterState {
|
|
|
448
441
|
this.staticBuckets = new Map<string, ResolvedBucket>(
|
|
449
442
|
mergeBuckets(this.querier.staticBuckets).map((b) => [b.bucket, b])
|
|
450
443
|
);
|
|
451
|
-
this.lookups = new Set<string>(this.querier.parameterQueryLookups.map((l) => JSONBig.stringify(l.values)));
|
|
452
444
|
this.subscribedStreamNames = new Set(Object.keys(streamsByName));
|
|
453
445
|
}
|
|
454
446
|
|
|
@@ -547,7 +539,6 @@ export class BucketParameterState {
|
|
|
547
539
|
*/
|
|
548
540
|
private async getCheckpointUpdateDynamic(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate> {
|
|
549
541
|
const querier = this.querier;
|
|
550
|
-
const storage = this.bucketStorage;
|
|
551
542
|
const staticBuckets = this.staticBuckets.values();
|
|
552
543
|
const update = checkpoint.update;
|
|
553
544
|
|
|
@@ -561,10 +552,10 @@ export class BucketParameterState {
|
|
|
561
552
|
invalidateDataBuckets = true;
|
|
562
553
|
}
|
|
563
554
|
|
|
564
|
-
if (update.invalidateParameterBuckets) {
|
|
555
|
+
if (update.invalidateParameterBuckets || this.lookupsFromPreviousCheckpoint == null) {
|
|
565
556
|
hasParameterChange = true;
|
|
566
557
|
} else {
|
|
567
|
-
if (hasIntersection(this.
|
|
558
|
+
if (hasIntersection(this.lookupsFromPreviousCheckpoint, update.updatedParameterLookups)) {
|
|
568
559
|
// This is a very coarse re-check of all queries
|
|
569
560
|
hasParameterChange = true;
|
|
570
561
|
}
|
|
@@ -572,13 +563,20 @@ export class BucketParameterState {
|
|
|
572
563
|
|
|
573
564
|
let dynamicBuckets: ResolvedBucket[];
|
|
574
565
|
if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
|
|
566
|
+
const recordedLookups = new Set<string>();
|
|
567
|
+
|
|
575
568
|
dynamicBuckets = await querier.queryDynamicBucketDescriptions({
|
|
576
569
|
getParameterSets(lookups) {
|
|
570
|
+
for (const lookup of lookups) {
|
|
571
|
+
recordedLookups.add(lookup.serializedRepresentation);
|
|
572
|
+
}
|
|
573
|
+
|
|
577
574
|
return checkpoint.base.getParameterSets(lookups);
|
|
578
575
|
}
|
|
579
576
|
});
|
|
580
577
|
this.cachedDynamicBuckets = dynamicBuckets;
|
|
581
578
|
this.cachedDynamicBucketSet = new Set<string>(dynamicBuckets.map((b) => b.bucket));
|
|
579
|
+
this.lookupsFromPreviousCheckpoint = recordedLookups;
|
|
582
580
|
invalidateDataBuckets = true;
|
|
583
581
|
} else {
|
|
584
582
|
dynamicBuckets = this.cachedDynamicBuckets;
|
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
|
|
|
@@ -9,7 +9,7 @@ import * as util from '../util/util-index.js';
|
|
|
9
9
|
|
|
10
10
|
import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
11
11
|
import { mergeAsyncIterables } from '../streams/streams-index.js';
|
|
12
|
-
import { BucketChecksumState, CheckpointLine
|
|
12
|
+
import { BucketChecksumState, CheckpointLine } from './BucketChecksumState.js';
|
|
13
13
|
import { OperationsSentStats, RequestTracker, statsForBatch } from './RequestTracker.js';
|
|
14
14
|
import { SyncContext } from './SyncContext.js';
|
|
15
15
|
import { TokenStreamOptions, acquireSemaphoreAbortable, settledPromise, tokenStream } from './util.js';
|
|
@@ -17,7 +17,7 @@ import { TokenStreamOptions, acquireSemaphoreAbortable, settledPromise, tokenStr
|
|
|
17
17
|
export interface SyncStreamParameters {
|
|
18
18
|
syncContext: SyncContext;
|
|
19
19
|
bucketStorage: storage.SyncRulesBucketStorage;
|
|
20
|
-
syncRules:
|
|
20
|
+
syncRules: HydratedSyncRules;
|
|
21
21
|
params: util.StreamingSyncRequest;
|
|
22
22
|
token: auth.JwtPayload;
|
|
23
23
|
logger?: Logger;
|
|
@@ -94,18 +94,15 @@ export async function* streamResponse(
|
|
|
94
94
|
async function* streamResponseInner(
|
|
95
95
|
syncContext: SyncContext,
|
|
96
96
|
bucketStorage: storage.SyncRulesBucketStorage,
|
|
97
|
-
syncRules:
|
|
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();
|
|
@@ -70,13 +70,20 @@ export abstract class ConfigCollector {
|
|
|
70
70
|
return this.parseJSON(content);
|
|
71
71
|
default: {
|
|
72
72
|
// No content type provided, need to try both
|
|
73
|
+
let yamlError: unknown;
|
|
73
74
|
try {
|
|
74
75
|
return this.parseYaml(content);
|
|
75
|
-
} catch (ex) {
|
|
76
|
+
} catch (ex) {
|
|
77
|
+
yamlError = ex;
|
|
78
|
+
}
|
|
76
79
|
try {
|
|
77
80
|
return this.parseJSON(content);
|
|
78
81
|
} catch (ex) {
|
|
79
|
-
throw new Error(
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Could not parse PowerSync config file content as JSON or YAML: JSON Error: ${ex}${
|
|
84
|
+
yamlError ? `\nYAML Error: ${yamlError}` : ''
|
|
85
|
+
}`
|
|
86
|
+
);
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
}
|
package/src/util/config.ts
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
|
+
import winston from 'winston';
|
|
2
3
|
|
|
3
|
-
import { container } from '@powersync/lib-services-framework';
|
|
4
|
+
import { container, logger, LogFormat, DEFAULT_LOG_LEVEL, DEFAULT_LOG_FORMAT } from '@powersync/lib-services-framework';
|
|
5
|
+
import { configFile } from '@powersync/service-types';
|
|
4
6
|
import { ResolvedPowerSyncConfig, RunnerConfig } from './config/types.js';
|
|
5
7
|
import { CompoundConfigCollector } from './util-index.js';
|
|
6
8
|
|
|
9
|
+
export function configureLogger(config?: configFile.LoggingConfig): void {
|
|
10
|
+
const level = process.env.PS_LOG_LEVEL ?? config?.level ?? DEFAULT_LOG_LEVEL;
|
|
11
|
+
const format =
|
|
12
|
+
(process.env.PS_LOG_FORMAT as configFile.LoggingConfig['format']) ?? config?.format ?? DEFAULT_LOG_FORMAT;
|
|
13
|
+
const winstonFormat = format === 'json' ? LogFormat.production : LogFormat.development;
|
|
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
|
+
|
|
20
|
+
logger.configure({ level, format: winstonFormat, transports: [new winston.transports.Console()] });
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
24
|
* Loads the resolved config using the registered config collector
|
|
9
25
|
*/
|
|
10
26
|
export async function loadConfig(runnerConfig: RunnerConfig) {
|
|
11
27
|
const collector = container.getImplementation(CompoundConfigCollector);
|
|
12
|
-
|
|
28
|
+
const config = await collector.collectConfig(runnerConfig);
|
|
29
|
+
configureLogger(config.base_config.system?.logging);
|
|
30
|
+
return config;
|
|
13
31
|
}
|
|
14
32
|
|
|
15
33
|
export async function loadSyncRules(config: ResolvedPowerSyncConfig): Promise<string | undefined> {
|
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', () => {
|
package/test/src/config.test.ts
CHANGED
|
@@ -69,4 +69,21 @@ describe('Config', () => {
|
|
|
69
69
|
|
|
70
70
|
expect(config.api_parameters.max_buckets_per_connection).toBe(1);
|
|
71
71
|
});
|
|
72
|
+
it('should throw YAML validation error for invalid base64 config', {}, async () => {
|
|
73
|
+
const yamlConfig = /* yaml */ `
|
|
74
|
+
# PowerSync config
|
|
75
|
+
replication:
|
|
76
|
+
connections: []
|
|
77
|
+
storage:
|
|
78
|
+
type: !env INVALID_VAR
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const collector = new CompoundConfigCollector();
|
|
82
|
+
|
|
83
|
+
await expect(
|
|
84
|
+
collector.collectConfig({
|
|
85
|
+
config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
|
|
86
|
+
})
|
|
87
|
+
).rejects.toThrow(/YAML Error:[\s\S]*Attempting to substitute environment variable INVALID_VAR/);
|
|
88
|
+
});
|
|
72
89
|
});
|
|
@@ -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 = {
|
|
@@ -45,7 +45,7 @@ describe('Stream Route', () => {
|
|
|
45
45
|
|
|
46
46
|
const storage = {
|
|
47
47
|
getParsedSyncRules() {
|
|
48
|
-
return new SqlSyncRules('bucket_definitions: {}');
|
|
48
|
+
return new SqlSyncRules('bucket_definitions: {}').hydrate();
|
|
49
49
|
},
|
|
50
50
|
watchCheckpointChanges: async function* (options) {
|
|
51
51
|
throw new Error('Simulated storage error');
|
|
@@ -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
|
|
@@ -83,7 +83,7 @@ describe('Stream Route', () => {
|
|
|
83
83
|
it('logs the application metadata', async () => {
|
|
84
84
|
const storage = {
|
|
85
85
|
getParsedSyncRules() {
|
|
86
|
-
return new SqlSyncRules('bucket_definitions: {}');
|
|
86
|
+
return new SqlSyncRules('bucket_definitions: {}').hydrate();
|
|
87
87
|
},
|
|
88
88
|
watchCheckpointChanges: async function* (options) {
|
|
89
89
|
throw new Error('Simulated storage error');
|
|
@@ -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 = {
|