@syncular/tauri 0.15.16 → 0.15.17

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
@@ -59,6 +59,21 @@ preflight at both the JavaScript and native command boundaries. Supply bootstrap
59
59
  headers through trusted plugin configuration; rotate them only after successful
60
60
  activation.
61
61
 
62
+ ## Privacy-safe diagnostics
63
+
64
+ `diagnosticsSnapshot({ expectedSubscriptions })` and `onDiagnostics(listener)`
65
+ carry the native Rust core's versioned support evidence through the Tauri event
66
+ channel. The bridge marks the host as `{ kind: 'tauri', role: 'single' }`; it
67
+ does not infer state from IPC commands. Expected subscriptions accept only
68
+ stable PHI-free ids and generated table names, never scope values.
69
+
70
+ The snapshot is suitable for a redacted “copy diagnostics” workflow: it omits
71
+ rows, clinical row counts, scopes, SQL, paths, client/actor/lease ids, auth,
72
+ keys, mutations, stack traces, and arbitrary prose. Do not supplement it with
73
+ the SQLite file, WebView console dump, or application state. Diagnostics stays
74
+ blocked during security preflight because subscription/table evidence is
75
+ protected. See SPEC §7.6 and `@syncular/react`'s `useDiagnostics`.
76
+
62
77
  ## React availability guard
63
78
 
64
79
  The Tauri bridge carries `currentSchemaVersion`, `schemaFloor`, and migration
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * `invoke`/`listen` either from its ESM entry points, from the ambient
27
27
  * `window.__TAURI__`, or via injected doubles (tests).
28
28
  */
29
- import type { ClientChangeListener, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LeaseState, LocalDataPurgeInput, LocalDataPurgeResult, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SchemaFloor, SecurityLifecycle, SqlRow, SqlValue, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
29
+ import type { ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LeaseState, LocalDataPurgeInput, LocalDataPurgeResult, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SchemaFloor, SecurityLifecycle, SqlRow, SqlValue, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
30
30
  /** One event pushed on `syncular://event` (the derived client-observable set). */
31
31
  interface SyncularEvent {
32
32
  readonly type: string;
@@ -89,11 +89,13 @@ export declare class TauriSyncClient {
89
89
  }): Promise<void>;
90
90
  onInvalidate(listener: InvalidationListener): () => void;
91
91
  onChange(listener: ClientChangeListener): () => void;
92
+ onDiagnostics(listener: ClientDiagnosticsListener): () => void;
92
93
  onPresence(listener: (scopeKey: string) => void): () => void;
93
94
  query(sql: string, params?: readonly SqlValue[]): Promise<SqlRow[]>;
94
95
  querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
95
96
  localRevision(): Promise<bigint>;
96
97
  statusSnapshot(): Promise<SyncStatusSnapshot>;
98
+ diagnosticsSnapshot(request?: ClientDiagnosticsRequest): Promise<ClientDiagnosticsSnapshot>;
97
99
  mutate(mutations: readonly MutationInput[]): Promise<string>;
98
100
  patch(table: string, rowId: string, partial: Readonly<Record<string, unknown>>, options?: {
99
101
  readonly baseVersion?: number;
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@
29
29
  // -- Types the bridge speaks (structurally the web-client's) -----------------
30
30
  // Most imports stay type-only; the stable preflight error code is shared at
31
31
  // runtime so every host surfaces byte-identical policy evidence.
32
- import { SECURITY_PREFLIGHT_REQUIRED_CODE } from '@syncular/client';
32
+ import { SECURITY_PREFLIGHT_REQUIRED_CODE, withClientDiagnosticsHost, } from '@syncular/client';
33
33
  /** The plugin's Tauri event name — mirror of `tauri-plugin-syncular`. */
34
34
  export const SYNCULAR_EVENT = 'syncular://event';
35
35
  const PLUGIN = 'plugin:syncular|';
@@ -152,6 +152,7 @@ export class TauriSyncClient {
152
152
  #tauri;
153
153
  #invalidationListeners = new Set();
154
154
  #changeListeners = new Set();
155
+ #diagnosticsListeners = new Set();
155
156
  #presenceListeners = new Set();
156
157
  #unlisten;
157
158
  #closed = false;
@@ -233,6 +234,20 @@ export class TauriSyncClient {
233
234
  }
234
235
  break;
235
236
  }
237
+ case 'diagnostics': {
238
+ const snapshot = decodeDiagnosticsSnapshot(event.snapshot, 'tauri');
239
+ if (snapshot === undefined)
240
+ break;
241
+ for (const listener of this.#diagnosticsListeners) {
242
+ try {
243
+ listener(snapshot);
244
+ }
245
+ catch {
246
+ /* a diagnostics observer must never break event dispatch */
247
+ }
248
+ }
249
+ break;
250
+ }
236
251
  default:
237
252
  // Unknown extension events are deliberately ignored. All durable
238
253
  // observable state arrives in the revisioned `change` batch.
@@ -282,6 +297,10 @@ export class TauriSyncClient {
282
297
  this.#changeListeners.add(listener);
283
298
  return () => this.#changeListeners.delete(listener);
284
299
  }
300
+ onDiagnostics(listener) {
301
+ this.#diagnosticsListeners.add(listener);
302
+ return () => this.#diagnosticsListeners.delete(listener);
303
+ }
285
304
  onPresence(listener) {
286
305
  this.#presenceListeners.add(listener);
287
306
  return () => this.#presenceListeners.delete(listener);
@@ -319,6 +338,16 @@ export class TauriSyncClient {
319
338
  async statusSnapshot() {
320
339
  return (await this.#command('statusSnapshot', {}));
321
340
  }
341
+ async diagnosticsSnapshot(request = {}) {
342
+ const snapshot = (await this.#command('diagnosticsSnapshot', {
343
+ expectedSubscriptions: request.expectedSubscriptions ?? [],
344
+ }));
345
+ return withClientDiagnosticsHost(snapshot, {
346
+ ...snapshot.host,
347
+ kind: 'tauri',
348
+ role: 'single',
349
+ });
350
+ }
322
351
  async mutate(mutations) {
323
352
  const result = (await this.#command('mutate', {
324
353
  mutations: mutations.map(encodeMutation),
@@ -504,9 +533,22 @@ export class TauriSyncClient {
504
533
  this.#unlisten = undefined;
505
534
  this.#invalidationListeners.clear();
506
535
  this.#changeListeners.clear();
536
+ this.#diagnosticsListeners.clear();
507
537
  this.#presenceListeners.clear();
508
538
  }
509
539
  }
540
+ function decodeDiagnosticsSnapshot(value, kind) {
541
+ if (value === null || typeof value !== 'object')
542
+ return undefined;
543
+ const snapshot = value;
544
+ if (snapshot.version !== 1 || snapshot.host === undefined)
545
+ return undefined;
546
+ return withClientDiagnosticsHost(snapshot, {
547
+ ...snapshot.host,
548
+ kind,
549
+ role: 'single',
550
+ });
551
+ }
510
552
  /** The error a `{error}` reply surfaces (mirrors the web-client `ClientSyncError`). */
511
553
  export class TauriSyncError extends Error {
512
554
  code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/tauri",
3
- "version": "0.15.16",
3
+ "version": "0.15.17",
4
4
  "description": "Tauri integration for the Syncular client",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -48,12 +48,12 @@
48
48
  "test": "bun test"
49
49
  },
50
50
  "dependencies": {
51
- "@syncular/client": "0.15.16"
51
+ "@syncular/client": "0.15.17"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@tauri-apps/api": ">=2.0.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@syncular/react": "0.15.16"
57
+ "@syncular/react": "0.15.17"
58
58
  }
59
59
  }
package/src/index.ts CHANGED
@@ -30,6 +30,9 @@
30
30
  import type {
31
31
  ClientChangeBatch,
32
32
  ClientChangeListener,
33
+ ClientDiagnosticsListener,
34
+ ClientDiagnosticsRequest,
35
+ ClientDiagnosticsSnapshot,
33
36
  CommitOutcome,
34
37
  CommitOutcomeQuery,
35
38
  ConflictRecord,
@@ -56,7 +59,10 @@ import type {
56
59
  // -- Types the bridge speaks (structurally the web-client's) -----------------
57
60
  // Most imports stay type-only; the stable preflight error code is shared at
58
61
  // runtime so every host surfaces byte-identical policy evidence.
59
- import { SECURITY_PREFLIGHT_REQUIRED_CODE } from '@syncular/client';
62
+ import {
63
+ SECURITY_PREFLIGHT_REQUIRED_CODE,
64
+ withClientDiagnosticsHost,
65
+ } from '@syncular/client';
60
66
 
61
67
  /** A driver-protocol reply: `{result}` on success or `{error}` on failure. */
62
68
  interface CommandReply {
@@ -250,6 +256,7 @@ export class TauriSyncClient {
250
256
  readonly #tauri: TauriApi;
251
257
  readonly #invalidationListeners = new Set<InvalidationListener>();
252
258
  readonly #changeListeners = new Set<ClientChangeListener>();
259
+ readonly #diagnosticsListeners = new Set<ClientDiagnosticsListener>();
253
260
  readonly #presenceListeners = new Set<(scopeKey: string) => void>();
254
261
  #unlisten: (() => void) | undefined;
255
262
  #closed = false;
@@ -349,6 +356,18 @@ export class TauriSyncClient {
349
356
  }
350
357
  break;
351
358
  }
359
+ case 'diagnostics': {
360
+ const snapshot = decodeDiagnosticsSnapshot(event.snapshot, 'tauri');
361
+ if (snapshot === undefined) break;
362
+ for (const listener of this.#diagnosticsListeners) {
363
+ try {
364
+ listener(snapshot);
365
+ } catch {
366
+ /* a diagnostics observer must never break event dispatch */
367
+ }
368
+ }
369
+ break;
370
+ }
352
371
  default:
353
372
  // Unknown extension events are deliberately ignored. All durable
354
373
  // observable state arrives in the revisioned `change` batch.
@@ -414,6 +433,11 @@ export class TauriSyncClient {
414
433
  return () => this.#changeListeners.delete(listener);
415
434
  }
416
435
 
436
+ onDiagnostics(listener: ClientDiagnosticsListener): () => void {
437
+ this.#diagnosticsListeners.add(listener);
438
+ return () => this.#diagnosticsListeners.delete(listener);
439
+ }
440
+
417
441
  onPresence(listener: (scopeKey: string) => void): () => void {
418
442
  this.#presenceListeners.add(listener);
419
443
  return () => this.#presenceListeners.delete(listener);
@@ -470,6 +494,19 @@ export class TauriSyncClient {
470
494
  return (await this.#command('statusSnapshot', {})) as SyncStatusSnapshot;
471
495
  }
472
496
 
497
+ async diagnosticsSnapshot(
498
+ request: ClientDiagnosticsRequest = {},
499
+ ): Promise<ClientDiagnosticsSnapshot> {
500
+ const snapshot = (await this.#command('diagnosticsSnapshot', {
501
+ expectedSubscriptions: request.expectedSubscriptions ?? [],
502
+ })) as ClientDiagnosticsSnapshot;
503
+ return withClientDiagnosticsHost(snapshot, {
504
+ ...snapshot.host,
505
+ kind: 'tauri',
506
+ role: 'single',
507
+ });
508
+ }
509
+
473
510
  async mutate(mutations: readonly MutationInput[]): Promise<string> {
474
511
  const result = (await this.#command('mutate', {
475
512
  mutations: mutations.map(encodeMutation),
@@ -746,10 +783,25 @@ export class TauriSyncClient {
746
783
  this.#unlisten = undefined;
747
784
  this.#invalidationListeners.clear();
748
785
  this.#changeListeners.clear();
786
+ this.#diagnosticsListeners.clear();
749
787
  this.#presenceListeners.clear();
750
788
  }
751
789
  }
752
790
 
791
+ function decodeDiagnosticsSnapshot(
792
+ value: unknown,
793
+ kind: 'tauri',
794
+ ): ClientDiagnosticsSnapshot | undefined {
795
+ if (value === null || typeof value !== 'object') return undefined;
796
+ const snapshot = value as ClientDiagnosticsSnapshot;
797
+ if (snapshot.version !== 1 || snapshot.host === undefined) return undefined;
798
+ return withClientDiagnosticsHost(snapshot, {
799
+ ...snapshot.host,
800
+ kind,
801
+ role: 'single',
802
+ });
803
+ }
804
+
753
805
  /** The error a `{error}` reply surfaces (mirrors the web-client `ClientSyncError`). */
754
806
  export class TauriSyncError extends Error {
755
807
  readonly code: string;