@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.
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/util/utils.ts"],"names":[],"mappings":"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"}
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.10.1",
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.21.0",
40
- "@powersync/service-types": "0.4.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",
@@ -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 SUPPORTED_ALGORITHMS = [...HS_ALGORITHMS, ...RSA_ALGORITHMS];
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 == this.source.alg;
44
- } else if (this.source.kty == 'RSA') {
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 == 'oct') {
47
+ } else if (this.source.kty === 'oct') {
47
48
  return HS_ALGORITHMS.includes(jwtAlg);
48
- } else {
49
- // We don't support 'ec' yet
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 == 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
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
- // Only RSA public keys supported here
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 {
@@ -199,6 +199,7 @@ export class MongoBucketStorage
199
199
  last_checkpoint: null,
200
200
  last_checkpoint_lsn: null,
201
201
  no_checkpoint_before: null,
202
+ keepalive_op: null,
202
203
  snapshot_done: false,
203
204
  state: SyncRuleState.PROCESSING,
204
205
  slot_name: slot_name,
@@ -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 (record.tag == 'update') {
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 == 'delete') {
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 new Promise((resolve) => setTimeout(resolve, Math.random() * 50));
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(`Waiting until ${this.no_checkpoint_before_lsn} before creating checkpoint, currently at ${lsn}`);
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 chance to also flush
688
- const t = 150;
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
- storeCurrentData: options.storeCurrentData
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
+ }
@@ -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
  });
@@ -1,6 +1,7 @@
1
1
  import { OplogEntry } from '@/util/protocol-types.js';
2
2
  import { describe, expect, test } from 'vitest';
3
- import { reduceBucket, validateBucket } from './bucket_validation.js';
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,