@syncular/tauri 0.15.15 → 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 +20 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +44 -1
- package/package.json +3 -3
- package/src/index.ts +54 -1
package/README.md
CHANGED
|
@@ -54,6 +54,26 @@ detaching listeners, so disposing a resource does not leave a key-bearing core
|
|
|
54
54
|
behind. The Rust core overwrites owned key buffers on replacement/drop; the app
|
|
55
55
|
still owns OS secure-store deletion and any key buffers it supplied.
|
|
56
56
|
|
|
57
|
+
Runtime `setHeaders()` is an active-session operation and is rejected during
|
|
58
|
+
preflight at both the JavaScript and native command boundaries. Supply bootstrap
|
|
59
|
+
headers through trusted plugin configuration; rotate them only after successful
|
|
60
|
+
activation.
|
|
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
|
+
|
|
57
77
|
## React availability guard
|
|
58
78
|
|
|
59
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),
|
|
@@ -348,6 +377,7 @@ export class TauriSyncClient {
|
|
|
348
377
|
* the realtime socket applies it on its next (re)connect.
|
|
349
378
|
*/
|
|
350
379
|
async setHeaders(headers) {
|
|
380
|
+
this.#requireActive();
|
|
351
381
|
const reply = await this.#tauri.invoke(`${PLUGIN}syncular_set_headers`, { headers });
|
|
352
382
|
if (reply.error !== undefined) {
|
|
353
383
|
throw new TauriSyncError(reply.error.code, reply.error.message);
|
|
@@ -503,9 +533,22 @@ export class TauriSyncClient {
|
|
|
503
533
|
this.#unlisten = undefined;
|
|
504
534
|
this.#invalidationListeners.clear();
|
|
505
535
|
this.#changeListeners.clear();
|
|
536
|
+
this.#diagnosticsListeners.clear();
|
|
506
537
|
this.#presenceListeners.clear();
|
|
507
538
|
}
|
|
508
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
|
+
}
|
|
509
552
|
/** The error a `{error}` reply surfaces (mirrors the web-client `ClientSyncError`). */
|
|
510
553
|
export class TauriSyncError extends Error {
|
|
511
554
|
code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/tauri",
|
|
3
|
-
"version": "0.15.
|
|
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.
|
|
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.
|
|
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 {
|
|
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),
|
|
@@ -509,6 +546,7 @@ export class TauriSyncClient {
|
|
|
509
546
|
* the realtime socket applies it on its next (re)connect.
|
|
510
547
|
*/
|
|
511
548
|
async setHeaders(headers: Readonly<Record<string, string>>): Promise<void> {
|
|
549
|
+
this.#requireActive();
|
|
512
550
|
const reply = await this.#tauri.invoke<CommandReply>(
|
|
513
551
|
`${PLUGIN}syncular_set_headers`,
|
|
514
552
|
{ headers },
|
|
@@ -745,10 +783,25 @@ export class TauriSyncClient {
|
|
|
745
783
|
this.#unlisten = undefined;
|
|
746
784
|
this.#invalidationListeners.clear();
|
|
747
785
|
this.#changeListeners.clear();
|
|
786
|
+
this.#diagnosticsListeners.clear();
|
|
748
787
|
this.#presenceListeners.clear();
|
|
749
788
|
}
|
|
750
789
|
}
|
|
751
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
|
+
|
|
752
805
|
/** The error a `{error}` reply surfaces (mirrors the web-client `ClientSyncError`). */
|
|
753
806
|
export class TauriSyncError extends Error {
|
|
754
807
|
readonly code: string;
|