@syncular/tauri 0.30.14 → 0.30.15

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/README.md CHANGED
@@ -153,7 +153,8 @@ quarantine, or crash-resumed cleanup must finish before clinical data is
153
153
  available. The native database opens and migrates, but query/snapshot, mutation,
154
154
  subscription, sync, realtime, presence, blob, and automatic retry work fails
155
155
  with `client.security_preflight_required`. Status, local revision, lifecycle,
156
- and `purgeLocalData` remain available.
156
+ and `purgeLocalData` remain available. A creation-time `defineAuthorityReads`
157
+ policy also enables the read-only `authoritySnapshot()` described below.
157
158
 
158
159
  ```ts
159
160
  const client = await createTauriSyncClient({
@@ -180,6 +181,76 @@ with `setHeaders()` only after successful activation. Closed clients reject data
180
181
  and control calls with `client.closed` before testing preflight; listener
181
182
  registration and local progress reads throw that code. `close()` is idempotent.
182
183
 
184
+ ## Authority evidence before activation
185
+
186
+ Declare authority reads at client creation with `defineAuthorityReads` from
187
+ `@syncular/client`. Each table declares plain columns, including its primary
188
+ key and scope columns, and concrete scope selectors. These selectors remain
189
+ fixed for that client. A declaration cannot include encrypted, bytes, blob,
190
+ CRDT or internal columns. Choose only authority fields; clinical fields and
191
+ credentials do not belong in this policy.
192
+
193
+ ```ts
194
+ import { defineAuthorityReads } from '@syncular/client';
195
+
196
+ const client = await createTauriSyncClient({
197
+ schema,
198
+ securityPreflight: true,
199
+ transportEnabled: false,
200
+ authorityReads: defineAuthorityReads([{
201
+ table: 'memberships',
202
+ columns: ['id', 'user_id', 'facility_id', 'status', 'version'],
203
+ scopes: { membership_id: acceptedMembershipIds },
204
+ }]),
205
+ });
206
+ const evidence = await client.authoritySnapshot();
207
+ ```
208
+
209
+ The native application must also set an independent ceiling at plugin creation:
210
+
211
+ ```rust
212
+ let config = SyncularConfig {
213
+ authority_columns: [("memberships".into(), vec![
214
+ "id".into(), "user_id".into(), "facility_id".into(),
215
+ "status".into(), "version".into(),
216
+ ])].into(),
217
+ ..Default::default()
218
+ };
219
+ ```
220
+
221
+ Rust rejects any webview declaration outside that ceiling. It also validates
222
+ columns against the schema. `authoritySnapshot()` accepts zero arguments;
223
+ forged IPC carrying SQL, replacement tables or columns fails with
224
+ `client.authority_read_forbidden`. Ordinary `query`, `querySnapshot` and writes
225
+ still fail with `client.security_preflight_required`.
226
+
227
+ The result contains `revision: bigint`, `complete`, and `tables`. Every table
228
+ contains accepted `rows` (`values`, server `version`, `hasLocalIntent`), scoped
229
+ `localIntentRowIds`, the declared `scopes`, `coverage`, and sanitized
230
+ `persisted` subscription evidence (`requestedScopes`, `effectiveScopes`,
231
+ `cursor`, `status`, `complete`). It exposes no intended values, bootstrap
232
+ tokens, subscription parameters or keys. Both cores read rows, revision and
233
+ coverage in one SQLite snapshot. The native authority read runs on the mutable
234
+ owner; it does not use the ordinary query sidecar or its latency contract.
235
+
236
+ `coverage` is `complete`, `pending` or `missing`. Complete coverage requires
237
+ completed unfiltered subscriptions for every requested scope tuple. Several
238
+ subscriptions can cover the selection together. Scope loss and an unfinished
239
+ bootstrap invalidate completeness. Completed empty coverage has an empty row
240
+ set. The application must reject admission when an expected authority row is
241
+ absent, even when the set is complete.
242
+
243
+ Accepted bases remain separate from pending, failed and protected ACK intent.
244
+ A local-only creation appears only in `localIntentRowIds`. The application
245
+ validates the complete chain against independently accepted authority evidence,
246
+ actor/device identity, signed lease and trusted time before installing keys.
247
+ The SDK does not authorize the application. The read changes no lifecycle,
248
+ transport, keyring, subscriptions or rows.
249
+
250
+ The direct Bun/SQLite client and worker handle expose the same policy and
251
+ snapshot shape. Importing the policy is opt-in; ordinary clients do not ship
252
+ its reader. Update npm packages and native crates together and rebuild the app.
253
+
183
254
  ## Privacy-safe diagnostics
184
255
 
185
256
  `diagnosticsSnapshot({ expectedSubscriptions })` and `onDiagnostics(listener)`
package/dist/index.d.ts CHANGED
@@ -28,7 +28,7 @@ import type { ClientSnapshotMethods, PromiseMethods } from '@syncular/client';
28
28
  * `invoke`/`listen` either from its ESM entry points, from the ambient
29
29
  * `window.__TAURI__`, or via injected doubles (tests).
30
30
  */
31
- import type { ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataRebootstrapInput, LocalDataRebootstrapResult, MutationInput, PresencePeer, PreviousVersionAudit, PreviousVersionReadSpec, PreviousVersionSnapshot, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SecurityLifecycle, SqlRow, SqlValue, SyncClientConfig, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
31
+ import type { AuthorityReadPolicy, AuthoritySnapshot, ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataRebootstrapInput, LocalDataRebootstrapResult, MutationInput, PresencePeer, PreviousVersionAudit, PreviousVersionReadSpec, PreviousVersionSnapshot, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SecurityLifecycle, SqlRow, SqlValue, SyncClientConfig, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
32
32
  /** One event pushed on `syncular://event` (the derived client-observable set). */
33
33
  interface SyncularEvent {
34
34
  readonly type: string;
@@ -74,6 +74,8 @@ export interface TauriSyncClientConfig {
74
74
  * encoded into the native command envelope and never sent to the server.
75
75
  */
76
76
  readonly encryption?: EncryptionKeyringConfig;
77
+ /** Fixed plain authority reads permitted before security activation. */
78
+ readonly authorityReads?: AuthorityReadPolicy;
77
79
  /** Open the native replica behind the fail-closed security gate. */
78
80
  readonly securityPreflight?: boolean;
79
81
  /** Open local storage with network transport closed until explicitly resumed. */
@@ -126,6 +128,7 @@ export declare class TauriSyncClient implements PromiseMethods<ClientSnapshotMet
126
128
  onDiagnostics(listener: ClientDiagnosticsListener): () => void;
127
129
  onPresence(listener: (scopeKey: string) => void): () => void;
128
130
  query(sql: string, params?: readonly SqlValue[]): Promise<SqlRow[]>;
131
+ authoritySnapshot(): Promise<AuthoritySnapshot>;
129
132
  querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
130
133
  localRevision(): Promise<bigint>;
131
134
  statusSnapshot(): Promise<SyncStatusSnapshot>;
package/dist/index.js CHANGED
@@ -163,6 +163,7 @@ export class TauriSyncClient {
163
163
  this.#requireOpen();
164
164
  if (this.#securityLifecycle === 'preflight' &&
165
165
  ![
166
+ 'authoritySnapshot',
166
167
  'securityLifecycle',
167
168
  'beginSecurityPreflight',
168
169
  'activateSecurity',
@@ -364,6 +365,24 @@ export class TauriSyncClient {
364
365
  const rows = reply.result.rows ?? [];
365
366
  return rows.map((r) => decodeRow(r));
366
367
  }
368
+ async authoritySnapshot() {
369
+ this.#requireOpen();
370
+ if (arguments.length)
371
+ throw new TauriSyncError('client.authority_read_forbidden', 'authoritySnapshot accepts no arguments');
372
+ const result = (await this.#command('authoritySnapshot', {}));
373
+ this.#requireOpen();
374
+ return {
375
+ ...result,
376
+ revision: BigInt(result.revision),
377
+ tables: result.tables.map((table) => ({
378
+ ...table,
379
+ rows: table.rows.map((row) => ({
380
+ ...row,
381
+ values: decodeRow(row.values),
382
+ })),
383
+ })),
384
+ };
385
+ }
367
386
  async querySnapshot(spec) {
368
387
  this.#requireActive();
369
388
  const reply = await this.#tauri.invoke(`${PLUGIN}syncular_query_snapshot`, {
@@ -578,19 +597,20 @@ export class TauriSyncClient {
578
597
  async close() {
579
598
  if (this.#closed)
580
599
  return;
600
+ const shutdown = this.#command('shutdown', {});
601
+ this.#closed = true;
581
602
  try {
582
- await this.#command('shutdown', {});
603
+ await shutdown;
583
604
  }
584
605
  finally {
585
- this.#closed = true;
606
+ this.#unlisten?.();
607
+ this.#unlisten = undefined;
608
+ this.#invalidationListeners.clear();
609
+ this.#progress.clear();
610
+ this.#changeListeners.clear();
611
+ this.#diagnosticsListeners.clear();
612
+ this.#presenceListeners.clear();
586
613
  }
587
- this.#unlisten?.();
588
- this.#unlisten = undefined;
589
- this.#invalidationListeners.clear();
590
- this.#progress.clear();
591
- this.#changeListeners.clear();
592
- this.#diagnosticsListeners.clear();
593
- this.#presenceListeners.clear();
594
614
  }
595
615
  }
596
616
  function decodeDiagnosticsSnapshot(value, kind) {
@@ -743,6 +763,9 @@ export async function createTauriSyncClient(config) {
743
763
  ...(config.transportEnabled !== undefined
744
764
  ? { transportEnabled: config.transportEnabled }
745
765
  : {}),
766
+ ...(config.authorityReads !== undefined
767
+ ? { authorityReads: config.authorityReads.declarations }
768
+ : {}),
746
769
  ...(config.securityPreflight !== undefined
747
770
  ? { securityPreflight: config.securityPreflight }
748
771
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/tauri",
3
- "version": "0.30.14",
3
+ "version": "0.30.15",
4
4
  "description": "Tauri integration for the Syncular client",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -48,13 +48,13 @@
48
48
  "test": "bun test"
49
49
  },
50
50
  "dependencies": {
51
- "@syncular/client": "0.30.14"
51
+ "@syncular/client": "0.30.15"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@tauri-apps/api": ">=2.0.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@syncular/react": "0.30.14",
58
- "@syncular/server": "0.30.14"
57
+ "@syncular/react": "0.30.15",
58
+ "@syncular/server": "0.30.15"
59
59
  }
60
60
  }
package/src/index.ts CHANGED
@@ -34,6 +34,8 @@ import type { ClientSnapshotMethods, PromiseMethods } from '@syncular/client';
34
34
  */
35
35
 
36
36
  import type {
37
+ AuthorityReadPolicy,
38
+ AuthoritySnapshot,
37
39
  ClientChangeBatch,
38
40
  ClientChangeListener,
39
41
  ClientDiagnosticsListener,
@@ -139,6 +141,8 @@ export interface TauriSyncClientConfig {
139
141
  * encoded into the native command envelope and never sent to the server.
140
142
  */
141
143
  readonly encryption?: EncryptionKeyringConfig;
144
+ /** Fixed plain authority reads permitted before security activation. */
145
+ readonly authorityReads?: AuthorityReadPolicy;
142
146
  /** Open the native replica behind the fail-closed security gate. */
143
147
  readonly securityPreflight?: boolean;
144
148
  /** Open local storage with network transport closed until explicitly resumed. */
@@ -346,6 +350,7 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
346
350
  if (
347
351
  this.#securityLifecycle === 'preflight' &&
348
352
  ![
353
+ 'authoritySnapshot',
349
354
  'securityLifecycle',
350
355
  'beginSecurityPreflight',
351
356
  'activateSecurity',
@@ -588,6 +593,31 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
588
593
  return rows.map((r) => decodeRow(r as Record<string, unknown>));
589
594
  }
590
595
 
596
+ async authoritySnapshot(): Promise<AuthoritySnapshot> {
597
+ this.#requireOpen();
598
+ if (arguments.length)
599
+ throw new TauriSyncError(
600
+ 'client.authority_read_forbidden',
601
+ 'authoritySnapshot accepts no arguments',
602
+ );
603
+ const result = (await this.#command('authoritySnapshot', {})) as Omit<
604
+ AuthoritySnapshot,
605
+ 'revision'
606
+ > & { revision: string };
607
+ this.#requireOpen();
608
+ return {
609
+ ...result,
610
+ revision: BigInt(result.revision),
611
+ tables: result.tables.map((table) => ({
612
+ ...table,
613
+ rows: table.rows.map((row) => ({
614
+ ...row,
615
+ values: decodeRow(row.values),
616
+ })),
617
+ })),
618
+ };
619
+ }
620
+
591
621
  async querySnapshot<Row = SqlRow>(
592
622
  spec: QueryReadSpec,
593
623
  ): Promise<QuerySnapshot<Row>> {
@@ -932,18 +962,19 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
932
962
  /** Shut down the native core, release its keyring, then detach listeners. */
933
963
  async close(): Promise<void> {
934
964
  if (this.#closed) return;
965
+ const shutdown = this.#command('shutdown', {});
966
+ this.#closed = true;
935
967
  try {
936
- await this.#command('shutdown', {});
968
+ await shutdown;
937
969
  } finally {
938
- this.#closed = true;
970
+ this.#unlisten?.();
971
+ this.#unlisten = undefined;
972
+ this.#invalidationListeners.clear();
973
+ this.#progress.clear();
974
+ this.#changeListeners.clear();
975
+ this.#diagnosticsListeners.clear();
976
+ this.#presenceListeners.clear();
939
977
  }
940
- this.#unlisten?.();
941
- this.#unlisten = undefined;
942
- this.#invalidationListeners.clear();
943
- this.#progress.clear();
944
- this.#changeListeners.clear();
945
- this.#diagnosticsListeners.clear();
946
- this.#presenceListeners.clear();
947
978
  }
948
979
  }
949
980
 
@@ -1119,6 +1150,9 @@ export async function createTauriSyncClient(
1119
1150
  ...(config.transportEnabled !== undefined
1120
1151
  ? { transportEnabled: config.transportEnabled }
1121
1152
  : {}),
1153
+ ...(config.authorityReads !== undefined
1154
+ ? { authorityReads: config.authorityReads.declarations }
1155
+ : {}),
1122
1156
  ...(config.securityPreflight !== undefined
1123
1157
  ? { securityPreflight: config.securityPreflight }
1124
1158
  : {}),