@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.
@@ -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
+ }
@@ -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
- const publicKey: jose.JWK = {
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([publicKey]);
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: publicKey.kid!,
106
+ kid: publicKeyRSA.kid!,
96
107
  alg: 'HS256',
97
- k: publicKey.n!
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: publicKey.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([publicKey, sharedKey, sharedKey2]);
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(publicKey)]
310
+ keys: [await KeySpec.importKey(publicKeyRSA)]
300
311
  });
301
312
 
302
313
  let key = (await cached.getKeys()).keys[0];
303
- expect(key.kid).toEqual(publicKey.kid!);
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(publicKey.kid!);
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(publicKey.kid!);
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(publicKey.kid!);
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(publicKey)]
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(publicKey.kid!);
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
  });