@spooky-sync/core 0.0.1-canary.280 → 0.0.1-canary.281
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 +2 -0
- package/dist/index.d.ts +7 -329
- package/dist/index.js +748 -39
- package/dist/live.d.ts +141 -0
- package/dist/live.js +709 -0
- package/dist/pure.d.ts +322 -0
- package/dist/pure.js +24 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/types.d.ts +742 -3
- package/dist/worker-edges.js +415 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +46 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +16 -4
- package/scripts/check-worker-entries.mjs +172 -0
- package/src/client/services.ts +10 -38
- package/src/client/sp00ky-client.ts +13 -0
- package/src/index.ts +32 -0
- package/src/live/index.test.ts +478 -0
- package/src/live/index.ts +1013 -0
- package/src/modules/auth/index.ts +52 -0
- package/src/modules/web-push/index.test.ts +797 -0
- package/src/modules/web-push/index.ts +997 -0
- package/src/pure.ts +65 -0
- package/src/push/types.ts +174 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/testing/fake-services.ts +1 -1
- package/src/types.ts +11 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/sha256.ts +15 -0
- package/tsdown.config.ts +31 -2
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { QueryPlan, QueryPlan as QueryPlan$1, RecordId, SchemaStructure, WhereNode } from "@spooky-sync/query-builder";
|
|
2
|
-
import {
|
|
1
|
+
import { AccessDefinition, ColumnSchema, QueryPlan, QueryPlan as QueryPlan$1, RecordId, SchemaStructure, TypeNameToTypeMap, WhereNode } from "@spooky-sync/query-builder";
|
|
2
|
+
import { RecordId as RecordId$1, Surreal, SurrealEvents, SurrealTransaction } from "surrealdb";
|
|
3
|
+
import { Level, Level as Level$1, Logger, LoggerOptions } from "pino";
|
|
3
4
|
|
|
4
5
|
//#region src/modules/auth/impersonation.d.ts
|
|
5
6
|
|
|
@@ -480,6 +481,734 @@ type LocalEngineChoice = 'surrealdb' | 'sqlite' | LocalStore;
|
|
|
480
481
|
/** Thrown when relation decomposition nests past {@link MAX_RELATION_DEPTH} —
|
|
481
482
|
* a guard against a cyclic schema producing unbounded fan-out. */
|
|
482
483
|
//#endregion
|
|
484
|
+
//#region src/services/logger/index.d.ts
|
|
485
|
+
type Logger$1 = Logger;
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/services/database/database.d.ts
|
|
488
|
+
declare abstract class AbstractDatabaseService {
|
|
489
|
+
protected client: Surreal;
|
|
490
|
+
protected logger: Logger$1;
|
|
491
|
+
protected events: DatabaseEventSystem;
|
|
492
|
+
/**
|
|
493
|
+
* Per-query deadline in ms; `0` disables. The remote service sets it from
|
|
494
|
+
* `queryTimeoutMs` (see `RemoteDatabaseService`), the local one from
|
|
495
|
+
* `localOpTimeoutMs` (see `LocalDatabaseService`): a local query can be
|
|
496
|
+
* legitimately slow, but it must never be endless - every query waits on the
|
|
497
|
+
* previous link of {@link query}'s chain, and one that never settled wedged
|
|
498
|
+
* every later local op behind it.
|
|
499
|
+
*/
|
|
500
|
+
protected queryTimeoutMs: number;
|
|
501
|
+
/** The error a deadline expiry rejects with; the local service substitutes
|
|
502
|
+
* its typed `LocalOpTimeoutError`. "timed out" in the message is
|
|
503
|
+
* load-bearing either way: `classifySyncError` keys off it. */
|
|
504
|
+
protected timeoutError(_query: string): Error;
|
|
505
|
+
protected abstract eventType: typeof DatabaseEventTypes.LocalQuery | typeof DatabaseEventTypes.RemoteQuery;
|
|
506
|
+
constructor(client: Surreal, logger: Logger$1, events: DatabaseEventSystem);
|
|
507
|
+
abstract connect(): Promise<void>;
|
|
508
|
+
getClient(): Surreal;
|
|
509
|
+
getEvents(): DatabaseEventSystem;
|
|
510
|
+
tx(): Promise<SurrealTransaction>;
|
|
511
|
+
/**
|
|
512
|
+
* How many statements may be in flight at once.
|
|
513
|
+
*
|
|
514
|
+
* `1` serializes every statement behind the previous one. The local engines
|
|
515
|
+
* need that: overlapping ops trip the WASM / SQLite transaction layer. The
|
|
516
|
+
* remote service raises it (`RemoteDatabaseService`): a SurrealDB socket
|
|
517
|
+
* multiplexes requests by id, and serializing them capped the client at one
|
|
518
|
+
* statement per round trip (~5/s at 170ms RTT), so the `_00_list_ref` poll
|
|
519
|
+
* alone could hold every registration behind it and one slow one-shot read
|
|
520
|
+
* stalled the whole client. The scheduler's `MAX_CONCURRENT_DOWN` only means
|
|
521
|
+
* something when this is above 1.
|
|
522
|
+
*/
|
|
523
|
+
protected maxConcurrentQueries: number;
|
|
524
|
+
private inFlightQueries;
|
|
525
|
+
private queryWaiters;
|
|
526
|
+
/** FIFO slot acquisition, so `maxConcurrentQueries = 1` keeps strict order. */
|
|
527
|
+
private acquireQuerySlot;
|
|
528
|
+
private releaseQuerySlot;
|
|
529
|
+
/**
|
|
530
|
+
* Hook run right before a statement is sent, inside its slot. The remote
|
|
531
|
+
* service uses it to hold statements while a connect is in flight, so a
|
|
532
|
+
* query never runs on a socket that is open but has not had its namespace
|
|
533
|
+
* and token applied yet. The default does nothing.
|
|
534
|
+
*/
|
|
535
|
+
protected beforeQuery(): Promise<void>;
|
|
536
|
+
/**
|
|
537
|
+
* Execute a statement under {@link maxConcurrentQueries}.
|
|
538
|
+
*
|
|
539
|
+
* A statement that never settles would hold its slot forever, and with a
|
|
540
|
+
* limit of 1 that blocks every later statement behind it. {@link
|
|
541
|
+
* queryTimeoutMs} bounds each one: on expiry this promise rejects and the
|
|
542
|
+
* slot is released, even though the underlying RPC is still parked in the
|
|
543
|
+
* SDK's pending map.
|
|
544
|
+
*/
|
|
545
|
+
query<T extends unknown[]>(query: string, vars?: Record<string, unknown>): Promise<T>;
|
|
546
|
+
/**
|
|
547
|
+
* Like {@link query} but never rejects on a statement error: every
|
|
548
|
+
* statement answers on its own as `{ status: 'OK', result }` or
|
|
549
|
+
* `{ status: 'ERR', error }`. This is what lets a batched outbox push judge
|
|
550
|
+
* each mutation separately.
|
|
551
|
+
*/
|
|
552
|
+
queryResponses(query: string, vars?: Record<string, unknown>): Promise<Array<{
|
|
553
|
+
status: 'OK';
|
|
554
|
+
result: unknown;
|
|
555
|
+
} | {
|
|
556
|
+
status: 'ERR';
|
|
557
|
+
error: string;
|
|
558
|
+
}>>;
|
|
559
|
+
private runStatement;
|
|
560
|
+
execute<T>(query: SealedQuery<T>, vars?: Record<string, unknown>): Promise<T>;
|
|
561
|
+
close(): Promise<void>;
|
|
562
|
+
}
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/services/database/remote.d.ts
|
|
565
|
+
/** Transport events the SDK publishes, mapped 1:1 to {@link ConnectionState}. */
|
|
566
|
+
type RemoteConnectionEvent = ConnectionState | 'error';
|
|
567
|
+
declare class RemoteDatabaseService extends AbstractDatabaseService {
|
|
568
|
+
private config;
|
|
569
|
+
protected eventType: "DATABASE_REMOTE_QUERY";
|
|
570
|
+
private readonly reconnectConfig;
|
|
571
|
+
/**
|
|
572
|
+
* In-flight `connect()`, so concurrent callers (boot + supervisor revive +
|
|
573
|
+
* an `online` event landing at the same moment) share one attempt instead of
|
|
574
|
+
* racing two sockets. Cleared on settle, so a later call always reconnects.
|
|
575
|
+
*/
|
|
576
|
+
private connecting;
|
|
577
|
+
/**
|
|
578
|
+
* The token to re-authenticate a freshly opened socket with, kept current by
|
|
579
|
+
* {@link setAuthToken}.
|
|
580
|
+
*
|
|
581
|
+
* `config.token` is fixed at construction and most apps never set it: they
|
|
582
|
+
* sign in later, which authenticates the socket that happens to be open at
|
|
583
|
+
* the time. That is enough for the SDK's OWN reconnects (it replays
|
|
584
|
+
* `version`/`use`/`authenticate` itself), but not for the supervisor's revive
|
|
585
|
+
* loop, which builds a socket from scratch. Without this the revived socket
|
|
586
|
+
* came back UNAUTHENTICATED and stayed that way for the life of the page,
|
|
587
|
+
* while the client's `currentUserId` — restored from local storage — kept
|
|
588
|
+
* reporting the user as signed in.
|
|
589
|
+
*
|
|
590
|
+
* The visible damage is silent and total: `fn::query::register` sends
|
|
591
|
+
* `<string>($auth.id OR '')`, so every view registered afterwards is stamped
|
|
592
|
+
* with an empty identity, and every `$auth.id` predicate in it resolves
|
|
593
|
+
* false. Public tables keep returning rows while everything owned by the user
|
|
594
|
+
* returns nothing, on a page that still looks signed in.
|
|
595
|
+
*/
|
|
596
|
+
private authToken;
|
|
597
|
+
constructor(config: Sp00kyConfig<any>['database'], logger: Logger$1);
|
|
598
|
+
getConfig(): Sp00kyConfig<any>['database'];
|
|
599
|
+
/**
|
|
600
|
+
* Send one SurrealQL statement so that it survives the page going away.
|
|
601
|
+
*
|
|
602
|
+
* A WebSocket `send()` during `pagehide` is not guaranteed to flush — the
|
|
603
|
+
* browser may tear the socket down first, and the frame is simply lost.
|
|
604
|
+
* Measured: an unload-time release over the live socket reached the server
|
|
605
|
+
* zero times out of one. `fetch` with `keepalive` is the primitive the
|
|
606
|
+
* platform actually guarantees here, so this goes over SurrealDB's HTTP
|
|
607
|
+
* `/sql` endpoint instead of the RPC socket.
|
|
608
|
+
*
|
|
609
|
+
* Best-effort by design: no await, no retry, errors swallowed. Every caller
|
|
610
|
+
* must have a server-side fallback that makes a lost beacon a non-event.
|
|
611
|
+
*/
|
|
612
|
+
beaconSql(sql: string): void;
|
|
613
|
+
/**
|
|
614
|
+
* Record the token every future connect should authenticate with, or `null`
|
|
615
|
+
* on sign-out. See {@link authToken}.
|
|
616
|
+
*
|
|
617
|
+
* Does not touch the CURRENT socket: callers authenticate that themselves
|
|
618
|
+
* (sign-in and session restore both already do). This only makes the next
|
|
619
|
+
* from-scratch connect reproduce that state.
|
|
620
|
+
*/
|
|
621
|
+
setAuthToken(token: string | null): void;
|
|
622
|
+
/** Resolved reconnect tunables; the supervisor reads its own knobs here. */
|
|
623
|
+
getReconnectConfig(): Required<ReconnectConfig>;
|
|
624
|
+
/** Current transport state as reported by the SDK. */
|
|
625
|
+
getStatus(): ConnectionState;
|
|
626
|
+
/**
|
|
627
|
+
* Observe transport events. Thin passthrough so callers (the supervisor,
|
|
628
|
+
* sync, CRDT) don't have to reach through `getClient()`.
|
|
629
|
+
*/
|
|
630
|
+
subscribeConnection<K extends RemoteConnectionEvent>(event: K, cb: (...payload: SurrealEvents[K]) => void): () => void;
|
|
631
|
+
/**
|
|
632
|
+
* Tear the socket down on purpose. Used by the heartbeat watchdog when a
|
|
633
|
+
* socket stops answering but never closes: `close()` makes the SDK publish
|
|
634
|
+
* `disconnected`, which is what drives the supervisor's revive loop.
|
|
635
|
+
*/
|
|
636
|
+
forceClose(): Promise<void>;
|
|
637
|
+
/**
|
|
638
|
+
* Hold statements while a connect is in flight.
|
|
639
|
+
*
|
|
640
|
+
* `connect()` opens the socket, then selects the namespace/database, then
|
|
641
|
+
* authenticates - three round trips. The SDK considers the connection ready
|
|
642
|
+
* after the first, so a query queued during the other two ran on a socket
|
|
643
|
+
* with no namespace ("Specify a namespace to use") or, worse, no identity.
|
|
644
|
+
* Seen 2026-09-06 after the supervisor rebuilt a socket: a collection
|
|
645
|
+
* recount failed with exactly that error. Waiting on the in-flight connect
|
|
646
|
+
* costs nothing when no connect is running; a failed connect is not
|
|
647
|
+
* swallowed here - the query then fails on its own, as before.
|
|
648
|
+
*/
|
|
649
|
+
protected beforeQuery(): Promise<void>;
|
|
650
|
+
/**
|
|
651
|
+
* Open (or re-open) the remote connection.
|
|
652
|
+
*
|
|
653
|
+
* Safe to call repeatedly: concurrent calls share the in-flight attempt, and
|
|
654
|
+
* a call after a `disconnected` builds a fresh socket. `use()` and
|
|
655
|
+
* `authenticate()` are re-applied here for the cold path; the SDK also
|
|
656
|
+
* replays them itself on its own internal reconnects.
|
|
657
|
+
*/
|
|
658
|
+
connect(): Promise<void>;
|
|
659
|
+
private doConnect;
|
|
660
|
+
signin(params: any): Promise<any>;
|
|
661
|
+
signup(params: any): Promise<any>;
|
|
662
|
+
authenticate(token: string): Promise<any>;
|
|
663
|
+
invalidate(): Promise<void>;
|
|
664
|
+
}
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/modules/auth/events/index.d.ts
|
|
667
|
+
declare const AuthEventTypes: {
|
|
668
|
+
readonly AuthStateChanged: "AUTH_STATE_CHANGED";
|
|
669
|
+
readonly ImpersonationChanged: "IMPERSONATION_CHANGED";
|
|
670
|
+
};
|
|
671
|
+
type AuthEventTypeMap = {
|
|
672
|
+
[AuthEventTypes.AuthStateChanged]: EventDefinition<typeof AuthEventTypes.AuthStateChanged, string | null>;
|
|
673
|
+
[AuthEventTypes.ImpersonationChanged]: EventDefinition<typeof AuthEventTypes.ImpersonationChanged, ImpersonationInfo | null>;
|
|
674
|
+
};
|
|
675
|
+
type AuthEventSystem = EventSystem<AuthEventTypeMap>;
|
|
676
|
+
declare function createAuthEventSystem(): AuthEventSystem;
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/modules/auth/index.d.ts
|
|
679
|
+
/** What `fn::_00_impersonate::list_users` returns per row. */
|
|
680
|
+
interface ImpersonationCandidate {
|
|
681
|
+
id: string;
|
|
682
|
+
is_admin: boolean;
|
|
683
|
+
[field: string]: unknown;
|
|
684
|
+
}
|
|
685
|
+
/** An open session, as `fn::_00_impersonate::active` lists it. */
|
|
686
|
+
interface ActiveImpersonation {
|
|
687
|
+
session: string;
|
|
688
|
+
target: string;
|
|
689
|
+
admin: string;
|
|
690
|
+
reason: string;
|
|
691
|
+
started_at: unknown;
|
|
692
|
+
expires_at: unknown;
|
|
693
|
+
}
|
|
694
|
+
/** What a sign-out hook sees: the session that is about to end. */
|
|
695
|
+
interface SignOutContext {
|
|
696
|
+
/** `table:id` of the signed-in record, or null. */
|
|
697
|
+
userId: string | null;
|
|
698
|
+
token: string | null;
|
|
699
|
+
/** The session is an admin impersonation (its token is the target's). */
|
|
700
|
+
impersonating: boolean;
|
|
701
|
+
}
|
|
702
|
+
type SignOutHook = (ctx: SignOutContext) => Promise<void> | void;
|
|
703
|
+
/** Upper bound on the before-sign-out hooks, so a dead socket never holds a sign-out. */
|
|
704
|
+
declare const SIGN_OUT_HOOK_TIMEOUT_MS = 2000;
|
|
705
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
706
|
+
type MapColumnType<T extends ColumnSchema> = T['optional'] extends true ? TypeNameToTypeMap[T['type']] | undefined : TypeNameToTypeMap[T['type']];
|
|
707
|
+
type ExtractAccessParams<S extends SchemaStructure, Name extends keyof S['access'], Method extends 'signIn' | 'signup'> = S['access'] extends undefined ? never : S['access'][Name] extends AccessDefinition ? Prettify<{ [K in keyof S['access'][Name][Method]['params']]: MapColumnType<S['access'][Name][Method]['params'][K]> }> : never;
|
|
708
|
+
declare class AuthService<S extends SchemaStructure> {
|
|
709
|
+
private schema;
|
|
710
|
+
private remote;
|
|
711
|
+
private persistenceClient;
|
|
712
|
+
private logger;
|
|
713
|
+
token: string | null;
|
|
714
|
+
currentUser: any | null;
|
|
715
|
+
isAuthenticated: boolean;
|
|
716
|
+
/**
|
|
717
|
+
* The record-access method name for the current session (e.g. `"account"`),
|
|
718
|
+
* derived from the token's `AC` claim. Consumed by the in-browser SSP's
|
|
719
|
+
* permission injection so `$access`-gated table predicates resolve locally,
|
|
720
|
+
* mirroring the server's `$access`. Null when logged out.
|
|
721
|
+
*/
|
|
722
|
+
access: string | null;
|
|
723
|
+
isLoading: boolean;
|
|
724
|
+
private events;
|
|
725
|
+
private signOutHooks;
|
|
726
|
+
/** The impersonation last announced, for change detection. */
|
|
727
|
+
private announcedImpersonation;
|
|
728
|
+
/** Target of an impersonation that just ended, until the client purges its
|
|
729
|
+
* local bucket (see `consumeEndedImpersonation`). */
|
|
730
|
+
private endedImpersonationTarget;
|
|
731
|
+
private impersonationTimers;
|
|
732
|
+
get eventSystem(): AuthEventSystem;
|
|
733
|
+
constructor(schema: S, remote: RemoteDatabaseService, persistenceClient: PersistenceClient, logger: Logger$1);
|
|
734
|
+
init(): Promise<void>;
|
|
735
|
+
getAccessDefinition<Name extends keyof S['access']>(name: Name): AccessDefinition | undefined;
|
|
736
|
+
/**
|
|
737
|
+
* Subscribe to auth state changes.
|
|
738
|
+
* callback is called immediately with current value and whenever validation status changes.
|
|
739
|
+
*/
|
|
740
|
+
subscribe(cb: (userId: string | null) => void): () => void;
|
|
741
|
+
/**
|
|
742
|
+
* Run `hook` at the start of every sign-out, while the token and the socket
|
|
743
|
+
* still belong to the leaving user (Web Push removes this device's
|
|
744
|
+
* subscription here). Hooks are best-effort: they run in parallel, errors
|
|
745
|
+
* are swallowed, and the sign-out goes on after
|
|
746
|
+
* {@link SIGN_OUT_HOOK_TIMEOUT_MS} whatever they are doing. Returns a disposer.
|
|
747
|
+
*/
|
|
748
|
+
onBeforeSignOut(hook: SignOutHook): () => void;
|
|
749
|
+
private runSignOutHooks;
|
|
750
|
+
private notifyListeners;
|
|
751
|
+
/**
|
|
752
|
+
* The impersonation the current session is, or null. Derived from the
|
|
753
|
+
* token in use, so it always describes the identity the server sees.
|
|
754
|
+
*/
|
|
755
|
+
get impersonation(): ImpersonationInfo | null;
|
|
756
|
+
/** Subscribe to impersonation changes; called immediately with the current value. */
|
|
757
|
+
subscribeImpersonation(cb: (info: ImpersonationInfo | null) => void): () => void;
|
|
758
|
+
/**
|
|
759
|
+
* Act as `targetId` (a record id such as `user:abc`). Admins only, and only
|
|
760
|
+
* when the project enabled `impersonation` in sp00ky.yml: the server refuses
|
|
761
|
+
* everything else. `reason` is recorded in the audit log.
|
|
762
|
+
*
|
|
763
|
+
* The admin's session is kept aside and restored by
|
|
764
|
+
* {@link stopImpersonating}. Local data switches to the target's bucket
|
|
765
|
+
* exactly as on a sign-in, and that bucket is removed on stop.
|
|
766
|
+
*/
|
|
767
|
+
impersonate(targetId: string, reason: string): Promise<ImpersonationInfo>;
|
|
768
|
+
/**
|
|
769
|
+
* Return to the admin session. Always leaves the impersonated identity,
|
|
770
|
+
* even when the server cannot be reached: the admin token is restored
|
|
771
|
+
* locally first and verified afterwards, and a rejected admin token ends in
|
|
772
|
+
* a full sign-out rather than staying on the target.
|
|
773
|
+
*/
|
|
774
|
+
stopImpersonating(opts?: {
|
|
775
|
+
serverEnded?: boolean;
|
|
776
|
+
}): Promise<void>;
|
|
777
|
+
/** Re-sign the impersonation token before it expires. */
|
|
778
|
+
renewImpersonation(): Promise<void>;
|
|
779
|
+
/** Users an admin may pick from. Admin-only on the server. */
|
|
780
|
+
searchImpersonationTargets(search: string): Promise<ImpersonationCandidate[]>;
|
|
781
|
+
/** Open impersonation sessions. Admin-only on the server. */
|
|
782
|
+
listActiveImpersonations(): Promise<ActiveImpersonation[]>;
|
|
783
|
+
/**
|
|
784
|
+
* The target of an impersonation that ended, once. The bucket switch calls
|
|
785
|
+
* this after leaving a bucket so it can delete that user's local data.
|
|
786
|
+
*/
|
|
787
|
+
consumeEndedImpersonation(): string | null;
|
|
788
|
+
/**
|
|
789
|
+
* Restore a session from the locally cached JWT, with NO network.
|
|
790
|
+
*
|
|
791
|
+
* This is what makes a warm boot paint instantly and what makes an offline
|
|
792
|
+
* boot possible at all: the token is in local storage, and it already carries
|
|
793
|
+
* both the access method and the `$auth.id` record id. Everything the client
|
|
794
|
+
* needs to route queries (`setCurrentUserId`) and to satisfy `$auth`-gated
|
|
795
|
+
* permission predicates in the in-browser SSP (`setSessionAuth`) is therefore
|
|
796
|
+
* available before a socket exists.
|
|
797
|
+
*
|
|
798
|
+
* The session is OPTIMISTIC: the token is unverified here. `check()` runs
|
|
799
|
+
* afterwards in the background and downgrades to a real sign-out if the
|
|
800
|
+
* server rejects it. Nothing is trusted that the server has not also seen -
|
|
801
|
+
* the local store only ever holds rows the server previously sent.
|
|
802
|
+
*
|
|
803
|
+
* Returns the restored user id, or null when there is no usable token.
|
|
804
|
+
*/
|
|
805
|
+
restoreSessionFromToken(): Promise<string | null>;
|
|
806
|
+
/**
|
|
807
|
+
* Check for existing session and validate
|
|
808
|
+
*/
|
|
809
|
+
check(accessToken?: string): Promise<void>;
|
|
810
|
+
/**
|
|
811
|
+
* The server refused `token`. A refused impersonation token (stopped,
|
|
812
|
+
* expired, or revoked) returns to the admin session instead of signing the
|
|
813
|
+
* admin out; anything else signs out.
|
|
814
|
+
*/
|
|
815
|
+
private rejectToken;
|
|
816
|
+
/**
|
|
817
|
+
* Sign out and clear session. While impersonating this ends the
|
|
818
|
+
* impersonation and signs the admin out too.
|
|
819
|
+
*/
|
|
820
|
+
signOut(): Promise<void>;
|
|
821
|
+
private setSession;
|
|
822
|
+
/** Fallback when the token carries no `AC` claim: if the schema defines
|
|
823
|
+
* exactly one record-access method, assume the session used it. */
|
|
824
|
+
private defaultAccessName;
|
|
825
|
+
signUp<Name extends keyof S['access'] & string>(accessName: Name, params: ExtractAccessParams<S, Name, 'signup'>): Promise<void>;
|
|
826
|
+
signIn<Name extends keyof S['access'] & string>(accessName: Name, params: ExtractAccessParams<S, Name, 'signIn'>): Promise<void>;
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/push/types.d.ts
|
|
830
|
+
/**
|
|
831
|
+
* Web Push wire types shared by the page (`db.webPush`), the service worker
|
|
832
|
+
* bridge (`@spooky-sync/core/sw`) and the live feed. Pure: types plus a few
|
|
833
|
+
* dependency-free helpers, safe in any context.
|
|
834
|
+
*
|
|
835
|
+
* `PushPayload` mirrors `packages/push-core/src/config.rs` (`PushPayload`,
|
|
836
|
+
* `PayloadKind`, `Op`, `NotificationTemplate`). Keep the two in step.
|
|
837
|
+
*/
|
|
838
|
+
/** Payload format version this build understands. */
|
|
839
|
+
declare const PUSH_PAYLOAD_VERSION = 1;
|
|
840
|
+
type PushPayloadKind = 'rule' | 'message';
|
|
841
|
+
type PushOp = 'create' | 'update' | 'delete';
|
|
842
|
+
type PushUrgency = 'very-low' | 'low' | 'normal' | 'high';
|
|
843
|
+
/** One button on a notification (`NotificationAction` plus a per-action `url`). */
|
|
844
|
+
interface PushNotificationAction {
|
|
845
|
+
action: string;
|
|
846
|
+
title: string;
|
|
847
|
+
icon?: string;
|
|
848
|
+
/** Opened instead of the notification's `url` when this action is clicked. */
|
|
849
|
+
url?: string;
|
|
850
|
+
[key: string]: unknown;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* What a content push shows. Every key other than `title` and `url` is passed
|
|
854
|
+
* to `showNotification` as is, unknown ones included (`vibrate`, `timestamp`,
|
|
855
|
+
* whatever a browser adds later).
|
|
856
|
+
*/
|
|
857
|
+
interface PushNotification {
|
|
858
|
+
title?: string;
|
|
859
|
+
body?: string;
|
|
860
|
+
icon?: string;
|
|
861
|
+
badge?: string;
|
|
862
|
+
image?: string;
|
|
863
|
+
lang?: string;
|
|
864
|
+
dir?: string;
|
|
865
|
+
/** Defaults to the push's `topic`. */
|
|
866
|
+
tag?: string;
|
|
867
|
+
/** Opened (or focused) on click. */
|
|
868
|
+
url?: string;
|
|
869
|
+
requireInteraction?: boolean;
|
|
870
|
+
renotify?: boolean;
|
|
871
|
+
silent?: boolean;
|
|
872
|
+
actions?: PushNotificationAction[];
|
|
873
|
+
/** Merged into the notification's `data` next to the bridge's own keys. */
|
|
874
|
+
data?: Record<string, unknown>;
|
|
875
|
+
[key: string]: unknown;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* The JSON every push carries (then RFC 8291 encrypted). `notification`
|
|
879
|
+
* present: a content push the service worker shows as is. Absent: a
|
|
880
|
+
* content-free nudge, the app renders from its own data.
|
|
881
|
+
*/
|
|
882
|
+
interface PushPayload {
|
|
883
|
+
v: number;
|
|
884
|
+
kind: PushPayloadKind;
|
|
885
|
+
/** Rule name (`kind: 'rule'`). */
|
|
886
|
+
rule?: string;
|
|
887
|
+
/** `_00_push_message` id (`kind: 'message'`). */
|
|
888
|
+
message?: string;
|
|
889
|
+
/** The row that fired the rule. */
|
|
890
|
+
table?: string;
|
|
891
|
+
id?: string;
|
|
892
|
+
op?: PushOp;
|
|
893
|
+
topic?: string;
|
|
894
|
+
notification?: PushNotification;
|
|
895
|
+
data?: Record<string, unknown>;
|
|
896
|
+
/** Epoch ms when the engine built it. */
|
|
897
|
+
ts: number;
|
|
898
|
+
}
|
|
899
|
+
/** Page -> service worker: the session to use for live rendering. */
|
|
900
|
+
interface BridgeTokenMessage {
|
|
901
|
+
type: 'sp00ky:token';
|
|
902
|
+
token: string;
|
|
903
|
+
userId: string;
|
|
904
|
+
/** The SurrealDB endpoint (`wss://host/rpc`), not the push endpoint. */
|
|
905
|
+
endpoint: string;
|
|
906
|
+
namespace: string;
|
|
907
|
+
database: string;
|
|
908
|
+
/** VAPID public key (base64url) the page subscribed with, for `pushsubscriptionchange`. */
|
|
909
|
+
publicKey?: string;
|
|
910
|
+
/** This browser's push endpoint, when subscribed. */
|
|
911
|
+
pushEndpoint?: string;
|
|
912
|
+
}
|
|
913
|
+
/** Page -> service worker: forget the session and close every notification. */
|
|
914
|
+
interface BridgeSignOutMessage {
|
|
915
|
+
type: 'sp00ky:signout';
|
|
916
|
+
}
|
|
917
|
+
/** Service worker -> page messages. */
|
|
918
|
+
type WebPushClientMessage = /** A push arrived while this page was visible, so nothing was shown. */
|
|
919
|
+
{
|
|
920
|
+
type: 'sp00ky:push';
|
|
921
|
+
payload: PushPayload;
|
|
922
|
+
}
|
|
923
|
+
/** A notification was clicked and the bridge was told to route in-app. */ | {
|
|
924
|
+
type: 'sp00ky:navigate';
|
|
925
|
+
url: string;
|
|
926
|
+
payload?: PushPayload;
|
|
927
|
+
action?: string;
|
|
928
|
+
}
|
|
929
|
+
/** The browser rotated the push subscription; the page should `sync()`. */ | {
|
|
930
|
+
type: 'sp00ky:subscriptionchange';
|
|
931
|
+
endpoint?: string;
|
|
932
|
+
};
|
|
933
|
+
/** `urlBase64ToUint8Array`: the VAPID key as `applicationServerKey` wants it. */
|
|
934
|
+
|
|
935
|
+
/** A payload this build can act on (`v` known, `kind` set). */
|
|
936
|
+
declare function isPushPayload(value: unknown): value is PushPayload;
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/modules/web-push/index.d.ts
|
|
939
|
+
/**
|
|
940
|
+
* `db.webPush`: this browser's Web Push subscription, on the page.
|
|
941
|
+
*
|
|
942
|
+
* Every server call is a `fn::push::*` function over the client's remote
|
|
943
|
+
* connection (see `apps/cli/src/push_tables.surql`). Nothing here touches a
|
|
944
|
+
* browser API at import time, and every method answers "unsupported" instead
|
|
945
|
+
* of throwing where there is no window, service worker or push manager
|
|
946
|
+
* (SSR, Node, workers, iOS Safari outside a home-screen app).
|
|
947
|
+
*/
|
|
948
|
+
interface WebPushConfig {
|
|
949
|
+
/**
|
|
950
|
+
* Registered with `navigator.serviceWorker.register(url)` when the page has
|
|
951
|
+
* no service worker yet. Leave unset when the app (or vite-plugin-pwa)
|
|
952
|
+
* registers its own.
|
|
953
|
+
*/
|
|
954
|
+
serviceWorkerUrl?: string;
|
|
955
|
+
/**
|
|
956
|
+
* Reconcile this browser's subscription after sign-in (see `sync()`): a
|
|
957
|
+
* rotated key, a browser that dropped the subscription or a server row that
|
|
958
|
+
* went away are repaired without a prompt. Default `true`.
|
|
959
|
+
*/
|
|
960
|
+
autoSync?: boolean;
|
|
961
|
+
/**
|
|
962
|
+
* Let `sync()` subscribe a signed-in user whenever notification permission
|
|
963
|
+
* is already granted, even if they never subscribed on this browser.
|
|
964
|
+
* Default `false`: only a user who called `subscribe()` here is restored.
|
|
965
|
+
*/
|
|
966
|
+
autoResubscribe?: boolean;
|
|
967
|
+
/**
|
|
968
|
+
* Remove this device's subscription for the user on `signOut()` (before the
|
|
969
|
+
* token is dropped, bounded by a short timeout). Default `true`.
|
|
970
|
+
*/
|
|
971
|
+
unsubscribeOnSignOut?: boolean;
|
|
972
|
+
/**
|
|
973
|
+
* Post the session token to the app's service worker whenever it changes,
|
|
974
|
+
* while notifications are allowed, so `@spooky-sync/core/sw` can render from
|
|
975
|
+
* live data. Never posts an impersonation token. Default `true`.
|
|
976
|
+
*/
|
|
977
|
+
bridge?: boolean;
|
|
978
|
+
}
|
|
979
|
+
type WebPushUnsupportedReason = 'no-window' | 'insecure-context' | 'no-service-worker' | 'no-push-manager' | 'no-notification' | 'ios-not-installed';
|
|
980
|
+
interface WebPushSupport {
|
|
981
|
+
supported: boolean;
|
|
982
|
+
permission: NotificationPermission | 'unsupported';
|
|
983
|
+
reason?: WebPushUnsupportedReason;
|
|
984
|
+
}
|
|
985
|
+
interface WebPushInfo {
|
|
986
|
+
/** A host with push on has published its key. */
|
|
987
|
+
enabled: boolean;
|
|
988
|
+
/** VAPID public key (base64url), the browser's `applicationServerKey`. */
|
|
989
|
+
publicKey?: string;
|
|
990
|
+
/** Id of that key. */
|
|
991
|
+
kid?: string;
|
|
992
|
+
}
|
|
993
|
+
interface WebPushDevice {
|
|
994
|
+
/** `_00_push_subscription:<id>` */
|
|
995
|
+
id: string;
|
|
996
|
+
endpoint: string;
|
|
997
|
+
/** Key id the device subscribed under. */
|
|
998
|
+
kid: string;
|
|
999
|
+
label?: string;
|
|
1000
|
+
userAgent?: string;
|
|
1001
|
+
/** Rule names this device receives; undefined = every rule. */
|
|
1002
|
+
rules?: string[];
|
|
1003
|
+
meta?: Record<string, unknown>;
|
|
1004
|
+
createdAt?: Date;
|
|
1005
|
+
updatedAt?: Date;
|
|
1006
|
+
lastOkAt?: Date;
|
|
1007
|
+
lastError?: string;
|
|
1008
|
+
failures: number;
|
|
1009
|
+
disabledAt?: Date;
|
|
1010
|
+
disabledReason?: string;
|
|
1011
|
+
/** Subscribed under the current key (pushes reach it). Undefined when unknown. */
|
|
1012
|
+
current?: boolean;
|
|
1013
|
+
/** This browser. */
|
|
1014
|
+
thisDevice: boolean;
|
|
1015
|
+
}
|
|
1016
|
+
interface WebPushSubscribeOptions {
|
|
1017
|
+
/** Defaults to the page's service worker registration (`serviceWorker.ready`). */
|
|
1018
|
+
registration?: ServiceWorkerRegistration;
|
|
1019
|
+
/** Shown in the device list ("Work laptop"). */
|
|
1020
|
+
label?: string;
|
|
1021
|
+
/** Rule names this device wants. Omitted or empty: every rule. */
|
|
1022
|
+
rules?: string[];
|
|
1023
|
+
/** Free-form app data about the device. */
|
|
1024
|
+
meta?: Record<string, unknown>;
|
|
1025
|
+
/** Ask for notification permission when it is still `default`. Default `true`. Call from a user gesture. */
|
|
1026
|
+
requestPermission?: boolean;
|
|
1027
|
+
}
|
|
1028
|
+
interface WebPushUnsubscribeOptions {
|
|
1029
|
+
/** Remove every device of the user, not only this one. */
|
|
1030
|
+
all?: boolean;
|
|
1031
|
+
/** Keep the browser's PushSubscription (only the server row goes). */
|
|
1032
|
+
keepBrowserSubscription?: boolean;
|
|
1033
|
+
}
|
|
1034
|
+
interface WebPushSyncOptions {
|
|
1035
|
+
registration?: ServiceWorkerRegistration;
|
|
1036
|
+
/** Overrides `webPush.autoResubscribe` for this call. */
|
|
1037
|
+
autoResubscribe?: boolean;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* What `sync()` did. `ok`: nothing to repair. `resubscribed`: the browser
|
|
1041
|
+
* subscription was (re)created. `registered`: the server row was re-created.
|
|
1042
|
+
* The rest say why nothing happened.
|
|
1043
|
+
*/
|
|
1044
|
+
type WebPushSyncStatus = 'ok' | 'resubscribed' | 'registered' | 'unsupported' | 'signed-out' | 'impersonating' | 'permission-default' | 'permission-denied' | 'not-subscribed' | 'disabled' | 'no-registration' | 'error';
|
|
1045
|
+
interface WebPushUpdateOptions {
|
|
1046
|
+
label?: string;
|
|
1047
|
+
/** Rule names; `null` or `[]` resets the device to every rule. */
|
|
1048
|
+
rules?: string[] | null;
|
|
1049
|
+
meta?: Record<string, unknown>;
|
|
1050
|
+
}
|
|
1051
|
+
/** A push to yourself (`fn::push::notify`). */
|
|
1052
|
+
interface WebPushMessageInput {
|
|
1053
|
+
/** Omit for a content-free nudge. */
|
|
1054
|
+
notification?: PushNotification;
|
|
1055
|
+
data?: Record<string, unknown>;
|
|
1056
|
+
topic?: string;
|
|
1057
|
+
urgency?: PushUrgency;
|
|
1058
|
+
/** Push-service TTL, seconds. */
|
|
1059
|
+
ttl?: number;
|
|
1060
|
+
/** In the future: scheduled. */
|
|
1061
|
+
sendAt?: Date | string | number;
|
|
1062
|
+
}
|
|
1063
|
+
interface WebPushMessage {
|
|
1064
|
+
/** `_00_push_message:<id>` */
|
|
1065
|
+
id: string;
|
|
1066
|
+
status: string;
|
|
1067
|
+
sendAt?: Date;
|
|
1068
|
+
createdAt?: Date;
|
|
1069
|
+
}
|
|
1070
|
+
type WebPushErrorCode = 'unsupported' | 'signed-out' | 'impersonating' | 'disabled' | 'permission-denied' | 'permission-default' | 'no-registration' | 'not-subscribed' | 'subscribe-failed' | 'server';
|
|
1071
|
+
declare class WebPushError extends Error {
|
|
1072
|
+
readonly code: WebPushErrorCode;
|
|
1073
|
+
readonly cause?: unknown | undefined;
|
|
1074
|
+
constructor(code: WebPushErrorCode, message: string, cause?: unknown | undefined);
|
|
1075
|
+
}
|
|
1076
|
+
interface WebPushAuthPort {
|
|
1077
|
+
readonly token: string | null;
|
|
1078
|
+
readonly currentUser: {
|
|
1079
|
+
id?: unknown;
|
|
1080
|
+
} | null;
|
|
1081
|
+
readonly isAuthenticated: boolean;
|
|
1082
|
+
readonly impersonation: unknown;
|
|
1083
|
+
subscribe(cb: (userId: unknown) => void): () => void;
|
|
1084
|
+
onBeforeSignOut(hook: (ctx: SignOutContext) => Promise<void> | void): () => void;
|
|
1085
|
+
}
|
|
1086
|
+
interface WebPushRemotePort {
|
|
1087
|
+
query<T extends unknown[]>(sql: string, vars?: Record<string, unknown>): Promise<T>;
|
|
1088
|
+
}
|
|
1089
|
+
/** Browser globals, read at call time. Injectable for tests. */
|
|
1090
|
+
interface WebPushGlobals {
|
|
1091
|
+
window?: unknown;
|
|
1092
|
+
navigator?: Navigator & {
|
|
1093
|
+
standalone?: boolean;
|
|
1094
|
+
};
|
|
1095
|
+
Notification?: {
|
|
1096
|
+
permission: NotificationPermission;
|
|
1097
|
+
requestPermission(cb?: (p: NotificationPermission) => void): Promise<NotificationPermission> | void;
|
|
1098
|
+
};
|
|
1099
|
+
isSecureContext?: boolean;
|
|
1100
|
+
matchMedia?: (query: string) => {
|
|
1101
|
+
matches: boolean;
|
|
1102
|
+
};
|
|
1103
|
+
PushManager?: unknown;
|
|
1104
|
+
}
|
|
1105
|
+
interface WebPushModuleDeps {
|
|
1106
|
+
remote: WebPushRemotePort;
|
|
1107
|
+
auth: WebPushAuthPort;
|
|
1108
|
+
persistence: PersistenceClient;
|
|
1109
|
+
logger: Logger$1;
|
|
1110
|
+
database: {
|
|
1111
|
+
endpoint?: string;
|
|
1112
|
+
namespace: string;
|
|
1113
|
+
database: string;
|
|
1114
|
+
};
|
|
1115
|
+
/** Transport state; auto-sync waits for `connected`. */
|
|
1116
|
+
connection?: {
|
|
1117
|
+
subscribe(cb: (state: string) => void): () => void;
|
|
1118
|
+
};
|
|
1119
|
+
config?: WebPushConfig;
|
|
1120
|
+
globals?: () => WebPushGlobals;
|
|
1121
|
+
}
|
|
1122
|
+
declare const PUSH_ENDPOINT_KEY = "sp00ky_push_endpoint";
|
|
1123
|
+
/** `table:id` of a record id value, without the SDK's escaping. */
|
|
1124
|
+
|
|
1125
|
+
/** Where Web Push can work, from the given globals. */
|
|
1126
|
+
declare function detectWebPushSupport(g: WebPushGlobals): WebPushSupport;
|
|
1127
|
+
declare class WebPushModule {
|
|
1128
|
+
private readonly deps;
|
|
1129
|
+
private readonly logger;
|
|
1130
|
+
private readonly config;
|
|
1131
|
+
private readonly globals;
|
|
1132
|
+
private infoCache;
|
|
1133
|
+
private lastInfo;
|
|
1134
|
+
private readonly bridges;
|
|
1135
|
+
private readonly disposers;
|
|
1136
|
+
private syncedFor;
|
|
1137
|
+
private syncTimer;
|
|
1138
|
+
private attached;
|
|
1139
|
+
constructor(deps: WebPushModuleDeps);
|
|
1140
|
+
/**
|
|
1141
|
+
* Wire sign-out, auto-sync and the token bridge. Called by the client once;
|
|
1142
|
+
* a no-op beyond the sign-out hook where push is unsupported.
|
|
1143
|
+
*/
|
|
1144
|
+
attach(): () => void;
|
|
1145
|
+
dispose(): void;
|
|
1146
|
+
support(): WebPushSupport;
|
|
1147
|
+
/** Is push on for this deployment, and with which key. Cached; `refresh` re-reads. */
|
|
1148
|
+
info(options?: {
|
|
1149
|
+
refresh?: boolean;
|
|
1150
|
+
}): Promise<WebPushInfo>;
|
|
1151
|
+
/** This browser holds a push subscription registered for the signed-in user. No server round trip. */
|
|
1152
|
+
isSubscribed(): Promise<boolean>;
|
|
1153
|
+
/** The user's devices, this one marked. */
|
|
1154
|
+
devices(): Promise<WebPushDevice[]>;
|
|
1155
|
+
/**
|
|
1156
|
+
* Ask for permission (when still `default`), subscribe this browser with the
|
|
1157
|
+
* deployment's key and register it for the signed-in user. Call it from a
|
|
1158
|
+
* click handler: permission prompts need a user gesture.
|
|
1159
|
+
*/
|
|
1160
|
+
subscribe(options?: WebPushSubscribeOptions): Promise<WebPushDevice>;
|
|
1161
|
+
/** Remove this device (or every device, `all`) for the signed-in user. Returns how many rows went. */
|
|
1162
|
+
unsubscribe(options?: WebPushUnsubscribeOptions): Promise<number>;
|
|
1163
|
+
/**
|
|
1164
|
+
* Reconcile after sign-in (runs by itself with `autoSync`). Needs granted
|
|
1165
|
+
* permission and a user who subscribed on this browser (or
|
|
1166
|
+
* `autoResubscribe`). Repairs a missing browser subscription, one made with
|
|
1167
|
+
* a rotated key, and a missing, disabled or stale server row. Never throws.
|
|
1168
|
+
*/
|
|
1169
|
+
sync(options?: WebPushSyncOptions): Promise<WebPushSyncStatus>;
|
|
1170
|
+
/** Change this device's label, rule filter or meta. */
|
|
1171
|
+
update(options: WebPushUpdateOptions): Promise<WebPushDevice | null>;
|
|
1172
|
+
/** Push to yourself, now or at `sendAt` (reminders). Omit `notification` for a nudge. */
|
|
1173
|
+
notify(message: WebPushMessageInput): Promise<WebPushMessage>;
|
|
1174
|
+
/** Cancel a scheduled message. `true` when it was still pending. */
|
|
1175
|
+
cancel(id: string | RecordId$1): Promise<boolean>;
|
|
1176
|
+
/** A visible test notification to every device of the user. */
|
|
1177
|
+
test(options?: {
|
|
1178
|
+
title?: string;
|
|
1179
|
+
body?: string;
|
|
1180
|
+
}): Promise<WebPushMessage>;
|
|
1181
|
+
/**
|
|
1182
|
+
* Post the session to the service worker now and whenever the token
|
|
1183
|
+
* changes (`{ type: 'sp00ky:token', token, userId, endpoint, namespace,
|
|
1184
|
+
* database }`). Never posts an impersonation token. Returns a disposer.
|
|
1185
|
+
* With `webPush.bridge` (the default) this already runs while notification
|
|
1186
|
+
* permission is granted.
|
|
1187
|
+
*/
|
|
1188
|
+
bridge(registration?: ServiceWorkerRegistration): () => void;
|
|
1189
|
+
/** Messages from the push service worker (`sp00ky:push` while visible, `sp00ky:navigate`, ...). */
|
|
1190
|
+
onMessage(cb: (message: WebPushClientMessage) => void): () => void;
|
|
1191
|
+
private userId;
|
|
1192
|
+
private assertSignedIn;
|
|
1193
|
+
private assertNotImpersonating;
|
|
1194
|
+
private call;
|
|
1195
|
+
private listRaw;
|
|
1196
|
+
private register;
|
|
1197
|
+
private readStored;
|
|
1198
|
+
private writeStored;
|
|
1199
|
+
/** The page's registration: given, existing, registered from config, or `ready` within `waitMs`. */
|
|
1200
|
+
private registration;
|
|
1201
|
+
/** This browser's subscription, without waiting for a worker. */
|
|
1202
|
+
private currentSubscription;
|
|
1203
|
+
private thisEndpoint;
|
|
1204
|
+
private postToWorker;
|
|
1205
|
+
private startBridge;
|
|
1206
|
+
private refreshBridges;
|
|
1207
|
+
private tokenMessage;
|
|
1208
|
+
/** Sign-out hook: tell the worker, and drop this device's row for the user. Bounded. */
|
|
1209
|
+
private beforeSignOut;
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
483
1212
|
//#region src/types.d.ts
|
|
484
1213
|
/**
|
|
485
1214
|
* A pino browser transmit object for forwarding logs to an external sink (e.g. OpenTelemetry).
|
|
@@ -833,6 +1562,16 @@ interface Sp00kyConfig<S extends SchemaStructure> {
|
|
|
833
1562
|
* Defaults to 30000; `0` disables the timeout.
|
|
834
1563
|
*/
|
|
835
1564
|
downTimeoutMs?: number;
|
|
1565
|
+
/**
|
|
1566
|
+
* Web Push on this page (`client.webPush`, `db.webPush`). Everything is
|
|
1567
|
+
* optional and the defaults need no service worker configuration here:
|
|
1568
|
+
* `autoSync` (repair this browser's subscription after sign-in),
|
|
1569
|
+
* `unsubscribeOnSignOut` (drop this device's subscription on `signOut()`),
|
|
1570
|
+
* `bridge` (post the session to the service worker for live rendering) are
|
|
1571
|
+
* on; `serviceWorkerUrl` registers a worker when the app does not.
|
|
1572
|
+
* See the `push-notifications` docs page.
|
|
1573
|
+
*/
|
|
1574
|
+
webPush?: WebPushConfig;
|
|
836
1575
|
}
|
|
837
1576
|
/** Tunables for sync-health reporting. See {@link Sp00kyConfig.syncHealth}. */
|
|
838
1577
|
interface SyncHealthConfig {
|
|
@@ -1249,4 +1988,4 @@ interface DebounceOptions {
|
|
|
1249
1988
|
delay?: number;
|
|
1250
1989
|
}
|
|
1251
1990
|
//#endregion
|
|
1252
|
-
export {
|
|
1991
|
+
export { WebPushSupport as $, RunOptions as A, SealedQuery as At, SyncHealthStatus as B, decodeTokenClaims as Bt, QueryTimeToLive as C, AuthEventSystem as Ct, RecordVersionArray as D, RemoteDatabaseService as Dt, ReconnectConfig as E, createAuthEventSystem as Et, StorageHealth as F, ImpersonationTimerHost as Ft, WebPushDevice as G, UpdateOptions as H, renewDelayMs as Ht, StorageHealthStatus as I, ImpersonationTimers as It, WebPushInfo as J, WebPushError as K, StoreType as L, ORIGIN_TOKEN_KEY as Lt, Sp00kyConfig as M, EventSystem as Mt, Sp00kyQueryResult as N, IMPERSONATION_ACCESS as Nt, RecordVersionDiff as O, Logger$1 as Ot, Sp00kyQueryResultPromise as P, ImpersonationInfo as Pt, WebPushSubscribeOptions as Q, SyncHealth as R, SESSION_POLL_MS as Rt, QueryStatusCallback as S, SignOutHook as St, QueryUpdateCallback as T, AuthEventTypes as Tt, PUSH_ENDPOINT_KEY as U, sameImpersonation as Ut, TimingPhase as V, impersonationFromToken as Vt, WebPushConfig as W, WebPushMessageInput as X, WebPushMessage as Y, WebPushModule as Z, QueryConfig as _, ActiveImpersonation as _t, Level$1 as a, detectWebPushSupport as at, QueryState as b, SIGN_OUT_HOOK_TIMEOUT_MS as bt, MutationCallback as c, PUSH_PAYLOAD_VERSION as ct, PersistenceClient as d, PushOp as dt, WebPushSyncOptions as et, PhaseStat as f, PushPayload as ft, QueryAuthorityCallback as g, isPushPayload as gt, PreloadRefresh as h, WebPushClientMessage as ht, InlineRow as i, WebPushUpdateOptions as it, ServerViewMeta as j, EventDefinition as jt, RegistrationTimings as k, LocalStore as kt, MutationEvent as l, PushNotification as lt, PreloadOptions as m, PushUrgency as mt, DebounceOptions as n, WebPushUnsubscribeOptions as nt, MATERIALIZATION_SAMPLE_WINDOW as o, BridgeSignOutMessage as ot, PinoTransmit as p, PushPayloadKind as pt, WebPushErrorCode as q, EventSubscriptionOptions as r, WebPushUnsupportedReason as rt, MembershipOutcome as s, BridgeTokenMessage as st, ConnectionState as t, WebPushSyncStatus as tt, MutationEventType as u, PushNotificationAction as ut, QueryConfigRecord as v, AuthService as vt, QueryTimings as w, AuthEventTypeMap as wt, QueryStatus as x, SignOutContext as xt, QueryHash as y, ImpersonationCandidate as yt, SyncHealthConfig as z, TokenClaims as zt };
|