@spooky-sync/core 0.0.1-canary.250 → 0.0.1-canary.256
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/AGENTS.md +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +43 -3
- package/dist/types.d.ts +9 -0
- package/package.json +3 -3
- package/src/client/sp00ky-client.test.ts +23 -0
- package/src/client/sp00ky-client.ts +17 -0
- package/src/query/register.saga.test.ts +35 -1
- package/src/query/register.saga.ts +26 -0
- package/src/types.ts +9 -0
package/AGENTS.md
CHANGED
|
@@ -50,6 +50,7 @@ re-materialized from state, never patched in place.
|
|
|
50
50
|
- **`db.update` takes `(table, id, payload, options?)`**. The `debounced` option (`{ debounced: true }` or `{ debounced: { delay, key } }`) coalesces rapid updates — use it for CRDT text/title fields.
|
|
51
51
|
- **`db.remoteQuery(sql, vars?)` is the one-shot remote read.** It runs one SurrealQL statement through the client's remote path (connect gate, per-statement timeout, concurrency limit) and returns the SurrealDB result array; results are not synced into the local cache. Use it for counts and other reads the query builder can't express.
|
|
52
52
|
- **`db.useRemote(fn)` hands out the bare SDK client.** Same cache bypass, but it skips the connect gate (a call during a reconnect fails with "Specify a namespace to use"). Reserve it for SDK features `remoteQuery` cannot reach (transactions, `live`).
|
|
53
|
+
- **Raw remote calls are gated by the query allowlist.** When the generated schema carries `policy.queryAllowlist: 'warn' | 'enforce'` (from `sync.queryAllowlist` in `sp00ky.yml`), `remoteQuery`, `useRemote` and `queryRaw` throw unless the client config sets `allowRawRemote: true`; builder queries are unaffected. A registration the SSP refuses as not allowlisted (403 `not_allowlisted`) is marked `remote: 'failed'` after one attempt with an error-level log carrying the surql and a `spky generate` hint; the view stays cold (`membershipKnown` false), it is never read as confirmed empty.
|
|
53
54
|
- **`@parent` columns are auto-populated.** Defined in the `.surql` schema as `record<user>` with the `-- @parent` annotation; never write them yourself — they're set from the auth context server-side.
|
|
54
55
|
- **`db.preload` is a registered query.** Resolved before on this device (a `_00_view` row exists) it returns at once; never resolved it waits until the server's membership and every body are local. It holds no subscriber, so it is evicted a ttl after registration unless a view mounts the same query. `refresh`/`staleTime` are ignored; pass `signal` to abort a cold wait.
|
|
55
56
|
- **A rejected write is not lost.** Application errors roll the local row back (create/update/delete) and move the mutation to `_00_failed_mutations`; the tray API above lists, retries (as a fresh write) or discards it. Network errors keep the row queued.
|
package/dist/index.d.ts
CHANGED
|
@@ -2255,6 +2255,12 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2255
2255
|
query<Table extends TableNames<S>>(table: Table, options: QueryOptions<TableModel<GetTable<S, Table>>, false>, ttl?: QueryTimeToLive): QueryBuilder<S, Table, Sp00kyQueryResultPromise>;
|
|
2256
2256
|
private registerInput;
|
|
2257
2257
|
private initQuery;
|
|
2258
|
+
/**
|
|
2259
|
+
* The raw remote escape hatches bypass the query builder, so their shapes
|
|
2260
|
+
* can never be on the server's query allowlist. When the generated schema
|
|
2261
|
+
* says the allowlist is on, refuse them unless the app opted in explicitly.
|
|
2262
|
+
*/
|
|
2263
|
+
private assertRawRemoteAllowed;
|
|
2258
2264
|
queryRaw(sql: string, params: Record<string, any>, ttl: QueryTimeToLive): Promise<string>;
|
|
2259
2265
|
/**
|
|
2260
2266
|
* Preload = register a query nobody subscribes to. Resolved before on this
|
package/dist/index.js
CHANGED
|
@@ -1073,8 +1073,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
1073
1073
|
|
|
1074
1074
|
//#endregion
|
|
1075
1075
|
//#region src/modules/devtools/index.ts
|
|
1076
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
1077
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
1076
|
+
const CORE_VERSION = "0.0.1-canary.256";
|
|
1077
|
+
const WASM_VERSION = "0.0.1-canary.256";
|
|
1078
1078
|
const SURREAL_VERSION = "3.0.3";
|
|
1079
1079
|
var DevToolsService = class DevToolsService {
|
|
1080
1080
|
eventsHistory = [];
|
|
@@ -3219,6 +3219,13 @@ function* ensureRegistered(env, opts = {}) {
|
|
|
3219
3219
|
}
|
|
3220
3220
|
const stmt = (results, i) => results[i];
|
|
3221
3221
|
/**
|
|
3222
|
+
* The SSP answers a registration whose shape is not on the query allowlist
|
|
3223
|
+
* with HTTP 403 `{"error":"not_allowlisted"}`. It reaches the client as the
|
|
3224
|
+
* `fn::query::register` statement error carrying SurrealDB's http error text
|
|
3225
|
+
* (status reason and/or body), and no retry can change the answer.
|
|
3226
|
+
*/
|
|
3227
|
+
const isNotAllowlisted = (error) => /not_allowlisted|\b403\b|forbidden/i.test(String(error?.message ?? error));
|
|
3228
|
+
/**
|
|
3222
3229
|
* One remote registration: `fn::query::register` plus the edge/meta/children
|
|
3223
3230
|
* read in ONE request, then membership application. Concurrent with every
|
|
3224
3231
|
* other registration; retried on its own backoff; stops when the entry is
|
|
@@ -3260,6 +3267,27 @@ function* registerRemote(env, hash, retry = false) {
|
|
|
3260
3267
|
} catch (error) {
|
|
3261
3268
|
const current = yield fx.state.read((s) => s.queries.get(hash));
|
|
3262
3269
|
if (!current) return;
|
|
3270
|
+
if (isNotAllowlisted(error)) {
|
|
3271
|
+
yield fx.state.update(applyLifecycle(hash, { type: "remote-failed" }));
|
|
3272
|
+
yield fx.emit({
|
|
3273
|
+
type: "log",
|
|
3274
|
+
level: "error",
|
|
3275
|
+
message: "query refused: not allowlisted",
|
|
3276
|
+
data: {
|
|
3277
|
+
hash,
|
|
3278
|
+
table: entry.def.tableName,
|
|
3279
|
+
surql: entry.def.surql,
|
|
3280
|
+
params: Object.keys(entry.def.params),
|
|
3281
|
+
hint: "Add or regenerate the query allowlist: spky generate, then spky deploy (or restart spky dev)."
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
yield fx.dispatch({
|
|
3285
|
+
type: "SyncOutcome",
|
|
3286
|
+
ok: false,
|
|
3287
|
+
error
|
|
3288
|
+
});
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3263
3291
|
const attempts = current.registerAttempts + 1;
|
|
3264
3292
|
yield fx.state.update(bumpRegisterAttempts(hash));
|
|
3265
3293
|
yield fx.emit({
|
|
@@ -12516,7 +12544,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
12516
12544
|
return new TabsCoordinator({
|
|
12517
12545
|
tabId: s.tabId,
|
|
12518
12546
|
fingerprint: computeTabsFingerprint({
|
|
12519
|
-
coreVersion: "0.0.1-canary.
|
|
12547
|
+
coreVersion: "0.0.1-canary.256",
|
|
12520
12548
|
schemaHash: hash53(config.schemaSurql),
|
|
12521
12549
|
endpoint: config.database.endpoint ?? "",
|
|
12522
12550
|
namespace: config.database.namespace,
|
|
@@ -12993,7 +13021,17 @@ var Sp00kyClient = class {
|
|
|
12993
13021
|
initQuery(table, q, ttl) {
|
|
12994
13022
|
return this.runtime.run(registerLocal(this.env, this.registerInput(table, q, ttl)));
|
|
12995
13023
|
}
|
|
13024
|
+
/**
|
|
13025
|
+
* The raw remote escape hatches bypass the query builder, so their shapes
|
|
13026
|
+
* can never be on the server's query allowlist. When the generated schema
|
|
13027
|
+
* says the allowlist is on, refuse them unless the app opted in explicitly.
|
|
13028
|
+
*/
|
|
13029
|
+
assertRawRemoteAllowed(method) {
|
|
13030
|
+
const mode = this.config.schema.policy?.queryAllowlist;
|
|
13031
|
+
if ((mode === "warn" || mode === "enforce") && !this.config.allowRawRemote) throw new Error(`sp00ky: ${method} is disabled because sync.queryAllowlist is on for this schema; pass allowRawRemote: true in the client config to opt in`);
|
|
13032
|
+
}
|
|
12996
13033
|
async queryRaw(sql, params, ttl) {
|
|
13034
|
+
this.assertRawRemoteAllowed("queryRaw");
|
|
12997
13035
|
const tableName = sql.split("FROM ")[1]?.split(" ")[0] ?? "";
|
|
12998
13036
|
return this.runtime.run(registerLocal(this.env, {
|
|
12999
13037
|
tableName,
|
|
@@ -13144,9 +13182,11 @@ var Sp00kyClient = class {
|
|
|
13144
13182
|
return this.services.remote.getClient().invalidate();
|
|
13145
13183
|
}
|
|
13146
13184
|
async useRemote(fn) {
|
|
13185
|
+
this.assertRawRemoteAllowed("useRemote");
|
|
13147
13186
|
return fn(this.services.remote.getClient());
|
|
13148
13187
|
}
|
|
13149
13188
|
async remoteQuery(sql, vars) {
|
|
13189
|
+
this.assertRawRemoteAllowed("remoteQuery");
|
|
13150
13190
|
return this.services.remote.query(sql, vars);
|
|
13151
13191
|
}
|
|
13152
13192
|
feature(key, options) {
|
package/dist/types.d.ts
CHANGED
|
@@ -628,6 +628,15 @@ interface Sp00kyConfig<S extends SchemaStructure> {
|
|
|
628
628
|
* one-shot but never sync live.
|
|
629
629
|
*/
|
|
630
630
|
enableAnonymousLiveQueries?: boolean;
|
|
631
|
+
/**
|
|
632
|
+
* Opt in to the raw remote escape hatches (`useRemote`, `remoteQuery`,
|
|
633
|
+
* `queryRaw`) when the generated schema's `policy.queryAllowlist` is `warn`
|
|
634
|
+
* or `enforce`. With the allowlist on, the server only accepts query shapes
|
|
635
|
+
* that `spky generate` collected from the query builder, so a hand-written
|
|
636
|
+
* SurrealQL string cannot be allowlisted and these methods throw unless this
|
|
637
|
+
* flag is `true`. Defaults to `false`.
|
|
638
|
+
*/
|
|
639
|
+
allowRawRemote?: boolean;
|
|
631
640
|
/**
|
|
632
641
|
* Surface sustained sync failures as a "degraded" health status that the app
|
|
633
642
|
* can observe via `subscribeToSyncHealth` (or the client-solid
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.256",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
67
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
66
|
+
"@spooky-sync/query-builder": "0.0.1-canary.256",
|
|
67
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.256",
|
|
68
68
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
69
69
|
"@surrealdb/wasm": "^3.0.3",
|
|
70
70
|
"blurhash": "^2.0.5",
|
|
@@ -225,6 +225,29 @@ describe('Sp00kyClient facade (builder, devtools source, event fan-out)', () =>
|
|
|
225
225
|
runtime.emit({ type: 'activity:changed', fetching: 0, pending: 4 });
|
|
226
226
|
expect(pending).toEqual([0, 4]);
|
|
227
227
|
});
|
|
228
|
+
it('gates queryRaw/useRemote/remoteQuery behind allowRawRemote when the schema policy turns the allowlist on', async () => {
|
|
229
|
+
const services = fakeServiceBundle<any>();
|
|
230
|
+
const a = fakeAdapters({});
|
|
231
|
+
const runtime = new Runtime({ env: defaultEnv(schema), adapters: a.adapters, logger: services.logger, tabId: 't' });
|
|
232
|
+
const withPolicy = (mode: string | undefined, allowRawRemote?: boolean) =>
|
|
233
|
+
new Sp00kyClient<any>(
|
|
234
|
+
{ ...config, schema: mode === undefined ? schema : { ...schema, policy: { queryAllowlist: mode } }, allowRawRemote } as any,
|
|
235
|
+
{ services, runtime }
|
|
236
|
+
);
|
|
237
|
+
const msg = (m: string) => `sp00ky: ${m} is disabled because sync.queryAllowlist is on for this schema; pass allowRawRemote: true in the client config to opt in`;
|
|
238
|
+
for (const mode of ['warn', 'enforce']) {
|
|
239
|
+
const gated = withPolicy(mode);
|
|
240
|
+
await expect(gated.queryRaw('SELECT * FROM thing', {}, '10m')).rejects.toThrow(msg('queryRaw'));
|
|
241
|
+
await expect(gated.useRemote(() => 1)).rejects.toThrow(msg('useRemote'));
|
|
242
|
+
await expect(gated.remoteQuery('RETURN 1')).rejects.toThrow(msg('remoteQuery'));
|
|
243
|
+
expect(a.names().filter((n) => n === 'remote.query')).toEqual([]);
|
|
244
|
+
}
|
|
245
|
+
for (const client of [withPolicy('enforce', true), withPolicy('off'), withPolicy(undefined)]) {
|
|
246
|
+
await expect(client.useRemote(() => 1)).resolves.toBe(1);
|
|
247
|
+
await expect(client.remoteQuery('RETURN 1')).resolves.toBeDefined();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
228
251
|
it('honours config knobs when building the saga env', () => {
|
|
229
252
|
const services = fakeServiceBundle<any>();
|
|
230
253
|
const a = fakeAdapters();
|
|
@@ -269,7 +269,22 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
269
269
|
return this.runtime.run(registerLocal(this.env, this.registerInput(table, q, ttl)));
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* The raw remote escape hatches bypass the query builder, so their shapes
|
|
274
|
+
* can never be on the server's query allowlist. When the generated schema
|
|
275
|
+
* says the allowlist is on, refuse them unless the app opted in explicitly.
|
|
276
|
+
*/
|
|
277
|
+
private assertRawRemoteAllowed(method: string): void {
|
|
278
|
+
const mode = this.config.schema.policy?.queryAllowlist;
|
|
279
|
+
if ((mode === 'warn' || mode === 'enforce') && !this.config.allowRawRemote) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`sp00ky: ${method} is disabled because sync.queryAllowlist is on for this schema; pass allowRawRemote: true in the client config to opt in`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
async queryRaw(sql: string, params: Record<string, any>, ttl: QueryTimeToLive): Promise<string> {
|
|
287
|
+
this.assertRawRemoteAllowed('queryRaw');
|
|
273
288
|
const tableName = sql.split('FROM ')[1]?.split(' ')[0] ?? '';
|
|
274
289
|
return this.runtime.run(registerLocal(this.env, { tableName, surql: sql, params, ttl }));
|
|
275
290
|
}
|
|
@@ -436,10 +451,12 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
436
451
|
}
|
|
437
452
|
|
|
438
453
|
async useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T> {
|
|
454
|
+
this.assertRawRemoteAllowed('useRemote');
|
|
439
455
|
return fn(this.services.remote.getClient());
|
|
440
456
|
}
|
|
441
457
|
|
|
442
458
|
async remoteQuery<T extends unknown[]>(sql: string, vars?: Record<string, unknown>): Promise<T> {
|
|
459
|
+
this.assertRawRemoteAllowed('remoteQuery');
|
|
443
460
|
return this.services.remote.query<T>(sql, vars);
|
|
444
461
|
}
|
|
445
462
|
|
|
@@ -4,7 +4,7 @@ import { runPure, sha256Hex } from '../testing/run-pure';
|
|
|
4
4
|
import { buildEntry, buildState } from '../testing/build';
|
|
5
5
|
import { defaultEnv } from './env';
|
|
6
6
|
import { queryHashInput } from './hash';
|
|
7
|
-
import { ensureRegistered, registerLocal, registerRemote } from './register.saga';
|
|
7
|
+
import { ensureRegistered, isNotAllowlisted, registerLocal, registerRemote } from './register.saga';
|
|
8
8
|
import * as R from '../state/reducers';
|
|
9
9
|
import { emptyState } from '../state/client-state';
|
|
10
10
|
import type { StatementResult } from '../kernel/effects';
|
|
@@ -212,6 +212,40 @@ describe('registerRemote', () => {
|
|
|
212
212
|
expect(last.state.queries.get('a')!.lifecycle.remote).toBe('failed');
|
|
213
213
|
expect(last.timers.size).toBe(0);
|
|
214
214
|
});
|
|
215
|
+
it('not allowlisted: 403/Forbidden/not_allowlisted fails after ONE attempt with an error log, no retry timer', async () => {
|
|
216
|
+
for (const text of ['HTTP 403 Forbidden', 'There was an error processing a remote HTTP request: {"error":"not_allowlisted"}', 'status 403']) {
|
|
217
|
+
const out = await runPure(registerRemote(env, 'a'), {
|
|
218
|
+
state: buildState([entry]),
|
|
219
|
+
handlers: { 'remote.query': () => [{ status: 'ERR', error: text }] },
|
|
220
|
+
});
|
|
221
|
+
const e = out.state.queries.get('a')!;
|
|
222
|
+
expect(e.lifecycle).toMatchObject({ remote: 'failed', phase: 'cold', fetchDepth: 0 });
|
|
223
|
+
expect(e.registerAttempts).toBe(0);
|
|
224
|
+
expect(out.timers.size).toBe(0);
|
|
225
|
+
expect(out.emitted).toContainEqual(
|
|
226
|
+
expect.objectContaining({
|
|
227
|
+
type: 'log',
|
|
228
|
+
level: 'error',
|
|
229
|
+
message: 'query refused: not allowlisted',
|
|
230
|
+
data: expect.objectContaining({ hash: 'a', table: entry.def.tableName, surql: entry.def.surql, params: [] }),
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
expect(out.dispatched).toContainEqual(expect.objectContaining({ type: 'SyncOutcome', ok: false }));
|
|
234
|
+
}
|
|
235
|
+
const thrown = await runPure(registerRemote(env, 'a'), {
|
|
236
|
+
state: buildState([entry]),
|
|
237
|
+
handlers: {
|
|
238
|
+
'remote.query': () => {
|
|
239
|
+
throw new Error('Forbidden');
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
expect(thrown.state.queries.get('a')!.lifecycle.remote).toBe('failed');
|
|
244
|
+
expect(thrown.timers.size).toBe(0);
|
|
245
|
+
expect(isNotAllowlisted(new Error('socket closed'))).toBe(false);
|
|
246
|
+
expect(isNotAllowlisted('HTTP 4030')).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
215
249
|
it('tolerates ERR meta/children statements: edges still apply, missing meta reads as no row', async () => {
|
|
216
250
|
const out = await runPure(registerRemote(env, 'a'), {
|
|
217
251
|
state: buildState([entry]),
|
|
@@ -128,6 +128,15 @@ export function* ensureRegistered(env: SagaEnv, opts: { requireAuth?: boolean; a
|
|
|
128
128
|
|
|
129
129
|
const stmt = (results: StatementResult[], i: number): StatementResult | undefined => results[i];
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The SSP answers a registration whose shape is not on the query allowlist
|
|
133
|
+
* with HTTP 403 `{"error":"not_allowlisted"}`. It reaches the client as the
|
|
134
|
+
* `fn::query::register` statement error carrying SurrealDB's http error text
|
|
135
|
+
* (status reason and/or body), and no retry can change the answer.
|
|
136
|
+
*/
|
|
137
|
+
export const isNotAllowlisted = (error: unknown): boolean =>
|
|
138
|
+
/not_allowlisted|\b403\b|forbidden/i.test(String((error as { message?: unknown })?.message ?? error));
|
|
139
|
+
|
|
131
140
|
/**
|
|
132
141
|
* One remote registration: `fn::query::register` plus the edge/meta/children
|
|
133
142
|
* read in ONE request, then membership application. Concurrent with every
|
|
@@ -174,6 +183,23 @@ export function* registerRemote(env: SagaEnv, hash: QueryHash, retry = false): S
|
|
|
174
183
|
} catch (error) {
|
|
175
184
|
const current = (yield fx.state.read((s) => s.queries.get(hash))) as QueryEntry | undefined;
|
|
176
185
|
if (!current) return;
|
|
186
|
+
if (isNotAllowlisted(error)) {
|
|
187
|
+
yield fx.state.update(R.applyLifecycle(hash, { type: 'remote-failed' }));
|
|
188
|
+
yield fx.emit({
|
|
189
|
+
type: 'log',
|
|
190
|
+
level: 'error',
|
|
191
|
+
message: 'query refused: not allowlisted',
|
|
192
|
+
data: {
|
|
193
|
+
hash,
|
|
194
|
+
table: entry.def.tableName,
|
|
195
|
+
surql: entry.def.surql,
|
|
196
|
+
params: Object.keys(entry.def.params),
|
|
197
|
+
hint: 'Add or regenerate the query allowlist: spky generate, then spky deploy (or restart spky dev).',
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
yield fx.dispatch({ type: 'SyncOutcome', ok: false, error });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
177
203
|
const attempts = current.registerAttempts + 1;
|
|
178
204
|
yield fx.state.update(R.bumpRegisterAttempts(hash));
|
|
179
205
|
yield fx.emit({ type: 'log', level: 'warn', message: 'remote registration failed', data: { hash, attempts, error } });
|
package/src/types.ts
CHANGED
|
@@ -322,6 +322,15 @@ export interface Sp00kyConfig<S extends SchemaStructure> {
|
|
|
322
322
|
* one-shot but never sync live.
|
|
323
323
|
*/
|
|
324
324
|
enableAnonymousLiveQueries?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Opt in to the raw remote escape hatches (`useRemote`, `remoteQuery`,
|
|
327
|
+
* `queryRaw`) when the generated schema's `policy.queryAllowlist` is `warn`
|
|
328
|
+
* or `enforce`. With the allowlist on, the server only accepts query shapes
|
|
329
|
+
* that `spky generate` collected from the query builder, so a hand-written
|
|
330
|
+
* SurrealQL string cannot be allowlisted and these methods throw unless this
|
|
331
|
+
* flag is `true`. Defaults to `false`.
|
|
332
|
+
*/
|
|
333
|
+
allowRawRemote?: boolean;
|
|
325
334
|
/**
|
|
326
335
|
* Surface sustained sync failures as a "degraded" health status that the app
|
|
327
336
|
* can observe via `subscribeToSyncHealth` (or the client-solid
|