@powersync/service-core 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/auth/KeySpec.d.ts +1 -0
- package/dist/auth/KeySpec.js +10 -8
- package/dist/auth/KeySpec.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.js +2 -2
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +6 -0
- package/dist/storage/BucketStorage.js.map +1 -1
- package/dist/storage/MongoBucketStorage.js +1 -0
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/mongo/MongoBucketBatch.d.ts +6 -0
- package/dist/storage/mongo/MongoBucketBatch.js +47 -11
- package/dist/storage/mongo/MongoBucketBatch.js.map +1 -1
- package/dist/storage/mongo/MongoSyncBucketStorage.js +4 -2
- package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/mongo/models.d.ts +7 -0
- package/dist/util/utils.d.ts +14 -1
- package/dist/util/utils.js +56 -0
- package/dist/util/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/auth/KeySpec.ts +12 -9
- package/src/auth/RemoteJWKSCollector.ts +2 -2
- package/src/storage/BucketStorage.ts +7 -0
- package/src/storage/MongoBucketStorage.ts +1 -0
- package/src/storage/mongo/MongoBucketBatch.ts +64 -10
- package/src/storage/mongo/MongoSyncBucketStorage.ts +4 -2
- package/src/storage/mongo/models.ts +8 -0
- package/src/util/utils.ts +59 -1
- package/test/src/auth.test.ts +54 -21
- package/test/src/bucket_validation.test.ts +2 -1
- package/test/src/bucket_validation.ts +1 -57
- package/test/src/util.ts +3 -1
- package/tsconfig.tsbuildinfo +1 -1
package/dist/util/utils.js
CHANGED
|
@@ -116,4 +116,60 @@ export function checkpointUserId(user_id, client_id) {
|
|
|
116
116
|
}
|
|
117
117
|
return `${user_id}/${client_id}`;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Reduce a bucket to the final state as stored on the client.
|
|
121
|
+
*
|
|
122
|
+
* This keeps the final state for each row as a PUT operation.
|
|
123
|
+
*
|
|
124
|
+
* All other operations are replaced with a single CLEAR operation,
|
|
125
|
+
* summing their checksums, and using a 0 as an op_id.
|
|
126
|
+
*
|
|
127
|
+
* This is the function $r(B)$, as described in /docs/bucket-properties.md.
|
|
128
|
+
*
|
|
129
|
+
* Used for tests.
|
|
130
|
+
*/
|
|
131
|
+
export function reduceBucket(operations) {
|
|
132
|
+
let rowState = new Map();
|
|
133
|
+
let otherChecksum = 0;
|
|
134
|
+
for (let op of operations) {
|
|
135
|
+
const key = rowKey(op);
|
|
136
|
+
if (op.op == 'PUT') {
|
|
137
|
+
const existing = rowState.get(key);
|
|
138
|
+
if (existing) {
|
|
139
|
+
otherChecksum = addChecksums(otherChecksum, existing.checksum);
|
|
140
|
+
}
|
|
141
|
+
rowState.set(key, op);
|
|
142
|
+
}
|
|
143
|
+
else if (op.op == 'REMOVE') {
|
|
144
|
+
const existing = rowState.get(key);
|
|
145
|
+
if (existing) {
|
|
146
|
+
otherChecksum = addChecksums(otherChecksum, existing.checksum);
|
|
147
|
+
}
|
|
148
|
+
rowState.delete(key);
|
|
149
|
+
otherChecksum = addChecksums(otherChecksum, op.checksum);
|
|
150
|
+
}
|
|
151
|
+
else if (op.op == 'CLEAR') {
|
|
152
|
+
rowState.clear();
|
|
153
|
+
otherChecksum = op.checksum;
|
|
154
|
+
}
|
|
155
|
+
else if (op.op == 'MOVE') {
|
|
156
|
+
otherChecksum = addChecksums(otherChecksum, op.checksum);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
throw new Error(`Unknown operation ${op.op}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const puts = [...rowState.values()].sort((a, b) => {
|
|
163
|
+
return Number(BigInt(a.op_id) - BigInt(b.op_id));
|
|
164
|
+
});
|
|
165
|
+
let finalState = [
|
|
166
|
+
// Special operation to indiciate the checksum remainder
|
|
167
|
+
{ op_id: '0', op: 'CLEAR', checksum: otherChecksum },
|
|
168
|
+
...puts
|
|
169
|
+
];
|
|
170
|
+
return finalState;
|
|
171
|
+
}
|
|
172
|
+
function rowKey(entry) {
|
|
173
|
+
return `${entry.object_type}/${entry.object_id}/${entry.subkey}`;
|
|
174
|
+
}
|
|
119
175
|
//# sourceMappingURL=utils.js.map
|
package/dist/util/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/util/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAS7B,MAAM,CAAC,MAAM,YAAY,GAAG,sCAAsC,CAAC;AAEnE,MAAM,UAAU,gBAAgB,CAAC,UAAkB;IACjD,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,EAAU,EAAE,IAAY;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC7B,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,UAAU,SAAS,EAAE,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC7B,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,6EAA6E;IAC7E,6CAA6C;IAC7C,IAAI,OAAO,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,yBAAyB,EAAE,KAAK,OAAO,EAAE,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAqB,EAAE,OAAoB;IACvE,mBAAmB;IACnB,MAAM,cAAc,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEzD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAS,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAElD,KAAK,IAAI,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;YACd,QAAQ;YACR,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAChD,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACjC,IAAI,QAAQ,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACjE,UAAU;gBACV,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAChD,CAAC;iBAAM,CAAC;gBACN,YAAY;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,cAAc,EAAE,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,CAAC;QAC5C,cAAc,EAAE,CAAC,GAAG,QAAQ,CAAC;KAC9B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,CAAS;IAC/C,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,CAAiB,EAAE,CAAyB;IAC7E,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QACd,OAAO,CAAC,CAAC;IACX,CAAC;SAAM,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;QAC5B,OAAO;YACL,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,KAAK,EAAE,CAAC,CAAC,YAAY;YACrB,QAAQ,EAAE,CAAC,CAAC,eAAe;SAC5B,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,OAAO;YACL,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,YAAY;YAC/B,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,eAAe,CAAC;SACtD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB,CAC5B,KAAoC,EACpC,OAAmC;IAEnC,IAAI,MAAM,GAAwB,EAAE,CAAC;IACrC,KAAK,IAAI,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,KAAoC,EACpC,OAAmC;IAEnC,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACxB,gDAAgD;QAChD,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,WAAW,GAAG,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAE1D,OAAO,cAAc,CAAC,WAAW,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAyB;IACtD,+EAA+E;IAC/E,4EAA4E;IAC5E,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAkC;IACjE,KAAK,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;QACpB,IAAI,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,WAAW,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAkC;IAC9D,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAA2B,EAAE,SAA6B;IACzF,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,GAAG,OAAO,IAAI,SAAS,EAAE,CAAC;AACnC,CAAC"}
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/util/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.12.0",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"type": "module",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"@powersync/lib-services-framework": "0.2.0",
|
|
37
37
|
"@powersync/service-jsonbig": "0.17.10",
|
|
38
38
|
"@powersync/service-rsocket-router": "0.0.14",
|
|
39
|
-
"@powersync/service-sync-rules": "0.
|
|
40
|
-
"@powersync/service-types": "0.
|
|
39
|
+
"@powersync/service-sync-rules": "0.22.0",
|
|
40
|
+
"@powersync/service-types": "0.5.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/async": "^3.2.24",
|
package/src/auth/KeySpec.ts
CHANGED
|
@@ -2,7 +2,8 @@ import * as jose from 'jose';
|
|
|
2
2
|
|
|
3
3
|
export const HS_ALGORITHMS = ['HS256', 'HS384', 'HS512'];
|
|
4
4
|
export const RSA_ALGORITHMS = ['RS256', 'RS384', 'RS512'];
|
|
5
|
-
export const
|
|
5
|
+
export const OKP_ALGORITHMS = ['EdDSA'];
|
|
6
|
+
export const SUPPORTED_ALGORITHMS = [...HS_ALGORITHMS, ...RSA_ALGORITHMS, ...OKP_ALGORITHMS];
|
|
6
7
|
|
|
7
8
|
export interface KeyOptions {
|
|
8
9
|
/**
|
|
@@ -38,17 +39,19 @@ export class KeySpec {
|
|
|
38
39
|
return this.source.kid;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
matchesAlgorithm(jwtAlg: string) {
|
|
42
|
+
matchesAlgorithm(jwtAlg: string): boolean {
|
|
42
43
|
if (this.source.alg) {
|
|
43
|
-
return jwtAlg
|
|
44
|
-
} else if (this.source.kty
|
|
44
|
+
return jwtAlg === this.source.alg;
|
|
45
|
+
} else if (this.source.kty === 'RSA') {
|
|
45
46
|
return RSA_ALGORITHMS.includes(jwtAlg);
|
|
46
|
-
} else if (this.source.kty
|
|
47
|
+
} else if (this.source.kty === 'oct') {
|
|
47
48
|
return HS_ALGORITHMS.includes(jwtAlg);
|
|
48
|
-
} else {
|
|
49
|
-
|
|
50
|
-
return false;
|
|
49
|
+
} else if (this.source.kty === 'OKP') {
|
|
50
|
+
return OKP_ALGORITHMS.includes(jwtAlg);
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
// 'EC' is unsupported
|
|
54
|
+
return false;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
async isValidSignature(token: string): Promise<boolean> {
|
|
@@ -56,7 +59,7 @@ export class KeySpec {
|
|
|
56
59
|
await jose.compactVerify(token, this.key);
|
|
57
60
|
return true;
|
|
58
61
|
} catch (e) {
|
|
59
|
-
if (e.code
|
|
62
|
+
if (e.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
|
|
60
63
|
return false;
|
|
61
64
|
} else {
|
|
62
65
|
// Token format error most likely
|
|
@@ -73,8 +73,8 @@ export class RemoteJWKSCollector implements KeyCollector {
|
|
|
73
73
|
|
|
74
74
|
let keys: KeySpec[] = [];
|
|
75
75
|
for (let keyData of data.keys) {
|
|
76
|
-
if (keyData.kty != 'RSA') {
|
|
77
|
-
//
|
|
76
|
+
if (keyData.kty != 'RSA' && keyData.kty != 'OKP') {
|
|
77
|
+
// HS (oct) keys not allowed because they are symmetric
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -211,6 +211,13 @@ export interface StartBatchOptions extends ParseSyncRulesOptions {
|
|
|
211
211
|
* database, for example from MongoDB.
|
|
212
212
|
*/
|
|
213
213
|
storeCurrentData: boolean;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Set to true for initial replication.
|
|
217
|
+
*
|
|
218
|
+
* This will avoid creating new operations for rows previously replicated.
|
|
219
|
+
*/
|
|
220
|
+
skipExistingRows?: boolean;
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
export interface SyncRulesBucketStorageListener extends DisposableListener {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
BucketStorageBatch,
|
|
10
10
|
FlushedResult,
|
|
11
11
|
mergeToast,
|
|
12
|
+
SaveOperationTag,
|
|
12
13
|
SaveOptions
|
|
13
14
|
} from '../BucketStorage.js';
|
|
14
15
|
import { SourceTable } from '../SourceTable.js';
|
|
@@ -20,6 +21,7 @@ import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js'
|
|
|
20
21
|
import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
|
|
21
22
|
import { PersistedBatch } from './PersistedBatch.js';
|
|
22
23
|
import { BSON_DESERIALIZE_OPTIONS, idPrefixFilter, replicaIdEquals, serializeLookup } from './util.js';
|
|
24
|
+
import * as timers from 'node:timers/promises';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* 15MB
|
|
@@ -39,8 +41,13 @@ export interface MongoBucketBatchOptions {
|
|
|
39
41
|
groupId: number;
|
|
40
42
|
slotName: string;
|
|
41
43
|
lastCheckpointLsn: string | null;
|
|
44
|
+
keepaliveOp: string | null;
|
|
42
45
|
noCheckpointBeforeLsn: string;
|
|
43
46
|
storeCurrentData: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Set to true for initial replication.
|
|
49
|
+
*/
|
|
50
|
+
skipExistingRows: boolean;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListener> implements BucketStorageBatch {
|
|
@@ -53,6 +60,7 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
53
60
|
|
|
54
61
|
private readonly slot_name: string;
|
|
55
62
|
private readonly storeCurrentData: boolean;
|
|
63
|
+
private readonly skipExistingRows: boolean;
|
|
56
64
|
|
|
57
65
|
private batch: OperationBatch | null = null;
|
|
58
66
|
private write_checkpoint_batch: CustomWriteCheckpointOptions[] = [];
|
|
@@ -86,7 +94,12 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
86
94
|
this.slot_name = options.slotName;
|
|
87
95
|
this.sync_rules = options.syncRules;
|
|
88
96
|
this.storeCurrentData = options.storeCurrentData;
|
|
97
|
+
this.skipExistingRows = options.skipExistingRows;
|
|
89
98
|
this.batch = new OperationBatch();
|
|
99
|
+
|
|
100
|
+
if (options.keepaliveOp) {
|
|
101
|
+
this.persisted_op = BigInt(options.keepaliveOp);
|
|
102
|
+
}
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
addCustomWriteCheckpoint(checkpoint: BatchedCustomWriteCheckpointOptions): void {
|
|
@@ -148,10 +161,13 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
148
161
|
op_seq: MongoIdSequence
|
|
149
162
|
): Promise<OperationBatch | null> {
|
|
150
163
|
let sizes: Map<string, number> | undefined = undefined;
|
|
151
|
-
if (this.storeCurrentData) {
|
|
164
|
+
if (this.storeCurrentData && !this.skipExistingRows) {
|
|
152
165
|
// We skip this step if we don't store current_data, since the sizes will
|
|
153
166
|
// always be small in that case.
|
|
154
167
|
|
|
168
|
+
// With skipExistingRows, we don't load the full documents into memory,
|
|
169
|
+
// so we can also skip the size lookup step.
|
|
170
|
+
|
|
155
171
|
// Find sizes of current_data documents, to assist in intelligent batching without
|
|
156
172
|
// exceeding memory limits.
|
|
157
173
|
//
|
|
@@ -204,11 +220,13 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
204
220
|
return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
|
|
205
221
|
});
|
|
206
222
|
let current_data_lookup = new Map<string, CurrentDataDocument>();
|
|
223
|
+
// With skipExistingRows, we only need to know whether or not the row exists.
|
|
224
|
+
const projection = this.skipExistingRows ? { _id: 1 } : undefined;
|
|
207
225
|
const cursor = this.db.current_data.find(
|
|
208
226
|
{
|
|
209
227
|
_id: { $in: lookups }
|
|
210
228
|
},
|
|
211
|
-
{ session }
|
|
229
|
+
{ session, projection }
|
|
212
230
|
);
|
|
213
231
|
for await (let doc of cursor.stream()) {
|
|
214
232
|
current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
|
|
@@ -273,7 +291,21 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
273
291
|
|
|
274
292
|
const before_key: SourceKey = { g: this.group_id, t: record.sourceTable.id, k: beforeId };
|
|
275
293
|
|
|
276
|
-
if (
|
|
294
|
+
if (this.skipExistingRows) {
|
|
295
|
+
if (record.tag == SaveOperationTag.INSERT) {
|
|
296
|
+
if (current_data != null) {
|
|
297
|
+
// Initial replication, and we already have the record.
|
|
298
|
+
// This may be a different version of the record, but streaming replication
|
|
299
|
+
// will take care of that.
|
|
300
|
+
// Skip the insert here.
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
throw new Error(`${record.tag} not supported with skipExistingRows: true`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (record.tag == SaveOperationTag.UPDATE) {
|
|
277
309
|
const result = current_data;
|
|
278
310
|
if (result == null) {
|
|
279
311
|
// Not an error if we re-apply a transaction
|
|
@@ -293,7 +325,7 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
293
325
|
after = mergeToast(after!, data);
|
|
294
326
|
}
|
|
295
327
|
}
|
|
296
|
-
} else if (record.tag ==
|
|
328
|
+
} else if (record.tag == SaveOperationTag.DELETE) {
|
|
297
329
|
const result = current_data;
|
|
298
330
|
if (result == null) {
|
|
299
331
|
// Not an error if we re-apply a transaction
|
|
@@ -494,7 +526,7 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
494
526
|
} else {
|
|
495
527
|
logger.warn('Transaction error', e as Error);
|
|
496
528
|
}
|
|
497
|
-
await
|
|
529
|
+
await timers.setTimeout(Math.random() * 50);
|
|
498
530
|
throw e;
|
|
499
531
|
}
|
|
500
532
|
},
|
|
@@ -587,7 +619,28 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
587
619
|
return false;
|
|
588
620
|
}
|
|
589
621
|
if (lsn < this.no_checkpoint_before_lsn) {
|
|
590
|
-
logger.info(
|
|
622
|
+
logger.info(
|
|
623
|
+
`Waiting until ${this.no_checkpoint_before_lsn} before creating checkpoint, currently at ${lsn}. Persisted op: ${this.persisted_op}`
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// Edge case: During initial replication, we have a no_checkpoint_before_lsn set,
|
|
627
|
+
// and don't actually commit the snapshot.
|
|
628
|
+
// The first commit can happen from an implicit keepalive message.
|
|
629
|
+
// That needs the persisted_op to get an accurate checkpoint, so
|
|
630
|
+
// we persist that in keepalive_op.
|
|
631
|
+
|
|
632
|
+
await this.db.sync_rules.updateOne(
|
|
633
|
+
{
|
|
634
|
+
_id: this.group_id
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
$set: {
|
|
638
|
+
keepalive_op: this.persisted_op == null ? null : String(this.persisted_op)
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
{ session: this.session }
|
|
642
|
+
);
|
|
643
|
+
|
|
591
644
|
return false;
|
|
592
645
|
}
|
|
593
646
|
|
|
@@ -597,7 +650,8 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
597
650
|
last_checkpoint_ts: now,
|
|
598
651
|
last_keepalive_ts: now,
|
|
599
652
|
snapshot_done: true,
|
|
600
|
-
last_fatal_error: null
|
|
653
|
+
last_fatal_error: null,
|
|
654
|
+
keepalive_op: null
|
|
601
655
|
};
|
|
602
656
|
|
|
603
657
|
if (this.persisted_op != null) {
|
|
@@ -631,6 +685,7 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
631
685
|
if (this.persisted_op != null) {
|
|
632
686
|
// The commit may have been skipped due to "no_checkpoint_before_lsn".
|
|
633
687
|
// Apply it now if relevant
|
|
688
|
+
logger.info(`Commit due to keepalive at ${lsn} / ${this.persisted_op}`);
|
|
634
689
|
return await this.commit(lsn);
|
|
635
690
|
}
|
|
636
691
|
|
|
@@ -684,9 +739,8 @@ export class MongoBucketBatch extends DisposableObserver<BucketBatchStorageListe
|
|
|
684
739
|
|
|
685
740
|
if (this.batch.shouldFlush()) {
|
|
686
741
|
const r = await this.flush();
|
|
687
|
-
// HACK: Give other streams a
|
|
688
|
-
|
|
689
|
-
await new Promise((resolve) => setTimeout(resolve, t));
|
|
742
|
+
// HACK: Give other streams a chance to also flush
|
|
743
|
+
await timers.setTimeout(5);
|
|
690
744
|
return r;
|
|
691
745
|
}
|
|
692
746
|
return null;
|
|
@@ -137,7 +137,7 @@ export class MongoSyncBucketStorage
|
|
|
137
137
|
{
|
|
138
138
|
_id: this.group_id
|
|
139
139
|
},
|
|
140
|
-
{ projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1 } }
|
|
140
|
+
{ projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1 } }
|
|
141
141
|
);
|
|
142
142
|
const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
|
|
143
143
|
|
|
@@ -148,7 +148,9 @@ export class MongoSyncBucketStorage
|
|
|
148
148
|
slotName: this.slot_name,
|
|
149
149
|
lastCheckpointLsn: checkpoint_lsn,
|
|
150
150
|
noCheckpointBeforeLsn: doc?.no_checkpoint_before ?? options.zeroLSN,
|
|
151
|
-
|
|
151
|
+
keepaliveOp: doc?.keepalive_op ?? null,
|
|
152
|
+
storeCurrentData: options.storeCurrentData,
|
|
153
|
+
skipExistingRows: options.skipExistingRows ?? false
|
|
152
154
|
});
|
|
153
155
|
this.iterateListeners((cb) => cb.batchStarted?.(batch));
|
|
154
156
|
|
|
@@ -135,6 +135,14 @@ export interface SyncRuleDocument {
|
|
|
135
135
|
*/
|
|
136
136
|
no_checkpoint_before: string | null;
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Goes together with no_checkpoint_before.
|
|
140
|
+
*
|
|
141
|
+
* If a keepalive is triggered that creates the checkpoint > no_checkpoint_before,
|
|
142
|
+
* then the checkpoint must be equal to this keepalive_op.
|
|
143
|
+
*/
|
|
144
|
+
keepalive_op: string | null;
|
|
145
|
+
|
|
138
146
|
slot_name: string | null;
|
|
139
147
|
|
|
140
148
|
/**
|
package/src/util/utils.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as sync_rules from '@powersync/service-sync-rules';
|
|
|
2
2
|
import * as bson from 'bson';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import * as uuid from 'uuid';
|
|
5
|
-
import { BucketChecksum, OpId } from './protocol-types.js';
|
|
5
|
+
import { BucketChecksum, OpId, OplogEntry } from './protocol-types.js';
|
|
6
6
|
|
|
7
7
|
import * as storage from '../storage/storage-index.js';
|
|
8
8
|
|
|
@@ -144,3 +144,61 @@ export function checkpointUserId(user_id: string | undefined, client_id: string
|
|
|
144
144
|
}
|
|
145
145
|
return `${user_id}/${client_id}`;
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reduce a bucket to the final state as stored on the client.
|
|
150
|
+
*
|
|
151
|
+
* This keeps the final state for each row as a PUT operation.
|
|
152
|
+
*
|
|
153
|
+
* All other operations are replaced with a single CLEAR operation,
|
|
154
|
+
* summing their checksums, and using a 0 as an op_id.
|
|
155
|
+
*
|
|
156
|
+
* This is the function $r(B)$, as described in /docs/bucket-properties.md.
|
|
157
|
+
*
|
|
158
|
+
* Used for tests.
|
|
159
|
+
*/
|
|
160
|
+
export function reduceBucket(operations: OplogEntry[]) {
|
|
161
|
+
let rowState = new Map<string, OplogEntry>();
|
|
162
|
+
let otherChecksum = 0;
|
|
163
|
+
|
|
164
|
+
for (let op of operations) {
|
|
165
|
+
const key = rowKey(op);
|
|
166
|
+
if (op.op == 'PUT') {
|
|
167
|
+
const existing = rowState.get(key);
|
|
168
|
+
if (existing) {
|
|
169
|
+
otherChecksum = addChecksums(otherChecksum, existing.checksum as number);
|
|
170
|
+
}
|
|
171
|
+
rowState.set(key, op);
|
|
172
|
+
} else if (op.op == 'REMOVE') {
|
|
173
|
+
const existing = rowState.get(key);
|
|
174
|
+
if (existing) {
|
|
175
|
+
otherChecksum = addChecksums(otherChecksum, existing.checksum as number);
|
|
176
|
+
}
|
|
177
|
+
rowState.delete(key);
|
|
178
|
+
otherChecksum = addChecksums(otherChecksum, op.checksum as number);
|
|
179
|
+
} else if (op.op == 'CLEAR') {
|
|
180
|
+
rowState.clear();
|
|
181
|
+
otherChecksum = op.checksum as number;
|
|
182
|
+
} else if (op.op == 'MOVE') {
|
|
183
|
+
otherChecksum = addChecksums(otherChecksum, op.checksum as number);
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error(`Unknown operation ${op.op}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const puts = [...rowState.values()].sort((a, b) => {
|
|
190
|
+
return Number(BigInt(a.op_id) - BigInt(b.op_id));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let finalState: OplogEntry[] = [
|
|
194
|
+
// Special operation to indiciate the checksum remainder
|
|
195
|
+
{ op_id: '0', op: 'CLEAR', checksum: otherChecksum },
|
|
196
|
+
...puts
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
return finalState;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function rowKey(entry: OplogEntry) {
|
|
203
|
+
return `${entry.object_type}/${entry.object_id}/${entry.subkey}`;
|
|
204
|
+
}
|
package/test/src/auth.test.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { CachedKeyCollector } from '@/auth/CachedKeyCollector.js';
|
|
2
|
-
import { KeyResult } from '@/auth/KeyCollector.js';
|
|
3
|
-
import { KeySpec } from '@/auth/KeySpec.js';
|
|
4
|
-
import { KeyStore } from '@/auth/KeyStore.js';
|
|
5
|
-
import { RemoteJWKSCollector } from '@/auth/RemoteJWKSCollector.js';
|
|
6
|
-
import { StaticKeyCollector } from '@/auth/StaticKeyCollector.js';
|
|
7
|
-
import * as jose from 'jose';
|
|
8
1
|
import { describe, expect, test } from 'vitest';
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import { StaticKeyCollector } from '../../src/auth/StaticKeyCollector.js';
|
|
3
|
+
import * as jose from 'jose';
|
|
4
|
+
import { KeyStore } from '../../src/auth/KeyStore.js';
|
|
5
|
+
import { KeySpec } from '../../src/auth/KeySpec.js';
|
|
6
|
+
import { RemoteJWKSCollector } from '../../src/auth/RemoteJWKSCollector.js';
|
|
7
|
+
import { KeyResult } from '../../src/auth/KeyCollector.js';
|
|
8
|
+
import { CachedKeyCollector } from '../../src/auth/CachedKeyCollector.js';
|
|
9
|
+
import { JwtPayload } from '@/index.js';
|
|
10
|
+
|
|
11
|
+
const publicKeyRSA: jose.JWK = {
|
|
11
12
|
use: 'sig',
|
|
12
13
|
kty: 'RSA',
|
|
13
14
|
e: 'AQAB',
|
|
@@ -29,6 +30,16 @@ const sharedKey2: jose.JWK = {
|
|
|
29
30
|
k: Buffer.from('mysecret2', 'utf-8').toString('base64url')
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
const privateKeyEdDSA: jose.JWK = {
|
|
34
|
+
use: 'sig',
|
|
35
|
+
kty: 'OKP',
|
|
36
|
+
crv: 'Ed25519',
|
|
37
|
+
kid: 'k2',
|
|
38
|
+
x: 'nfaqgxakPaiiEdAtRGrubgh_SQ1mr6gAUx3--N-ehvo',
|
|
39
|
+
d: 'wweBqMbTrME6oChSEMYAOyYzxsGisQb-C1t0XMjb_Ng',
|
|
40
|
+
alg: 'EdDSA'
|
|
41
|
+
};
|
|
42
|
+
|
|
32
43
|
describe('JWT Auth', () => {
|
|
33
44
|
test('KeyStore basics', async () => {
|
|
34
45
|
const keys = await StaticKeyCollector.importKeys([sharedKey]);
|
|
@@ -86,20 +97,20 @@ describe('JWT Auth', () => {
|
|
|
86
97
|
});
|
|
87
98
|
|
|
88
99
|
test('Algorithm validation', async () => {
|
|
89
|
-
const keys = await StaticKeyCollector.importKeys([
|
|
100
|
+
const keys = await StaticKeyCollector.importKeys([publicKeyRSA]);
|
|
90
101
|
const store = new KeyStore(keys);
|
|
91
102
|
|
|
92
103
|
// Bad attempt at signing token with rsa public key
|
|
93
104
|
const spoofedKey: jose.JWK = {
|
|
94
105
|
kty: 'oct',
|
|
95
|
-
kid:
|
|
106
|
+
kid: publicKeyRSA.kid!,
|
|
96
107
|
alg: 'HS256',
|
|
97
|
-
k:
|
|
108
|
+
k: publicKeyRSA.n!
|
|
98
109
|
};
|
|
99
110
|
const signKey = (await jose.importJWK(spoofedKey)) as jose.KeyLike;
|
|
100
111
|
|
|
101
112
|
const signedJwt = await new jose.SignJWT({})
|
|
102
|
-
.setProtectedHeader({ alg: 'HS256', kid:
|
|
113
|
+
.setProtectedHeader({ alg: 'HS256', kid: publicKeyRSA.kid! })
|
|
103
114
|
.setSubject('f1')
|
|
104
115
|
.setIssuedAt()
|
|
105
116
|
.setIssuer('tester')
|
|
@@ -116,7 +127,7 @@ describe('JWT Auth', () => {
|
|
|
116
127
|
});
|
|
117
128
|
|
|
118
129
|
test('key selection for key with kid', async () => {
|
|
119
|
-
const keys = await StaticKeyCollector.importKeys([
|
|
130
|
+
const keys = await StaticKeyCollector.importKeys([publicKeyRSA, sharedKey, sharedKey2]);
|
|
120
131
|
const store = new KeyStore(keys);
|
|
121
132
|
const signKey = (await jose.importJWK(sharedKey)) as jose.KeyLike;
|
|
122
133
|
const signKey2 = (await jose.importJWK(sharedKey2)) as jose.KeyLike;
|
|
@@ -296,30 +307,30 @@ describe('JWT Auth', () => {
|
|
|
296
307
|
|
|
297
308
|
currentResponse = Promise.resolve({
|
|
298
309
|
errors: [],
|
|
299
|
-
keys: [await KeySpec.importKey(
|
|
310
|
+
keys: [await KeySpec.importKey(publicKeyRSA)]
|
|
300
311
|
});
|
|
301
312
|
|
|
302
313
|
let key = (await cached.getKeys()).keys[0];
|
|
303
|
-
expect(key.kid).toEqual(
|
|
314
|
+
expect(key.kid).toEqual(publicKeyRSA.kid!);
|
|
304
315
|
|
|
305
316
|
currentResponse = undefined as any;
|
|
306
317
|
|
|
307
318
|
key = (await cached.getKeys()).keys[0];
|
|
308
|
-
expect(key.kid).toEqual(
|
|
319
|
+
expect(key.kid).toEqual(publicKeyRSA.kid!);
|
|
309
320
|
|
|
310
321
|
cached.addTimeForTests(301_000);
|
|
311
322
|
currentResponse = Promise.reject('refresh failed');
|
|
312
323
|
|
|
313
324
|
// Uses the promise, refreshes in the background
|
|
314
325
|
let response = await cached.getKeys();
|
|
315
|
-
expect(response.keys[0].kid).toEqual(
|
|
326
|
+
expect(response.keys[0].kid).toEqual(publicKeyRSA.kid!);
|
|
316
327
|
expect(response.errors).toEqual([]);
|
|
317
328
|
|
|
318
329
|
// Wait for refresh to finish
|
|
319
330
|
await cached.addTimeForTests(0);
|
|
320
331
|
response = await cached.getKeys();
|
|
321
332
|
// Still have the cached key, but also have the error
|
|
322
|
-
expect(response.keys[0].kid).toEqual(
|
|
333
|
+
expect(response.keys[0].kid).toEqual(publicKeyRSA.kid!);
|
|
323
334
|
expect(response.errors[0].message).toMatch('Failed to fetch');
|
|
324
335
|
|
|
325
336
|
await cached.addTimeForTests(3601_000);
|
|
@@ -331,12 +342,34 @@ describe('JWT Auth', () => {
|
|
|
331
342
|
|
|
332
343
|
currentResponse = Promise.resolve({
|
|
333
344
|
errors: [],
|
|
334
|
-
keys: [await KeySpec.importKey(
|
|
345
|
+
keys: [await KeySpec.importKey(publicKeyRSA)]
|
|
335
346
|
});
|
|
336
347
|
|
|
337
348
|
// After a delay, we can refresh again
|
|
338
349
|
await cached.addTimeForTests(30_000);
|
|
339
350
|
key = (await cached.getKeys()).keys[0];
|
|
340
|
-
expect(key.kid).toEqual(
|
|
351
|
+
expect(key.kid).toEqual(publicKeyRSA.kid!);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('signing with EdDSA', async () => {
|
|
355
|
+
const keys = await StaticKeyCollector.importKeys([privateKeyEdDSA]);
|
|
356
|
+
const store = new KeyStore(keys);
|
|
357
|
+
const signKey = (await jose.importJWK(privateKeyEdDSA)) as jose.KeyLike;
|
|
358
|
+
|
|
359
|
+
const signedJwt = await new jose.SignJWT({ claim: 'test-claim' })
|
|
360
|
+
.setProtectedHeader({ alg: 'EdDSA', kid: 'k2' })
|
|
361
|
+
.setSubject('f1')
|
|
362
|
+
.setIssuedAt()
|
|
363
|
+
.setIssuer('tester')
|
|
364
|
+
.setAudience('tests')
|
|
365
|
+
.setExpirationTime('5m')
|
|
366
|
+
.sign(signKey);
|
|
367
|
+
|
|
368
|
+
const verified = (await store.verifyJwt(signedJwt, {
|
|
369
|
+
defaultAudiences: ['tests'],
|
|
370
|
+
maxAge: '6m'
|
|
371
|
+
})) as JwtPayload & { claim: string };
|
|
372
|
+
|
|
373
|
+
expect(verified.claim).toEqual('test-claim');
|
|
341
374
|
});
|
|
342
375
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OplogEntry } from '@/util/protocol-types.js';
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import { validateBucket } from './bucket_validation.js';
|
|
4
|
+
import { reduceBucket } from '@/index.js';
|
|
4
5
|
|
|
5
6
|
// This tests the reduceBucket function.
|
|
6
7
|
// While this function is not used directly in the service implementation,
|