@powersync/service-core 1.11.3 → 1.12.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 +56 -0
- package/dist/api/RouteAPI.d.ts +0 -4
- package/dist/auth/CachedKeyCollector.js +2 -7
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/CompoundKeyCollector.js.map +1 -1
- package/dist/auth/KeyCollector.d.ts +2 -2
- package/dist/auth/KeyStore.js +32 -14
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.d.ts +1 -0
- package/dist/auth/RemoteJWKSCollector.js +39 -16
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/auth-index.d.ts +1 -0
- package/dist/auth/auth-index.js +1 -0
- package/dist/auth/auth-index.js.map +1 -1
- package/dist/auth/utils.d.ts +6 -0
- package/dist/auth/utils.js +97 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/entry/commands/compact-action.js +4 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/migrate-action.js +4 -1
- package/dist/entry/commands/migrate-action.js.map +1 -1
- package/dist/entry/commands/test-connection-action.js +4 -1
- package/dist/entry/commands/test-connection-action.js.map +1 -1
- package/dist/routes/RouterEngine.d.ts +2 -0
- package/dist/routes/RouterEngine.js +15 -10
- package/dist/routes/RouterEngine.js.map +1 -1
- package/dist/routes/auth.d.ts +5 -16
- package/dist/routes/auth.js +6 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +3 -21
- package/dist/routes/configure-fastify.js +3 -6
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/configure-rsocket.js +28 -14
- package/dist/routes/configure-rsocket.js.map +1 -1
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.d.ts +4 -28
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/route-endpoints-index.d.ts +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +22 -8
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +6 -6
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +2 -14
- package/dist/routes/endpoints/sync-stream.js +28 -9
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.js +10 -6
- package/dist/routes/route-register.js.map +1 -1
- package/dist/routes/router.d.ts +8 -7
- package/dist/routes/router.js.map +1 -1
- package/dist/runner/teardown.js +4 -1
- package/dist/runner/teardown.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +40 -18
- package/dist/sync/BucketChecksumState.js +122 -74
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +22 -1
- package/dist/sync/RequestTracker.js +51 -2
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.d.ts +3 -5
- package/dist/sync/sync.js +49 -34
- package/dist/sync/sync.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +19 -4
- package/dist/system/ServiceContext.js +20 -8
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +4 -33
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/yaml-env.d.ts +7 -0
- package/dist/util/config/collectors/impl/yaml-env.js +59 -0
- package/dist/util/config/collectors/impl/yaml-env.js.map +1 -0
- package/dist/util/config/compound-config-collector.js +18 -1
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +11 -0
- package/dist/util/protocol-types.d.ts +9 -9
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/utils.d.ts +1 -1
- package/package.json +6 -7
- package/src/api/RouteAPI.ts +0 -5
- package/src/auth/CachedKeyCollector.ts +4 -6
- package/src/auth/CompoundKeyCollector.ts +2 -1
- package/src/auth/KeyCollector.ts +2 -2
- package/src/auth/KeyStore.ts +45 -20
- package/src/auth/RemoteJWKSCollector.ts +39 -16
- package/src/auth/auth-index.ts +1 -0
- package/src/auth/utils.ts +102 -0
- package/src/entry/commands/compact-action.ts +4 -1
- package/src/entry/commands/migrate-action.ts +4 -1
- package/src/entry/commands/test-connection-action.ts +4 -1
- package/src/routes/RouterEngine.ts +21 -11
- package/src/routes/auth.ts +7 -6
- package/src/routes/configure-fastify.ts +6 -8
- package/src/routes/configure-rsocket.ts +33 -18
- package/src/routes/endpoints/admin.ts +5 -5
- package/src/routes/endpoints/checkpointing.ts +2 -2
- package/src/routes/endpoints/route-endpoints-index.ts +1 -0
- package/src/routes/endpoints/socket-route.ts +27 -11
- package/src/routes/endpoints/sync-rules.ts +10 -10
- package/src/routes/endpoints/sync-stream.ts +34 -11
- package/src/routes/route-register.ts +10 -7
- package/src/routes/router.ts +11 -4
- package/src/runner/teardown.ts +5 -1
- package/src/sync/BucketChecksumState.ts +162 -77
- package/src/sync/RequestTracker.ts +70 -3
- package/src/sync/sync.ts +72 -49
- package/src/system/ServiceContext.ts +31 -12
- package/src/util/config/collectors/config-collector.ts +4 -40
- package/src/util/config/collectors/impl/yaml-env.ts +67 -0
- package/src/util/config/compound-config-collector.ts +22 -5
- package/src/util/config/types.ts +13 -0
- package/src/util/protocol-types.ts +15 -10
- package/test/src/auth.test.ts +29 -11
- package/test/src/config.test.ts +72 -0
- package/test/src/sync/BucketChecksumState.test.ts +32 -18
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol-types.js","sourceRoot":"","sources":["../../src/util/protocol-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"protocol-types.js","sourceRoot":"","sources":["../../src/util/protocol-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAI9B,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,CAAC,MAAM;IAEd;;OAEG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM;CAChB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C;;OAEG;IACH,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE;IAE1C;;OAEG;IACH,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;IAElC;;OAEG;IACH,gBAAgB,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE;IAEtC;;OAEG;IACH,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE;IAE9B;;OAEG;IACH,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE;IAEjC;;OAEG;IACH,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IAEtC;;OAEG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC"}
|
package/dist/util/utils.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export declare function isCompleteRow(storeData: boolean, row: sync_rules.Toasta
|
|
|
46
46
|
*
|
|
47
47
|
* Used for tests.
|
|
48
48
|
*/
|
|
49
|
-
export declare function reduceBucket(operations: OplogEntry[]): OplogEntry[];
|
|
49
|
+
export declare function reduceBucket(operations: OplogEntry[]): OplogEntry<import("./protocol-types.js").StoredOplogData>[];
|
|
50
50
|
/**
|
|
51
51
|
* Flattens string to reduce memory usage (around 320 bytes -> 120 bytes),
|
|
52
52
|
* at the cost of some upfront CPU usage.
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.12.1",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"type": "module",
|
|
@@ -29,19 +29,18 @@
|
|
|
29
29
|
"node-fetch": "^3.3.2",
|
|
30
30
|
"ts-codec": "^1.3.0",
|
|
31
31
|
"uri-js": "^4.4.1",
|
|
32
|
-
"uuid": "^
|
|
32
|
+
"uuid": "^11.1.0",
|
|
33
33
|
"winston": "^3.13.0",
|
|
34
34
|
"yaml": "^2.3.2",
|
|
35
|
-
"@powersync/lib-services-framework": "0.
|
|
35
|
+
"@powersync/lib-services-framework": "0.6.0",
|
|
36
36
|
"@powersync/service-jsonbig": "0.17.10",
|
|
37
|
-
"@powersync/service-rsocket-router": "0.0
|
|
38
|
-
"@powersync/service-sync-rules": "0.
|
|
39
|
-
"@powersync/service-types": "0.
|
|
37
|
+
"@powersync/service-rsocket-router": "0.1.0",
|
|
38
|
+
"@powersync/service-sync-rules": "0.27.0",
|
|
39
|
+
"@powersync/service-types": "0.11.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/async": "^3.2.24",
|
|
43
43
|
"@types/lodash": "^4.17.5",
|
|
44
|
-
"@types/uuid": "^9.0.4",
|
|
45
44
|
"fastify": "4.23.2",
|
|
46
45
|
"fastify-plugin": "^4.5.1"
|
|
47
46
|
},
|
package/src/api/RouteAPI.ts
CHANGED
|
@@ -49,11 +49,6 @@ export interface RouteAPI {
|
|
|
49
49
|
*/
|
|
50
50
|
getReplicationLag(options: ReplicationLagOptions): Promise<number | undefined>;
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Get the current LSN or equivalent replication HEAD position identifier
|
|
54
|
-
*/
|
|
55
|
-
getReplicationHead(): Promise<string>;
|
|
56
|
-
|
|
57
52
|
/**
|
|
58
53
|
* Get the current LSN or equivalent replication HEAD position identifier.
|
|
59
54
|
*
|
|
@@ -3,6 +3,8 @@ import timers from 'timers/promises';
|
|
|
3
3
|
import { KeySpec } from './KeySpec.js';
|
|
4
4
|
import { LeakyBucket } from './LeakyBucket.js';
|
|
5
5
|
import { KeyCollector, KeyResult } from './KeyCollector.js';
|
|
6
|
+
import { AuthorizationError } from '@powersync/lib-services-framework';
|
|
7
|
+
import { mapAuthConfigError } from './utils.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Manages caching and refreshing for a key collector.
|
|
@@ -39,7 +41,7 @@ export class CachedKeyCollector implements KeyCollector {
|
|
|
39
41
|
*/
|
|
40
42
|
private keyExpiry = 3600000;
|
|
41
43
|
|
|
42
|
-
private currentErrors:
|
|
44
|
+
private currentErrors: AuthorizationError[] = [];
|
|
43
45
|
/**
|
|
44
46
|
* Indicates a "fatal" error that should be retried.
|
|
45
47
|
*/
|
|
@@ -103,11 +105,7 @@ export class CachedKeyCollector implements KeyCollector {
|
|
|
103
105
|
} catch (e) {
|
|
104
106
|
this.error = true;
|
|
105
107
|
// No result - keep previous keys
|
|
106
|
-
|
|
107
|
-
this.currentErrors = [e];
|
|
108
|
-
} else {
|
|
109
|
-
this.currentErrors = [new jose.errors.JOSEError(e.message ?? 'Failed to fetch keys')];
|
|
110
|
-
}
|
|
108
|
+
this.currentErrors = [mapAuthConfigError(e)];
|
|
111
109
|
}
|
|
112
110
|
}
|
|
113
111
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as jose from 'jose';
|
|
2
2
|
import { KeySpec } from './KeySpec.js';
|
|
3
3
|
import { KeyCollector, KeyResult } from './KeyCollector.js';
|
|
4
|
+
import { AuthorizationError } from '@powersync/lib-services-framework';
|
|
4
5
|
|
|
5
6
|
export class CompoundKeyCollector implements KeyCollector {
|
|
6
7
|
private collectors: KeyCollector[];
|
|
@@ -15,7 +16,7 @@ export class CompoundKeyCollector implements KeyCollector {
|
|
|
15
16
|
|
|
16
17
|
async getKeys(): Promise<KeyResult> {
|
|
17
18
|
let keys: KeySpec[] = [];
|
|
18
|
-
let errors:
|
|
19
|
+
let errors: AuthorizationError[] = [];
|
|
19
20
|
const promises = this.collectors.map((collector) =>
|
|
20
21
|
collector.getKeys().then((result) => {
|
|
21
22
|
keys.push(...result.keys);
|
package/src/auth/KeyCollector.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AuthorizationError } from '@powersync/lib-services-framework';
|
|
2
2
|
import { KeySpec } from './KeySpec.js';
|
|
3
3
|
|
|
4
4
|
export interface KeyCollector {
|
|
@@ -22,6 +22,6 @@ export interface KeyCollector {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface KeyResult {
|
|
25
|
-
errors:
|
|
25
|
+
errors: AuthorizationError[];
|
|
26
26
|
keys: KeySpec[];
|
|
27
27
|
}
|
package/src/auth/KeyStore.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
1
|
+
import { logger, errors, AuthorizationError, ErrorCode } from '@powersync/lib-services-framework';
|
|
2
2
|
import * as jose from 'jose';
|
|
3
3
|
import secs from '../util/secs.js';
|
|
4
4
|
import { JwtPayload } from './JwtPayload.js';
|
|
5
5
|
import { KeyCollector } from './KeyCollector.js';
|
|
6
6
|
import { KeyOptions, KeySpec, SUPPORTED_ALGORITHMS } from './KeySpec.js';
|
|
7
|
+
import { mapAuthError } from './utils.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* KeyStore to get keys and verify tokens.
|
|
@@ -49,7 +50,8 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
49
50
|
clockTolerance: 60,
|
|
50
51
|
// More specific algorithm checking is done when selecting the key to use.
|
|
51
52
|
algorithms: SUPPORTED_ALGORITHMS,
|
|
52
|
-
|
|
53
|
+
// 'aud' presence is checked below, so we can add more details to the error message.
|
|
54
|
+
requiredClaims: ['sub', 'iat', 'exp']
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
let audiences = options.defaultAudiences;
|
|
@@ -60,8 +62,12 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
60
62
|
|
|
61
63
|
const tokenPayload = result.payload;
|
|
62
64
|
|
|
63
|
-
let aud = tokenPayload.aud
|
|
64
|
-
if (
|
|
65
|
+
let aud = tokenPayload.aud;
|
|
66
|
+
if (aud == null) {
|
|
67
|
+
throw new AuthorizationError(ErrorCode.PSYNC_S2105, `JWT payload is missing a required claim "aud"`, {
|
|
68
|
+
configurationDetails: `Current configuration allows these audience values: ${JSON.stringify(audiences)}`
|
|
69
|
+
});
|
|
70
|
+
} else if (!Array.isArray(aud)) {
|
|
65
71
|
aud = [aud];
|
|
66
72
|
}
|
|
67
73
|
if (
|
|
@@ -69,7 +75,11 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
69
75
|
return audiences.includes(a);
|
|
70
76
|
})
|
|
71
77
|
) {
|
|
72
|
-
throw new
|
|
78
|
+
throw new AuthorizationError(
|
|
79
|
+
ErrorCode.PSYNC_S2105,
|
|
80
|
+
`Unexpected "aud" claim value: ${JSON.stringify(tokenPayload.aud)}`,
|
|
81
|
+
{ configurationDetails: `Current configuration allows these audience values: ${JSON.stringify(audiences)}` }
|
|
82
|
+
);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
const tokenDuration = tokenPayload.exp! - tokenPayload.iat!;
|
|
@@ -78,12 +88,15 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
78
88
|
// is too far into the future.
|
|
79
89
|
const maxAge = keyOptions.maxLifetimeSeconds ?? secs(options.maxAge);
|
|
80
90
|
if (tokenDuration > maxAge) {
|
|
81
|
-
throw new
|
|
91
|
+
throw new AuthorizationError(
|
|
92
|
+
ErrorCode.PSYNC_S2104,
|
|
93
|
+
`Token must expire in a maximum of ${maxAge} seconds, got ${tokenDuration}s`
|
|
94
|
+
);
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
const parameters = tokenPayload.parameters;
|
|
85
98
|
if (parameters != null && (Array.isArray(parameters) || typeof parameters != 'object')) {
|
|
86
|
-
throw new
|
|
99
|
+
throw new AuthorizationError(ErrorCode.PSYNC_S2101, `Payload parameters must be an object`);
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
return tokenPayload as JwtPayload;
|
|
@@ -91,16 +104,20 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
91
104
|
|
|
92
105
|
private async verifyInternal(token: string, options: jose.JWTVerifyOptions) {
|
|
93
106
|
let keyOptions: KeyOptions | undefined = undefined;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
try {
|
|
108
|
+
const result = await jose.jwtVerify(
|
|
109
|
+
token,
|
|
110
|
+
async (header) => {
|
|
111
|
+
let key = await this.getCachedKey(token, header);
|
|
112
|
+
keyOptions = key.options;
|
|
113
|
+
return key.key;
|
|
114
|
+
},
|
|
115
|
+
options
|
|
116
|
+
);
|
|
117
|
+
return { result, keyOptions: keyOptions! };
|
|
118
|
+
} catch (e) {
|
|
119
|
+
throw mapAuthError(e, token);
|
|
120
|
+
}
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
private async getCachedKey(token: string, header: jose.JWTHeaderParameters): Promise<KeySpec> {
|
|
@@ -112,7 +129,10 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
112
129
|
for (let key of keys) {
|
|
113
130
|
if (key.kid == kid) {
|
|
114
131
|
if (!key.matchesAlgorithm(header.alg)) {
|
|
115
|
-
throw new
|
|
132
|
+
throw new AuthorizationError(ErrorCode.PSYNC_S2101, `Unexpected token algorithm ${header.alg}`, {
|
|
133
|
+
configurationDetails: `Key kid: ${key.source.kid}, alg: ${key.source.alg}, kty: ${key.source.kty}`
|
|
134
|
+
// Token details automatically populated elsewhere
|
|
135
|
+
});
|
|
116
136
|
}
|
|
117
137
|
return key;
|
|
118
138
|
}
|
|
@@ -145,8 +165,13 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
|
|
|
145
165
|
logger.error(`Failed to refresh keys`, e);
|
|
146
166
|
});
|
|
147
167
|
|
|
148
|
-
throw new
|
|
149
|
-
|
|
168
|
+
throw new AuthorizationError(
|
|
169
|
+
ErrorCode.PSYNC_S2101,
|
|
170
|
+
'Could not find an appropriate key in the keystore. The key is missing or no key matched the token KID',
|
|
171
|
+
{
|
|
172
|
+
configurationDetails: `Known kid values: ${keys.map((key) => key.kid ?? '*').join(', ')}`
|
|
173
|
+
// tokenDetails automatically populated later
|
|
174
|
+
}
|
|
150
175
|
);
|
|
151
176
|
}
|
|
152
177
|
}
|
|
@@ -4,6 +4,7 @@ import * as jose from 'jose';
|
|
|
4
4
|
import fetch from 'node-fetch';
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
AuthorizationError,
|
|
7
8
|
ErrorCode,
|
|
8
9
|
LookupOptions,
|
|
9
10
|
makeHostnameLookupFunction,
|
|
@@ -46,28 +47,43 @@ export class RemoteJWKSCollector implements KeyCollector {
|
|
|
46
47
|
this.agent = this.resolveAgent();
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
async
|
|
50
|
+
private async getJwksData(): Promise<any> {
|
|
50
51
|
const abortController = new AbortController();
|
|
51
52
|
const timeout = setTimeout(() => {
|
|
52
53
|
abortController.abort();
|
|
53
54
|
}, 30_000);
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(this.url, {
|
|
58
|
+
method: 'GET',
|
|
59
|
+
headers: {
|
|
60
|
+
Accept: 'application/json'
|
|
61
|
+
},
|
|
62
|
+
signal: abortController.signal,
|
|
63
|
+
agent: this.agent
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new AuthorizationError(ErrorCode.PSYNC_S2204, `JWKS request failed with ${res.statusText}`, {
|
|
68
|
+
configurationDetails: `JWKS URL: ${this.url}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
return (await res.json()) as any;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
throw new AuthorizationError(ErrorCode.PSYNC_S2204, `JWKS request failed`, {
|
|
75
|
+
configurationDetails: `JWKS URL: ${this.url}`,
|
|
76
|
+
// This covers most cases of FetchError
|
|
77
|
+
// `cause: e` could lose the error message
|
|
78
|
+
cause: { message: e.message, code: e.code }
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
async getKeys(): Promise<KeyResult> {
|
|
86
|
+
const data = await this.getJwksData();
|
|
71
87
|
|
|
72
88
|
// https://github.com/panva/jose/blob/358e864a0cccf1e0f9928a959f91f18f3f06a7de/src/jwks/local.ts#L36
|
|
73
89
|
if (
|
|
@@ -75,7 +91,14 @@ export class RemoteJWKSCollector implements KeyCollector {
|
|
|
75
91
|
!Array.isArray(data.keys) ||
|
|
76
92
|
!(data.keys as any[]).every((key) => typeof key == 'object' && !Array.isArray(key))
|
|
77
93
|
) {
|
|
78
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
keys: [],
|
|
96
|
+
errors: [
|
|
97
|
+
new AuthorizationError(ErrorCode.PSYNC_S2204, `Invalid JWKS response`, {
|
|
98
|
+
configurationDetails: `JWKS URL: ${this.url}. Response:\n${JSON.stringify(data, null, 2)}`
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
};
|
|
79
102
|
}
|
|
80
103
|
|
|
81
104
|
let keys: KeySpec[] = [];
|
package/src/auth/auth-index.ts
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { AuthorizationError, ErrorCode } from '@powersync/lib-services-framework';
|
|
2
|
+
import * as jose from 'jose';
|
|
3
|
+
|
|
4
|
+
export function mapJoseError(error: jose.errors.JOSEError, token: string): AuthorizationError {
|
|
5
|
+
const tokenDetails = tokenDebugDetails(token);
|
|
6
|
+
if (error.code === jose.errors.JWSInvalid.code || error.code === jose.errors.JWTInvalid.code) {
|
|
7
|
+
return new AuthorizationError(ErrorCode.PSYNC_S2101, 'Token is not a well-formed JWT. Check the token format.', {
|
|
8
|
+
tokenDetails,
|
|
9
|
+
cause: error
|
|
10
|
+
});
|
|
11
|
+
} else if (error.code === jose.errors.JWTClaimValidationFailed.code) {
|
|
12
|
+
// Jose message: missing required "sub" claim
|
|
13
|
+
const claim = (error as jose.errors.JWTClaimValidationFailed).claim;
|
|
14
|
+
return new AuthorizationError(
|
|
15
|
+
ErrorCode.PSYNC_S2101,
|
|
16
|
+
`JWT payload is missing a required claim ${JSON.stringify(claim)}`,
|
|
17
|
+
{
|
|
18
|
+
cause: error,
|
|
19
|
+
tokenDetails
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
} else if (error.code == jose.errors.JWTExpired.code) {
|
|
23
|
+
// Jose message: "exp" claim timestamp check failed
|
|
24
|
+
return new AuthorizationError(ErrorCode.PSYNC_S2103, `JWT has expired`, {
|
|
25
|
+
cause: error,
|
|
26
|
+
tokenDetails
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return new AuthorizationError(ErrorCode.PSYNC_S2101, error.message, { cause: error });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mapAuthError(error: any, token: string): AuthorizationError {
|
|
33
|
+
if (error instanceof AuthorizationError) {
|
|
34
|
+
error.tokenDetails ??= tokenDebugDetails(token);
|
|
35
|
+
return error;
|
|
36
|
+
} else if (error instanceof jose.errors.JOSEError) {
|
|
37
|
+
return mapJoseError(error, token);
|
|
38
|
+
}
|
|
39
|
+
return new AuthorizationError(ErrorCode.PSYNC_S2101, error.message, {
|
|
40
|
+
cause: error,
|
|
41
|
+
tokenDetails: tokenDebugDetails(token)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function mapJoseConfigError(error: jose.errors.JOSEError): AuthorizationError {
|
|
46
|
+
return new AuthorizationError(ErrorCode.PSYNC_S2201, error.message ?? 'Authorization error', { cause: error });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function mapAuthConfigError(error: any): AuthorizationError {
|
|
50
|
+
if (error instanceof AuthorizationError) {
|
|
51
|
+
return error;
|
|
52
|
+
} else if (error instanceof jose.errors.JOSEError) {
|
|
53
|
+
return mapJoseConfigError(error);
|
|
54
|
+
}
|
|
55
|
+
return new AuthorizationError(ErrorCode.PSYNC_S2201, error.message ?? 'Auth configuration error', { cause: error });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decode token for debugging purposes.
|
|
60
|
+
*
|
|
61
|
+
* We use this to add details to our logs. We don't log the entire token, since it may for example
|
|
62
|
+
* a password incorrectly used as a token.
|
|
63
|
+
*/
|
|
64
|
+
function tokenDebugDetails(token: string): string {
|
|
65
|
+
try {
|
|
66
|
+
// For valid tokens, we return the header and payload
|
|
67
|
+
const header = jose.decodeProtectedHeader(token);
|
|
68
|
+
const payload = jose.decodeJwt(token);
|
|
69
|
+
return `<header: ${JSON.stringify(header)} payload: ${JSON.stringify(payload)}>`;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// Token fails to parse. Return some details.
|
|
72
|
+
return invalidTokenDetails(token);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function invalidTokenDetails(token: string): string {
|
|
77
|
+
const parts = token.split('.');
|
|
78
|
+
if (parts.length !== 3) {
|
|
79
|
+
return `<token with ${parts.length} parts (needs 3), length=${token.length}>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
JSON.parse(Buffer.from(headerB64, 'base64url').toString('utf8'));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return `<token with unparsable header>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return `<token with unparsable payload>`;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
Buffer.from(signatureB64, 'base64url');
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return `<token with unparsable signature>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `<invalid JWT, length=${token.length}>`;
|
|
102
|
+
}
|
|
@@ -37,7 +37,10 @@ export function registerCompactAction(program: Command) {
|
|
|
37
37
|
logger.info(`Compacting storage for ${buckets?.join(', ')}...`);
|
|
38
38
|
}
|
|
39
39
|
const config = await utils.loadConfig(extractRunnerOptions(options));
|
|
40
|
-
const serviceContext = new system.ServiceContextContainer(
|
|
40
|
+
const serviceContext = new system.ServiceContextContainer({
|
|
41
|
+
serviceMode: system.ServiceContextMode.COMPACT,
|
|
42
|
+
configuration: config
|
|
43
|
+
});
|
|
41
44
|
|
|
42
45
|
// Register modules in order to allow custom module compacting
|
|
43
46
|
const moduleManager = container.getImplementation(modules.ModuleManager);
|
|
@@ -18,7 +18,10 @@ export function registerMigrationAction(program: Command) {
|
|
|
18
18
|
.argument('<direction>', 'Migration direction. `up` or `down`')
|
|
19
19
|
.action(async (direction: migrations.Direction, options) => {
|
|
20
20
|
const config = await utils.loadConfig(extractRunnerOptions(options));
|
|
21
|
-
const serviceContext = new system.ServiceContextContainer(
|
|
21
|
+
const serviceContext = new system.ServiceContextContainer({
|
|
22
|
+
serviceMode: system.ServiceContextMode.MIGRATION,
|
|
23
|
+
configuration: config
|
|
24
|
+
});
|
|
22
25
|
|
|
23
26
|
// Register modules in order to allow custom module migrations
|
|
24
27
|
const moduleManager = container.getImplementation(modules.ModuleManager);
|
|
@@ -17,7 +17,10 @@ export function registerTestConnectionAction(program: Command) {
|
|
|
17
17
|
return testConnectionCommand.description('Test connection').action(async (options) => {
|
|
18
18
|
try {
|
|
19
19
|
const config = await utils.loadConfig(extractRunnerOptions(options));
|
|
20
|
-
const serviceContext = new system.ServiceContextContainer(
|
|
20
|
+
const serviceContext = new system.ServiceContextContainer({
|
|
21
|
+
serviceMode: system.ServiceContextMode.TEST_CONNECTION,
|
|
22
|
+
configuration: config
|
|
23
|
+
});
|
|
21
24
|
|
|
22
25
|
const replication = new ReplicationEngine();
|
|
23
26
|
serviceContext.register(ReplicationEngine, replication);
|
|
@@ -2,15 +2,8 @@ import { logger } from '@powersync/lib-services-framework';
|
|
|
2
2
|
|
|
3
3
|
import * as api from '../api/api-index.js';
|
|
4
4
|
|
|
5
|
-
import { ADMIN_ROUTES } from './endpoints/admin.js';
|
|
6
|
-
import { CHECKPOINT_ROUTES } from './endpoints/checkpointing.js';
|
|
7
|
-
import { PROBES_ROUTES } from './endpoints/probes.js';
|
|
8
|
-
import { syncStreamReactive } from './endpoints/socket-route.js';
|
|
9
|
-
import { SYNC_RULES_ROUTES } from './endpoints/sync-rules.js';
|
|
10
|
-
import { SYNC_STREAM_ROUTES } from './endpoints/sync-stream.js';
|
|
11
5
|
import { SocketRouteGenerator } from './router-socket.js';
|
|
12
6
|
import { RouteDefinition } from './router.js';
|
|
13
|
-
import { SyncContext } from '../sync/SyncContext.js';
|
|
14
7
|
|
|
15
8
|
export type RouterSetupResponse = {
|
|
16
9
|
onShutdown: () => Promise<void>;
|
|
@@ -47,14 +40,25 @@ export class RouterEngine {
|
|
|
47
40
|
this.cleanupHandler = null;
|
|
48
41
|
this.closed = false;
|
|
49
42
|
|
|
50
|
-
// Default routes
|
|
51
43
|
this.routes = {
|
|
52
|
-
api_routes: [
|
|
53
|
-
stream_routes: [
|
|
54
|
-
socket_routes: [
|
|
44
|
+
api_routes: [],
|
|
45
|
+
stream_routes: [],
|
|
46
|
+
socket_routes: []
|
|
55
47
|
};
|
|
56
48
|
}
|
|
57
49
|
|
|
50
|
+
public registerRoutes(routes: Partial<RouterEngineRoutes>) {
|
|
51
|
+
this.routes.api_routes.push(...(routes.api_routes ?? []));
|
|
52
|
+
this.routes.stream_routes.push(...(routes.stream_routes ?? []));
|
|
53
|
+
this.routes.socket_routes.push(...(routes.socket_routes ?? []));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public get hasRoutes() {
|
|
57
|
+
return (
|
|
58
|
+
this.routes.api_routes.length > 0 || this.routes.stream_routes.length > 0 || this.routes.socket_routes.length > 0
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
public registerAPI(api: api.RouteAPI) {
|
|
59
63
|
if (this.api) {
|
|
60
64
|
logger.warn('A RouteAPI has already been registered. Overriding existing implementation');
|
|
@@ -75,6 +79,12 @@ export class RouterEngine {
|
|
|
75
79
|
*/
|
|
76
80
|
async start(setup: RouterSetup) {
|
|
77
81
|
logger.info('Starting Router Engine...');
|
|
82
|
+
|
|
83
|
+
if (!this.hasRoutes) {
|
|
84
|
+
logger.info('Router Engine will not start an HTTP server as no routes have been registered.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
const { onShutdown } = await setup(this.routes);
|
|
79
89
|
this.cleanupHandler = onShutdown;
|
|
80
90
|
logger.info('Successfully started Router Engine.');
|
package/src/routes/auth.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as auth from '../auth/auth-index.js';
|
|
|
4
4
|
import { ServiceContext } from '../system/ServiceContext.js';
|
|
5
5
|
import * as util from '../util/util-index.js';
|
|
6
6
|
import { BasicRouterRequest, Context, RequestEndpointHandlerPayload } from './router.js';
|
|
7
|
+
import { AuthorizationError, AuthorizationResponse, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
7
8
|
|
|
8
9
|
export function endpoint(req: BasicRouterRequest) {
|
|
9
10
|
const protocol = req.headers['x-forwarded-proto'] ?? req.protocol;
|
|
@@ -95,25 +96,25 @@ export function getTokenFromHeader(authHeader: string = ''): string | null {
|
|
|
95
96
|
return token ?? null;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
export const authUser = async (payload: RequestEndpointHandlerPayload) => {
|
|
99
|
+
export const authUser = async (payload: RequestEndpointHandlerPayload): Promise<AuthorizationResponse> => {
|
|
99
100
|
return authorizeUser(payload.context, payload.request.headers.authorization as string);
|
|
100
101
|
};
|
|
101
102
|
|
|
102
|
-
export async function authorizeUser(context: Context, authHeader: string = '') {
|
|
103
|
+
export async function authorizeUser(context: Context, authHeader: string = ''): Promise<AuthorizationResponse> {
|
|
103
104
|
const token = getTokenFromHeader(authHeader);
|
|
104
105
|
if (token == null) {
|
|
105
106
|
return {
|
|
106
107
|
authorized: false,
|
|
107
|
-
|
|
108
|
+
error: new AuthorizationError(ErrorCode.PSYNC_S2106, 'Authentication required')
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
const { context: tokenContext,
|
|
112
|
+
const { context: tokenContext, tokenError } = await generateContext(context.service_context, token);
|
|
112
113
|
|
|
113
114
|
if (!tokenContext) {
|
|
114
115
|
return {
|
|
115
116
|
authorized: false,
|
|
116
|
-
|
|
117
|
+
error: tokenError
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -140,7 +141,7 @@ export async function generateContext(serviceContext: ServiceContext, token: str
|
|
|
140
141
|
} catch (err) {
|
|
141
142
|
return {
|
|
142
143
|
context: null,
|
|
143
|
-
|
|
144
|
+
tokenError: auth.mapAuthError(err, token)
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type fastify from 'fastify';
|
|
2
|
+
import * as uuid from 'uuid';
|
|
3
|
+
|
|
2
4
|
import { registerFastifyRoutes } from './route-register.js';
|
|
3
5
|
|
|
4
6
|
import * as system from '../system/system-index.js';
|
|
@@ -9,7 +11,7 @@ import { PROBES_ROUTES } from './endpoints/probes.js';
|
|
|
9
11
|
import { SYNC_RULES_ROUTES } from './endpoints/sync-rules.js';
|
|
10
12
|
import { SYNC_STREAM_ROUTES } from './endpoints/sync-stream.js';
|
|
11
13
|
import { createRequestQueueHook, CreateRequestQueueParams } from './hooks.js';
|
|
12
|
-
import {
|
|
14
|
+
import { ContextProvider, RouteDefinition } from './router.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* A list of route definitions to be registered as endpoints.
|
|
@@ -58,15 +60,11 @@ export const DEFAULT_ROUTE_OPTIONS = {
|
|
|
58
60
|
export function configureFastifyServer(server: fastify.FastifyInstance, options: FastifyServerConfig) {
|
|
59
61
|
const { service_context, routes = DEFAULT_ROUTE_OPTIONS } = options;
|
|
60
62
|
|
|
61
|
-
const generateContext = async () => {
|
|
62
|
-
const { routerEngine } = service_context;
|
|
63
|
-
if (!routerEngine) {
|
|
64
|
-
throw new Error(`RouterEngine has not been registered`);
|
|
65
|
-
}
|
|
66
|
-
|
|
63
|
+
const generateContext: ContextProvider = async (request, options) => {
|
|
67
64
|
return {
|
|
68
65
|
user_id: undefined,
|
|
69
|
-
service_context: service_context
|
|
66
|
+
service_context: service_context,
|
|
67
|
+
logger: options.logger
|
|
70
68
|
};
|
|
71
69
|
};
|
|
72
70
|
|