@syncular/client 0.30.14 → 0.30.16

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.
@@ -13,6 +13,7 @@
13
13
  * RPC-driven and auto-driven sync rounds serialize on one queue because
14
14
  * the core owns exactly one loop.
15
15
  */
16
+ import type { defineAuthorityReads } from './authority';
16
17
  import { SyncClient } from './client';
17
18
  import type { ClientDatabase } from './database';
18
19
  import {
@@ -51,6 +52,8 @@ import {
51
52
  } from './worker-protocol';
52
53
 
53
54
  export interface SyncWorkerOverrides {
55
+ /** Explicit authority reader factory. Required when init declares authorityReads. */
56
+ readonly createAuthorityReads?: typeof defineAuthorityReads;
54
57
  /** Database factory indirection: tests inject bun:sqlite here. */
55
58
  readonly openDatabase?: (
56
59
  config: WorkerInitConfig,
@@ -238,6 +241,11 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
238
241
  }
239
242
 
240
243
  async function init(config: WorkerInitConfig): Promise<WorkerInitResult> {
244
+ if (config.authorityReads !== undefined && !overrides.createAuthorityReads)
245
+ throw new ClientSyncError(
246
+ 'client.authority_read_forbidden',
247
+ 'authorityReads requires startSyncWorker({ createAuthorityReads })',
248
+ );
241
249
  if (client !== undefined) {
242
250
  throw new ClientSyncError(
243
251
  WORKER_FAILED_CODE,
@@ -323,6 +331,13 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
323
331
  ...(config.encryption !== undefined
324
332
  ? { encryption: encryptionConfigFromKeyring(config.encryption) }
325
333
  : {}),
334
+ ...(config.authorityReads !== undefined
335
+ ? {
336
+ authorityReads: overrides.createAuthorityReads!(
337
+ config.authorityReads,
338
+ ),
339
+ }
340
+ : {}),
326
341
  ...(config.securityPreflight !== undefined
327
342
  ? { securityPreflight: config.securityPreflight }
328
343
  : {}),
@@ -389,8 +404,46 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
389
404
  return { clientId: started.clientId };
390
405
  }
391
406
 
407
+ // Forward every argument to core methods, including authority's zero-argument check.
408
+ const coreMethods = [
409
+ 'securityLifecycle',
410
+ 'subscribe',
411
+ 'unsubscribe',
412
+ 'windowState',
413
+ 'purgeLocalData',
414
+ 'rebootstrapLocalData',
415
+ 'previousVersionSnapshot',
416
+ 'previousVersionAudit',
417
+ 'previousVersionDiscard',
418
+ 'query',
419
+ 'authoritySnapshot',
420
+ 'querySnapshot',
421
+ 'statusSnapshot',
422
+ 'conflicts',
423
+ 'rejections',
424
+ 'commitOutcome',
425
+ 'commitOutcomes',
426
+ 'resolveCommitOutcome',
427
+ 'pendingCommits',
428
+ 'subscriptions',
429
+ 'subscription',
430
+ 'connectRealtime',
431
+ 'disconnectRealtime',
432
+ 'setPresence',
433
+ 'presence',
434
+ 'uploadBlob',
435
+ 'fetchBlob',
436
+ ] as const satisfies readonly (keyof WorkerApi & keyof SyncClient)[];
392
437
  const api: WorkerApi = {
393
- securityLifecycle: () => requireClient().securityLifecycle(),
438
+ ...(Object.fromEntries(
439
+ coreMethods.map((method) => [
440
+ method,
441
+ (...args: unknown[]) => {
442
+ const running = requireClient();
443
+ return Reflect.apply(running[method], running, args);
444
+ },
445
+ ]),
446
+ ) as Pick<WorkerApi, (typeof coreMethods)[number]>),
394
447
  beginSecurityPreflight: async () => {
395
448
  if (backgroundTimer !== undefined) clearTimeout(backgroundTimer);
396
449
  backgroundTimer = undefined;
@@ -405,12 +458,9 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
405
458
  : {}),
406
459
  });
407
460
  },
408
- subscribe: (input) => requireClient().subscribe(input),
409
- unsubscribe: (id) => requireClient().unsubscribe(id),
410
461
  setWindow: async (base, units) => {
411
462
  await requireClient().setWindowCommand(base, units);
412
463
  },
413
- windowState: (base) => requireClient().windowState(base),
414
464
  mutate: (mutations) => {
415
465
  return requireClient().mutateCommand(mutations).value;
416
466
  },
@@ -425,13 +475,6 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
425
475
  );
426
476
  return result.value;
427
477
  },
428
- purgeLocalData: (input) => requireClient().purgeLocalData(input),
429
- rebootstrapLocalData: (input) =>
430
- requireClient().rebootstrapLocalData(input),
431
- previousVersionSnapshot: (spec) =>
432
- requireClient().previousVersionSnapshot(spec),
433
- previousVersionAudit: () => requireClient().previousVersionAudit(),
434
- previousVersionDiscard: () => requireClient().previousVersionDiscard(),
435
478
  sync: () => {
436
479
  const running = requireClient();
437
480
  return serializedSync(() => running.sync());
@@ -440,10 +483,7 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
440
483
  const running = requireClient();
441
484
  return serializedSync(() => running.syncUntilIdle(maxRounds));
442
485
  },
443
- query: (sql, params) => requireClient().query(sql, params),
444
- querySnapshot: (spec) => requireClient().querySnapshot(spec),
445
486
  localRevision: () => requireClient().localRevision,
446
- statusSnapshot: () => requireClient().statusSnapshot(),
447
487
  diagnosticsSnapshot: (request) => {
448
488
  const snapshot = requireClient().diagnosticsSnapshot(request);
449
489
  return withClientDiagnosticsHost(snapshot, {
@@ -453,22 +493,6 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
453
493
  realtime: snapshot.host.realtime,
454
494
  });
455
495
  },
456
- conflicts: () => requireClient().conflicts(),
457
- rejections: () => requireClient().rejections(),
458
- commitOutcome: (clientCommitId) =>
459
- requireClient().commitOutcome(clientCommitId),
460
- commitOutcomes: (query) => requireClient().commitOutcomes(query),
461
- resolveCommitOutcome: (input) =>
462
- requireClient().resolveCommitOutcome(input),
463
- pendingCommits: () => requireClient().pendingCommits(),
464
- subscriptions: () => requireClient().subscriptions(),
465
- subscription: (id) => requireClient().subscription(id),
466
- connectRealtime: () => requireClient().connectRealtime(),
467
- disconnectRealtime: () => requireClient().disconnectRealtime(),
468
- setPresence: (scopeKey, doc) => requireClient().setPresence(scopeKey, doc),
469
- presence: (scopeKey) => requireClient().presence(scopeKey),
470
- uploadBlob: (bytes, options) => requireClient().uploadBlob(bytes, options),
471
- fetchBlob: (blobIdOrRef) => requireClient().fetchBlob(blobIdOrRef),
472
496
  setHeaders: (next) => {
473
497
  headers = next;
474
498
  },
@@ -192,6 +192,8 @@ export interface SyncClientHandleConfig {
192
192
  readonly headers?: Readonly<Record<string, string>>;
193
193
  /** Structured-clone-safe E2EE keyring installed only in the leader worker. */
194
194
  readonly encryption?: EncryptionKeyringConfig;
195
+ /** Fixed plain authority reads permitted before security activation. */
196
+ readonly authorityReads?: import('./authority').AuthorityReadPolicy;
195
197
  /** Open the worker-owned replica behind the fail-closed security gate. */
196
198
  readonly securityPreflight?: boolean;
197
199
  readonly transportEnabled?: boolean;
@@ -656,6 +658,15 @@ export class SyncClientHandle implements PromiseMethods<WorkerApi> {
656
658
  return this.#call('query', [sql, params]);
657
659
  }
658
660
 
661
+ authoritySnapshot(): Promise<import('./authority').AuthoritySnapshot> {
662
+ if (arguments.length)
663
+ throw new ClientSyncError(
664
+ 'client.authority_read_forbidden',
665
+ 'authoritySnapshot accepts no arguments',
666
+ );
667
+ return this.#call('authoritySnapshot', []);
668
+ }
669
+
659
670
  querySnapshot<Row = SqlRow>(
660
671
  spec: QueryReadSpec,
661
672
  ): Promise<QuerySnapshot<Row>> {
@@ -951,6 +962,9 @@ function buildInitConfig(
951
962
  ...(config.transportEnabled !== undefined
952
963
  ? { transportEnabled: config.transportEnabled }
953
964
  : {}),
965
+ ...(config.authorityReads !== undefined
966
+ ? { authorityReads: config.authorityReads.declarations }
967
+ : {}),
954
968
  ...(config.securityPreflight !== undefined
955
969
  ? { securityPreflight: config.securityPreflight }
956
970
  : {}),
@@ -111,6 +111,7 @@ export interface WorkerInitConfig {
111
111
  /** Portable raw keyring installed inside the worker-owned client core. */
112
112
  readonly encryption?: EncryptionKeyringConfig;
113
113
  /** Open the worker-owned replica behind the fail-closed security gate. */
114
+ readonly authorityReads?: readonly import('./authority').AuthorityReadDeclaration[];
114
115
  readonly securityPreflight?: boolean;
115
116
  readonly transportEnabled?: boolean;
116
117
  readonly clientId?: string;
@@ -186,6 +187,7 @@ export interface WorkerApi extends Omit<
186
187
  sync(): Promise<SyncSummary>;
187
188
  syncUntilIdle(maxRounds?: number): Promise<SyncSummary>;
188
189
  query(sql: string, params?: readonly SqlValue[]): SqlRow[];
190
+ authoritySnapshot(): import('./authority').AuthoritySnapshot;
189
191
  querySnapshot(spec: QueryReadSpec): QuerySnapshot;
190
192
  localRevision(): LocalRevision;
191
193
  pendingCommits(): OutboxCommit[];