@syncular/tauri 0.30.13 → 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 +72 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +32 -9
- package/package.json +4 -4
- package/src/index.ts +43 -9
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
|
|
603
|
+
await shutdown;
|
|
583
604
|
}
|
|
584
605
|
finally {
|
|
585
|
-
this.#
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
-
"@syncular/server": "0.30.
|
|
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
|
|
968
|
+
await shutdown;
|
|
937
969
|
} finally {
|
|
938
|
-
this.#
|
|
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
|
: {}),
|