@powersync/service-core 0.11.0 → 0.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 +17 -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/mongo/MongoCompactor.js +2 -1
- package/dist/storage/mongo/MongoCompactor.js.map +1 -1
- package/dist/storage/mongo/MongoWriteCheckpointAPI.js +3 -2
- package/dist/storage/mongo/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/mongo/PersistedBatch.js +15 -5
- package/dist/storage/mongo/PersistedBatch.js.map +1 -1
- package/dist/storage/mongo/util.d.ts +14 -0
- package/dist/storage/mongo/util.js +39 -0
- package/dist/storage/mongo/util.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/mongo/MongoCompactor.ts +2 -1
- package/src/storage/mongo/MongoWriteCheckpointAPI.ts +5 -2
- package/src/storage/mongo/PersistedBatch.ts +18 -5
- package/src/storage/mongo/util.ts +45 -0
- package/test/src/auth.test.ts +54 -21
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -156,3 +156,48 @@ export function isUUID(value: any): value is bson.UUID {
|
|
|
156
156
|
const uuid = value as bson.UUID;
|
|
157
157
|
return uuid._bsontype == 'Binary' && uuid.sub_type == bson.Binary.SUBTYPE_UUID;
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* MongoDB bulkWrite internally splits the operations into batches
|
|
162
|
+
* so that no batch exceeds 16MB. However, there are cases where
|
|
163
|
+
* the batch size is very close to 16MB, where additional metadata
|
|
164
|
+
* on the server pushes it over the limit, resulting in this error
|
|
165
|
+
* from the server:
|
|
166
|
+
*
|
|
167
|
+
* > MongoBulkWriteError: BSONObj size: 16814023 (0x1008FC7) is invalid. Size must be between 0 and 16793600(16MB) First element: insert: "bucket_data"
|
|
168
|
+
*
|
|
169
|
+
* We work around the issue by doing our own batching, limiting the
|
|
170
|
+
* batch size to 15MB. This does add additional overhead with
|
|
171
|
+
* BSON.calculateObjectSize.
|
|
172
|
+
*/
|
|
173
|
+
export async function safeBulkWrite<T extends mongo.Document>(
|
|
174
|
+
collection: mongo.Collection<T>,
|
|
175
|
+
operations: mongo.AnyBulkWriteOperation<T>[],
|
|
176
|
+
options: mongo.BulkWriteOptions
|
|
177
|
+
) {
|
|
178
|
+
// Must be below 16MB.
|
|
179
|
+
// We could probably go a little closer, but 15MB is a safe threshold.
|
|
180
|
+
const BULK_WRITE_LIMIT = 15 * 1024 * 1024;
|
|
181
|
+
|
|
182
|
+
let batch: mongo.AnyBulkWriteOperation<T>[] = [];
|
|
183
|
+
let currentSize = 0;
|
|
184
|
+
// Estimated overhead per operation, should be smaller in reality.
|
|
185
|
+
const keySize = 8;
|
|
186
|
+
for (let op of operations) {
|
|
187
|
+
const bsonSize =
|
|
188
|
+
mongo.BSON.calculateObjectSize(op, {
|
|
189
|
+
checkKeys: false,
|
|
190
|
+
ignoreUndefined: true
|
|
191
|
+
} as any) + keySize;
|
|
192
|
+
if (batch.length > 0 && currentSize + bsonSize > BULK_WRITE_LIMIT) {
|
|
193
|
+
await collection.bulkWrite(batch, options);
|
|
194
|
+
currentSize = 0;
|
|
195
|
+
batch = [];
|
|
196
|
+
}
|
|
197
|
+
batch.push(op);
|
|
198
|
+
currentSize += bsonSize;
|
|
199
|
+
}
|
|
200
|
+
if (batch.length > 0) {
|
|
201
|
+
await collection.bulkWrite(batch, options);
|
|
202
|
+
}
|
|
203
|
+
}
|
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
|
});
|