@syncular/tauri 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.
- package/README.md +79 -1
- package/dist/authority.d.ts +10 -0
- package/dist/authority.js +26 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +26 -18
- package/package.json +12 -4
- package/src/authority.ts +51 -0
- package/src/index.ts +45 -11
package/README.md
CHANGED
|
@@ -153,7 +153,9 @@ 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 on `createTauriAuthoritySyncClient` enables the read-only
|
|
158
|
+
`authoritySnapshot()` described below.
|
|
157
159
|
|
|
158
160
|
```ts
|
|
159
161
|
const client = await createTauriSyncClient({
|
|
@@ -180,6 +182,82 @@ with `setHeaders()` only after successful activation. Closed clients reject data
|
|
|
180
182
|
and control calls with `client.closed` before testing preflight; listener
|
|
181
183
|
registration and local progress reads throw that code. `close()` is idempotent.
|
|
182
184
|
|
|
185
|
+
## Authority evidence before activation
|
|
186
|
+
|
|
187
|
+
Declare authority reads at client creation with `defineAuthorityReads` from
|
|
188
|
+
`@syncular/client/authority`. Each table declares plain columns, including
|
|
189
|
+
its primary key and scope columns, and concrete scope selectors. These selectors remain
|
|
190
|
+
fixed for that client. A declaration cannot include encrypted, bytes, blob,
|
|
191
|
+
CRDT or internal columns. Choose only authority fields; clinical fields and
|
|
192
|
+
credentials do not belong in this policy.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { defineAuthorityReads } from '@syncular/client/authority';
|
|
196
|
+
import { createTauriAuthoritySyncClient } from '@syncular/tauri/authority';
|
|
197
|
+
|
|
198
|
+
const client = await createTauriAuthoritySyncClient({
|
|
199
|
+
schema,
|
|
200
|
+
securityPreflight: true,
|
|
201
|
+
transportEnabled: false,
|
|
202
|
+
authorityReads: defineAuthorityReads([{
|
|
203
|
+
table: 'memberships',
|
|
204
|
+
columns: ['id', 'user_id', 'facility_id', 'status', 'version'],
|
|
205
|
+
scopes: { membership_id: acceptedMembershipIds },
|
|
206
|
+
}]),
|
|
207
|
+
});
|
|
208
|
+
const evidence = await client.authoritySnapshot();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Use `createTauriAuthoritySyncClient` for the bridge that exposes
|
|
212
|
+
`authoritySnapshot()`. The ordinary `createTauriSyncClient` class does not
|
|
213
|
+
contain that method or its reply decoder. Upgrade 0.30.15 callers to these
|
|
214
|
+
explicit subpaths; the snapshot and native security checks keep their semantics.
|
|
215
|
+
|
|
216
|
+
The native application must also set an independent ceiling at plugin creation:
|
|
217
|
+
|
|
218
|
+
```rust
|
|
219
|
+
let config = SyncularConfig {
|
|
220
|
+
authority_columns: [("memberships".into(), vec![
|
|
221
|
+
"id".into(), "user_id".into(), "facility_id".into(),
|
|
222
|
+
"status".into(), "version".into(),
|
|
223
|
+
])].into(),
|
|
224
|
+
..Default::default()
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Rust rejects any webview declaration outside that ceiling. It also validates
|
|
229
|
+
columns against the schema. `authoritySnapshot()` accepts zero arguments;
|
|
230
|
+
forged IPC carrying SQL, replacement tables or columns fails with
|
|
231
|
+
`client.authority_read_forbidden`. Ordinary `query`, `querySnapshot` and writes
|
|
232
|
+
still fail with `client.security_preflight_required`.
|
|
233
|
+
|
|
234
|
+
The result contains `revision: bigint`, `complete`, and `tables`. Every table
|
|
235
|
+
contains accepted `rows` (`values`, server `version`, `hasLocalIntent`), scoped
|
|
236
|
+
`localIntentRowIds`, the declared `scopes`, `coverage`, and sanitized
|
|
237
|
+
`persisted` subscription evidence (`requestedScopes`, `effectiveScopes`,
|
|
238
|
+
`cursor`, `status`, `complete`). It exposes no intended values, bootstrap
|
|
239
|
+
tokens, subscription parameters or keys. Both cores read rows, revision and
|
|
240
|
+
coverage in one SQLite snapshot. The native authority read runs on the mutable
|
|
241
|
+
owner; it does not use the ordinary query sidecar or its latency contract.
|
|
242
|
+
|
|
243
|
+
`coverage` is `complete`, `pending` or `missing`. Complete coverage requires
|
|
244
|
+
completed unfiltered subscriptions for every requested scope tuple. Several
|
|
245
|
+
subscriptions can cover the selection together. Scope loss and an unfinished
|
|
246
|
+
bootstrap invalidate completeness. Completed empty coverage has an empty row
|
|
247
|
+
set. The application must reject admission when an expected authority row is
|
|
248
|
+
absent, even when the set is complete.
|
|
249
|
+
|
|
250
|
+
Accepted bases remain separate from pending, failed and protected ACK intent.
|
|
251
|
+
A local-only creation appears only in `localIntentRowIds`. The application
|
|
252
|
+
validates the complete chain against independently accepted authority evidence,
|
|
253
|
+
actor/device identity, signed lease and trusted time before installing keys.
|
|
254
|
+
The SDK does not authorize the application. The read changes no lifecycle,
|
|
255
|
+
transport, keyring, subscriptions or rows.
|
|
256
|
+
|
|
257
|
+
The direct Bun/SQLite client and worker handle expose the same policy and
|
|
258
|
+
snapshot shape. Importing the policy is opt-in; ordinary clients do not ship
|
|
259
|
+
its reader. Update npm packages and native crates together and rebuild the app.
|
|
260
|
+
|
|
183
261
|
## Privacy-safe diagnostics
|
|
184
262
|
|
|
185
263
|
`diagnosticsSnapshot({ expectedSubscriptions })` and `onDiagnostics(listener)`
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AuthorityReadPolicy, AuthoritySnapshot } from '@syncular/client/authority';
|
|
2
|
+
import { TauriSyncClient, type TauriSyncClientConfig } from './index.js';
|
|
3
|
+
/** Explicit native bridge with the zero-argument preflight authority read. */
|
|
4
|
+
export declare class TauriAuthoritySyncClient extends TauriSyncClient {
|
|
5
|
+
authoritySnapshot(): Promise<AuthoritySnapshot>;
|
|
6
|
+
}
|
|
7
|
+
/** Construct an authority-enabled bridge; Rust still enforces its independent ceiling. */
|
|
8
|
+
export declare function createTauriAuthoritySyncClient(config: TauriSyncClientConfig & {
|
|
9
|
+
readonly authorityReads: AuthorityReadPolicy;
|
|
10
|
+
}): Promise<TauriAuthoritySyncClient>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createTauriSyncClient, decodeRow, TauriSyncClient, TauriSyncError, } from './index.js';
|
|
2
|
+
/** Explicit native bridge with the zero-argument preflight authority read. */
|
|
3
|
+
export class TauriAuthoritySyncClient extends TauriSyncClient {
|
|
4
|
+
async authoritySnapshot() {
|
|
5
|
+
this.requireOpen();
|
|
6
|
+
if (arguments.length)
|
|
7
|
+
throw new TauriSyncError('client.authority_read_forbidden', 'authoritySnapshot accepts no arguments');
|
|
8
|
+
const result = (await this.command('authoritySnapshot', {}));
|
|
9
|
+
this.requireOpen();
|
|
10
|
+
return {
|
|
11
|
+
...result,
|
|
12
|
+
revision: BigInt(result.revision),
|
|
13
|
+
tables: result.tables.map((table) => ({
|
|
14
|
+
...table,
|
|
15
|
+
rows: table.rows.map((row) => ({
|
|
16
|
+
...row,
|
|
17
|
+
values: decodeRow(row.values),
|
|
18
|
+
})),
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Construct an authority-enabled bridge; Rust still enforces its independent ceiling. */
|
|
24
|
+
export function createTauriAuthoritySyncClient(config) {
|
|
25
|
+
return createTauriSyncClient(config, (...args) => new TauriAuthoritySyncClient(...args));
|
|
26
|
+
}
|
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, 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. */
|
|
@@ -95,6 +97,8 @@ export interface TauriSyncClientConfig {
|
|
|
95
97
|
export type BytesEnvelope = {
|
|
96
98
|
readonly $bytes: string;
|
|
97
99
|
};
|
|
100
|
+
/** @internal Shared native row codec. */
|
|
101
|
+
export declare function decodeRow(row: Record<string, unknown>): SqlRow;
|
|
98
102
|
/**
|
|
99
103
|
* The webview-side proxy implementing `SyncClientLike` over the plugin. Every
|
|
100
104
|
* snapshot method returns a promise from an IPC round trip. React hooks accept
|
|
@@ -104,6 +108,10 @@ export declare class TauriSyncClient implements PromiseMethods<ClientSnapshotMet
|
|
|
104
108
|
#private;
|
|
105
109
|
/** @internal — use {@link createTauriSyncClient}. */
|
|
106
110
|
constructor(tauri: TauriApi, unlisten: () => void, securityLifecycle?: SecurityLifecycle);
|
|
111
|
+
/** @internal Shared command gate for explicit bridge extensions. */
|
|
112
|
+
protected command(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
113
|
+
/** @internal Recheck local close after an extension's asynchronous reply. */
|
|
114
|
+
protected requireOpen(): void;
|
|
107
115
|
/** @internal — fan an incoming plugin event out to the local listeners. */
|
|
108
116
|
__dispatchEvent(event: SyncularEvent): void;
|
|
109
117
|
securityLifecycle(): Promise<SecurityLifecycle>;
|
|
@@ -201,4 +209,6 @@ export declare class TauriSyncError extends Error {
|
|
|
201
209
|
* React `<SyncProvider client={…}>`.
|
|
202
210
|
*/
|
|
203
211
|
export declare function createTauriSyncClient(config: TauriSyncClientConfig): Promise<TauriSyncClient>;
|
|
212
|
+
/** @internal Explicit bridge extension used by the authority entry. */
|
|
213
|
+
export declare function createTauriSyncClient<Client extends TauriSyncClient>(config: TauriSyncClientConfig, createClient: (...args: ConstructorParameters<typeof TauriSyncClient>) => Client): Promise<Client>;
|
|
204
214
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,8 @@ function decodeCell(value) {
|
|
|
82
82
|
// string (SQLite json columns arrive as text already; this is defensive).
|
|
83
83
|
return JSON.stringify(value);
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
/** @internal Shared native row codec. */
|
|
86
|
+
export function decodeRow(row) {
|
|
86
87
|
const out = {};
|
|
87
88
|
for (const [key, value] of Object.entries(row)) {
|
|
88
89
|
// Reserved `_sync_*` columns stay engine-internal (parity with the
|
|
@@ -163,6 +164,7 @@ export class TauriSyncClient {
|
|
|
163
164
|
this.#requireOpen();
|
|
164
165
|
if (this.#securityLifecycle === 'preflight' &&
|
|
165
166
|
![
|
|
167
|
+
'authoritySnapshot',
|
|
166
168
|
'securityLifecycle',
|
|
167
169
|
'beginSecurityPreflight',
|
|
168
170
|
'activateSecurity',
|
|
@@ -189,6 +191,14 @@ export class TauriSyncClient {
|
|
|
189
191
|
throw new TauriSyncError('client.closed', 'the Tauri sync client is closed');
|
|
190
192
|
}
|
|
191
193
|
}
|
|
194
|
+
/** @internal Shared command gate for explicit bridge extensions. */
|
|
195
|
+
command(method, params) {
|
|
196
|
+
return this.#command(method, params);
|
|
197
|
+
}
|
|
198
|
+
/** @internal Recheck local close after an extension's asynchronous reply. */
|
|
199
|
+
requireOpen() {
|
|
200
|
+
this.#requireOpen();
|
|
201
|
+
}
|
|
192
202
|
#requireActive() {
|
|
193
203
|
this.#requireOpen();
|
|
194
204
|
if (this.#securityLifecycle === 'preflight')
|
|
@@ -578,19 +588,20 @@ export class TauriSyncClient {
|
|
|
578
588
|
async close() {
|
|
579
589
|
if (this.#closed)
|
|
580
590
|
return;
|
|
591
|
+
const shutdown = this.#command('shutdown', {});
|
|
592
|
+
this.#closed = true;
|
|
581
593
|
try {
|
|
582
|
-
await
|
|
594
|
+
await shutdown;
|
|
583
595
|
}
|
|
584
596
|
finally {
|
|
585
|
-
this.#
|
|
597
|
+
this.#unlisten?.();
|
|
598
|
+
this.#unlisten = undefined;
|
|
599
|
+
this.#invalidationListeners.clear();
|
|
600
|
+
this.#progress.clear();
|
|
601
|
+
this.#changeListeners.clear();
|
|
602
|
+
this.#diagnosticsListeners.clear();
|
|
603
|
+
this.#presenceListeners.clear();
|
|
586
604
|
}
|
|
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
605
|
}
|
|
595
606
|
}
|
|
596
607
|
function decodeDiagnosticsSnapshot(value, kind) {
|
|
@@ -700,13 +711,7 @@ function encodeMutation(mutation) {
|
|
|
700
711
|
}
|
|
701
712
|
return { ...mutation, values };
|
|
702
713
|
}
|
|
703
|
-
|
|
704
|
-
* Construct the bridge and issue the native `create` (opening/attaching the
|
|
705
|
-
* file DB on the Rust side per the plugin config). Returns a ready
|
|
706
|
-
* `TauriSyncClient` that satisfies `SyncClientLike` — pass it straight to the
|
|
707
|
-
* React `<SyncProvider client={…}>`.
|
|
708
|
-
*/
|
|
709
|
-
export async function createTauriSyncClient(config) {
|
|
714
|
+
export async function createTauriSyncClient(config, createClient = (...args) => new TauriSyncClient(...args)) {
|
|
710
715
|
const tauri = await resolveTauri(config.tauri);
|
|
711
716
|
// Wire the event stream BEFORE create, so no early change batch is missed.
|
|
712
717
|
const clientRef = {
|
|
@@ -719,7 +724,7 @@ export async function createTauriSyncClient(config) {
|
|
|
719
724
|
unlisten();
|
|
720
725
|
throw new TauriSyncError('sync.invalid_request', 'securityPreflight and encryption are mutually exclusive; install keys with activateSecurity after preflight');
|
|
721
726
|
}
|
|
722
|
-
const client =
|
|
727
|
+
const client = createClient(tauri, unlisten, config.securityPreflight === true ? 'preflight' : 'active');
|
|
723
728
|
clientRef.client = client;
|
|
724
729
|
// The native side owns every path (plugin config); the JS side names a
|
|
725
730
|
// database and supplies the schema, clientId, and limits.
|
|
@@ -743,6 +748,9 @@ export async function createTauriSyncClient(config) {
|
|
|
743
748
|
...(config.transportEnabled !== undefined
|
|
744
749
|
? { transportEnabled: config.transportEnabled }
|
|
745
750
|
: {}),
|
|
751
|
+
...(config.authorityReads !== undefined
|
|
752
|
+
? { authorityReads: config.authorityReads.declarations }
|
|
753
|
+
: {}),
|
|
746
754
|
...(config.securityPreflight !== undefined
|
|
747
755
|
? { securityPreflight: config.securityPreflight }
|
|
748
756
|
: {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/tauri",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.16",
|
|
4
4
|
"description": "Tauri integration for the Syncular client",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
"types": "./dist/index.d.ts",
|
|
34
34
|
"default": "./dist/index.js"
|
|
35
35
|
}
|
|
36
|
+
},
|
|
37
|
+
"./authority": {
|
|
38
|
+
"bun": "./src/authority.ts",
|
|
39
|
+
"browser": "./dist/authority.js",
|
|
40
|
+
"import": {
|
|
41
|
+
"types": "./dist/authority.d.ts",
|
|
42
|
+
"default": "./dist/authority.js"
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
},
|
|
38
46
|
"files": [
|
|
@@ -48,13 +56,13 @@
|
|
|
48
56
|
"test": "bun test"
|
|
49
57
|
},
|
|
50
58
|
"dependencies": {
|
|
51
|
-
"@syncular/client": "0.30.
|
|
59
|
+
"@syncular/client": "0.30.16"
|
|
52
60
|
},
|
|
53
61
|
"peerDependencies": {
|
|
54
62
|
"@tauri-apps/api": ">=2.0.0"
|
|
55
63
|
},
|
|
56
64
|
"devDependencies": {
|
|
57
|
-
"@syncular/react": "0.30.
|
|
58
|
-
"@syncular/server": "0.30.
|
|
65
|
+
"@syncular/react": "0.30.16",
|
|
66
|
+
"@syncular/server": "0.30.16"
|
|
59
67
|
}
|
|
60
68
|
}
|
package/src/authority.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthorityReadPolicy,
|
|
3
|
+
AuthoritySnapshot,
|
|
4
|
+
} from '@syncular/client/authority';
|
|
5
|
+
import {
|
|
6
|
+
createTauriSyncClient,
|
|
7
|
+
decodeRow,
|
|
8
|
+
TauriSyncClient,
|
|
9
|
+
TauriSyncError,
|
|
10
|
+
type TauriSyncClientConfig,
|
|
11
|
+
} from './index';
|
|
12
|
+
|
|
13
|
+
/** Explicit native bridge with the zero-argument preflight authority read. */
|
|
14
|
+
export class TauriAuthoritySyncClient extends TauriSyncClient {
|
|
15
|
+
async authoritySnapshot(): Promise<AuthoritySnapshot> {
|
|
16
|
+
this.requireOpen();
|
|
17
|
+
if (arguments.length)
|
|
18
|
+
throw new TauriSyncError(
|
|
19
|
+
'client.authority_read_forbidden',
|
|
20
|
+
'authoritySnapshot accepts no arguments',
|
|
21
|
+
);
|
|
22
|
+
const result = (await this.command('authoritySnapshot', {})) as Omit<
|
|
23
|
+
AuthoritySnapshot,
|
|
24
|
+
'revision'
|
|
25
|
+
> & { revision: string };
|
|
26
|
+
this.requireOpen();
|
|
27
|
+
return {
|
|
28
|
+
...result,
|
|
29
|
+
revision: BigInt(result.revision),
|
|
30
|
+
tables: result.tables.map((table) => ({
|
|
31
|
+
...table,
|
|
32
|
+
rows: table.rows.map((row) => ({
|
|
33
|
+
...row,
|
|
34
|
+
values: decodeRow(row.values),
|
|
35
|
+
})),
|
|
36
|
+
})),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Construct an authority-enabled bridge; Rust still enforces its independent ceiling. */
|
|
42
|
+
export function createTauriAuthoritySyncClient(
|
|
43
|
+
config: TauriSyncClientConfig & {
|
|
44
|
+
readonly authorityReads: AuthorityReadPolicy;
|
|
45
|
+
},
|
|
46
|
+
): Promise<TauriAuthoritySyncClient> {
|
|
47
|
+
return createTauriSyncClient(
|
|
48
|
+
config,
|
|
49
|
+
(...args) => new TauriAuthoritySyncClient(...args),
|
|
50
|
+
);
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { ClientSnapshotMethods, PromiseMethods } from '@syncular/client';
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import type {
|
|
37
|
+
AuthorityReadPolicy,
|
|
37
38
|
ClientChangeBatch,
|
|
38
39
|
ClientChangeListener,
|
|
39
40
|
ClientDiagnosticsListener,
|
|
@@ -139,6 +140,8 @@ export interface TauriSyncClientConfig {
|
|
|
139
140
|
* encoded into the native command envelope and never sent to the server.
|
|
140
141
|
*/
|
|
141
142
|
readonly encryption?: EncryptionKeyringConfig;
|
|
143
|
+
/** Fixed plain authority reads permitted before security activation. */
|
|
144
|
+
readonly authorityReads?: AuthorityReadPolicy;
|
|
142
145
|
/** Open the native replica behind the fail-closed security gate. */
|
|
143
146
|
readonly securityPreflight?: boolean;
|
|
144
147
|
/** Open local storage with network transport closed until explicitly resumed. */
|
|
@@ -246,7 +249,8 @@ function decodeCell(value: unknown): SqlValue {
|
|
|
246
249
|
return JSON.stringify(value);
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
|
|
252
|
+
/** @internal Shared native row codec. */
|
|
253
|
+
export function decodeRow(row: Record<string, unknown>): SqlRow {
|
|
250
254
|
const out: SqlRow = {};
|
|
251
255
|
for (const [key, value] of Object.entries(row)) {
|
|
252
256
|
// Reserved `_sync_*` columns stay engine-internal (parity with the
|
|
@@ -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',
|
|
@@ -389,6 +394,19 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
|
|
|
389
394
|
}
|
|
390
395
|
}
|
|
391
396
|
|
|
397
|
+
/** @internal Shared command gate for explicit bridge extensions. */
|
|
398
|
+
protected command(
|
|
399
|
+
method: string,
|
|
400
|
+
params: Record<string, unknown>,
|
|
401
|
+
): Promise<unknown> {
|
|
402
|
+
return this.#command(method, params);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** @internal Recheck local close after an extension's asynchronous reply. */
|
|
406
|
+
protected requireOpen(): void {
|
|
407
|
+
this.#requireOpen();
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
#requireActive(): void {
|
|
393
411
|
this.#requireOpen();
|
|
394
412
|
if (this.#securityLifecycle === 'preflight') this.#throwSecurityPreflight();
|
|
@@ -932,18 +950,19 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
|
|
|
932
950
|
/** Shut down the native core, release its keyring, then detach listeners. */
|
|
933
951
|
async close(): Promise<void> {
|
|
934
952
|
if (this.#closed) return;
|
|
953
|
+
const shutdown = this.#command('shutdown', {});
|
|
954
|
+
this.#closed = true;
|
|
935
955
|
try {
|
|
936
|
-
await
|
|
956
|
+
await shutdown;
|
|
937
957
|
} finally {
|
|
938
|
-
this.#
|
|
958
|
+
this.#unlisten?.();
|
|
959
|
+
this.#unlisten = undefined;
|
|
960
|
+
this.#invalidationListeners.clear();
|
|
961
|
+
this.#progress.clear();
|
|
962
|
+
this.#changeListeners.clear();
|
|
963
|
+
this.#diagnosticsListeners.clear();
|
|
964
|
+
this.#presenceListeners.clear();
|
|
939
965
|
}
|
|
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
966
|
}
|
|
948
967
|
}
|
|
949
968
|
|
|
@@ -1066,8 +1085,20 @@ function encodeMutation(mutation: MutationInput): unknown {
|
|
|
1066
1085
|
* `TauriSyncClient` that satisfies `SyncClientLike` — pass it straight to the
|
|
1067
1086
|
* React `<SyncProvider client={…}>`.
|
|
1068
1087
|
*/
|
|
1088
|
+
export function createTauriSyncClient(
|
|
1089
|
+
config: TauriSyncClientConfig,
|
|
1090
|
+
): Promise<TauriSyncClient>;
|
|
1091
|
+
/** @internal Explicit bridge extension used by the authority entry. */
|
|
1092
|
+
export function createTauriSyncClient<Client extends TauriSyncClient>(
|
|
1093
|
+
config: TauriSyncClientConfig,
|
|
1094
|
+
createClient: (
|
|
1095
|
+
...args: ConstructorParameters<typeof TauriSyncClient>
|
|
1096
|
+
) => Client,
|
|
1097
|
+
): Promise<Client>;
|
|
1069
1098
|
export async function createTauriSyncClient(
|
|
1070
1099
|
config: TauriSyncClientConfig,
|
|
1100
|
+
createClient = (...args: ConstructorParameters<typeof TauriSyncClient>) =>
|
|
1101
|
+
new TauriSyncClient(...args),
|
|
1071
1102
|
): Promise<TauriSyncClient> {
|
|
1072
1103
|
const tauri = await resolveTauri(config.tauri);
|
|
1073
1104
|
|
|
@@ -1090,7 +1121,7 @@ export async function createTauriSyncClient(
|
|
|
1090
1121
|
);
|
|
1091
1122
|
}
|
|
1092
1123
|
|
|
1093
|
-
const client =
|
|
1124
|
+
const client = createClient(
|
|
1094
1125
|
tauri,
|
|
1095
1126
|
unlisten,
|
|
1096
1127
|
config.securityPreflight === true ? 'preflight' : 'active',
|
|
@@ -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
|
: {}),
|