@spooky-sync/core 0.0.1-canary.280 → 0.0.1-canary.282
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 +754 -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 +758 -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 +33 -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 +824 -0
- package/src/modules/web-push/index.ts +1027 -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,750 @@ 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
|
+
* Every kind of device the project reaches: `web` once a VAPID key is
|
|
994
|
+
* published, `apns` / `fcm` when `push.apns` / `push.fcm` are configured.
|
|
995
|
+
*/
|
|
996
|
+
providers?: PushDeviceKind[];
|
|
997
|
+
}
|
|
998
|
+
/** How a device is reached: a browser (Web Push), APNs or FCM. */
|
|
999
|
+
type PushDeviceKind = 'web' | 'apns' | 'fcm';
|
|
1000
|
+
interface WebPushDevice {
|
|
1001
|
+
/** `_00_push_subscription:<id>` */
|
|
1002
|
+
id: string;
|
|
1003
|
+
/** Web: the push service URL. Native: `<kind>:<token>`. */
|
|
1004
|
+
endpoint: string;
|
|
1005
|
+
/** Rows the Flutter / native side registered are `apns` or `fcm`. */
|
|
1006
|
+
kind: PushDeviceKind;
|
|
1007
|
+
/** `web`, `ios`, `android` or `macos`. */
|
|
1008
|
+
platform?: string;
|
|
1009
|
+
/** Native: bundle id / package name. */
|
|
1010
|
+
appId?: string;
|
|
1011
|
+
/** APNs: `sandbox` or `production`. */
|
|
1012
|
+
environment?: string;
|
|
1013
|
+
/** Key id a web device subscribed under (empty for native ones). */
|
|
1014
|
+
kid: string;
|
|
1015
|
+
label?: string;
|
|
1016
|
+
userAgent?: string;
|
|
1017
|
+
/** Rule names this device receives; undefined = every rule. */
|
|
1018
|
+
rules?: string[];
|
|
1019
|
+
meta?: Record<string, unknown>;
|
|
1020
|
+
createdAt?: Date;
|
|
1021
|
+
updatedAt?: Date;
|
|
1022
|
+
lastOkAt?: Date;
|
|
1023
|
+
lastError?: string;
|
|
1024
|
+
failures: number;
|
|
1025
|
+
disabledAt?: Date;
|
|
1026
|
+
disabledReason?: string;
|
|
1027
|
+
/** Subscribed under the current key (pushes reach it). Undefined when unknown. */
|
|
1028
|
+
current?: boolean;
|
|
1029
|
+
/** This browser. */
|
|
1030
|
+
thisDevice: boolean;
|
|
1031
|
+
}
|
|
1032
|
+
interface WebPushSubscribeOptions {
|
|
1033
|
+
/** Defaults to the page's service worker registration (`serviceWorker.ready`). */
|
|
1034
|
+
registration?: ServiceWorkerRegistration;
|
|
1035
|
+
/** Shown in the device list ("Work laptop"). */
|
|
1036
|
+
label?: string;
|
|
1037
|
+
/** Rule names this device wants. Omitted or empty: every rule. */
|
|
1038
|
+
rules?: string[];
|
|
1039
|
+
/** Free-form app data about the device. */
|
|
1040
|
+
meta?: Record<string, unknown>;
|
|
1041
|
+
/** Ask for notification permission when it is still `default`. Default `true`. Call from a user gesture. */
|
|
1042
|
+
requestPermission?: boolean;
|
|
1043
|
+
}
|
|
1044
|
+
interface WebPushUnsubscribeOptions {
|
|
1045
|
+
/** Remove every device of the user, not only this one. */
|
|
1046
|
+
all?: boolean;
|
|
1047
|
+
/** Keep the browser's PushSubscription (only the server row goes). */
|
|
1048
|
+
keepBrowserSubscription?: boolean;
|
|
1049
|
+
}
|
|
1050
|
+
interface WebPushSyncOptions {
|
|
1051
|
+
registration?: ServiceWorkerRegistration;
|
|
1052
|
+
/** Overrides `webPush.autoResubscribe` for this call. */
|
|
1053
|
+
autoResubscribe?: boolean;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* What `sync()` did. `ok`: nothing to repair. `resubscribed`: the browser
|
|
1057
|
+
* subscription was (re)created. `registered`: the server row was re-created.
|
|
1058
|
+
* The rest say why nothing happened.
|
|
1059
|
+
*/
|
|
1060
|
+
type WebPushSyncStatus = 'ok' | 'resubscribed' | 'registered' | 'unsupported' | 'signed-out' | 'impersonating' | 'permission-default' | 'permission-denied' | 'not-subscribed' | 'disabled' | 'no-registration' | 'error';
|
|
1061
|
+
interface WebPushUpdateOptions {
|
|
1062
|
+
label?: string;
|
|
1063
|
+
/** Rule names; `null` or `[]` resets the device to every rule. */
|
|
1064
|
+
rules?: string[] | null;
|
|
1065
|
+
meta?: Record<string, unknown>;
|
|
1066
|
+
}
|
|
1067
|
+
/** A push to yourself (`fn::push::notify`). */
|
|
1068
|
+
interface WebPushMessageInput {
|
|
1069
|
+
/** Omit for a content-free nudge. */
|
|
1070
|
+
notification?: PushNotification;
|
|
1071
|
+
data?: Record<string, unknown>;
|
|
1072
|
+
topic?: string;
|
|
1073
|
+
urgency?: PushUrgency;
|
|
1074
|
+
/** Push-service TTL, seconds. */
|
|
1075
|
+
ttl?: number;
|
|
1076
|
+
/** In the future: scheduled. */
|
|
1077
|
+
sendAt?: Date | string | number;
|
|
1078
|
+
}
|
|
1079
|
+
interface WebPushMessage {
|
|
1080
|
+
/** `_00_push_message:<id>` */
|
|
1081
|
+
id: string;
|
|
1082
|
+
status: string;
|
|
1083
|
+
sendAt?: Date;
|
|
1084
|
+
createdAt?: Date;
|
|
1085
|
+
}
|
|
1086
|
+
type WebPushErrorCode = 'unsupported' | 'signed-out' | 'impersonating' | 'disabled' | 'permission-denied' | 'permission-default' | 'no-registration' | 'not-subscribed' | 'subscribe-failed' | 'server';
|
|
1087
|
+
declare class WebPushError extends Error {
|
|
1088
|
+
readonly code: WebPushErrorCode;
|
|
1089
|
+
readonly cause?: unknown | undefined;
|
|
1090
|
+
constructor(code: WebPushErrorCode, message: string, cause?: unknown | undefined);
|
|
1091
|
+
}
|
|
1092
|
+
interface WebPushAuthPort {
|
|
1093
|
+
readonly token: string | null;
|
|
1094
|
+
readonly currentUser: {
|
|
1095
|
+
id?: unknown;
|
|
1096
|
+
} | null;
|
|
1097
|
+
readonly isAuthenticated: boolean;
|
|
1098
|
+
readonly impersonation: unknown;
|
|
1099
|
+
subscribe(cb: (userId: unknown) => void): () => void;
|
|
1100
|
+
onBeforeSignOut(hook: (ctx: SignOutContext) => Promise<void> | void): () => void;
|
|
1101
|
+
}
|
|
1102
|
+
interface WebPushRemotePort {
|
|
1103
|
+
query<T extends unknown[]>(sql: string, vars?: Record<string, unknown>): Promise<T>;
|
|
1104
|
+
}
|
|
1105
|
+
/** Browser globals, read at call time. Injectable for tests. */
|
|
1106
|
+
interface WebPushGlobals {
|
|
1107
|
+
window?: unknown;
|
|
1108
|
+
navigator?: Navigator & {
|
|
1109
|
+
standalone?: boolean;
|
|
1110
|
+
};
|
|
1111
|
+
Notification?: {
|
|
1112
|
+
permission: NotificationPermission;
|
|
1113
|
+
requestPermission(cb?: (p: NotificationPermission) => void): Promise<NotificationPermission> | void;
|
|
1114
|
+
};
|
|
1115
|
+
isSecureContext?: boolean;
|
|
1116
|
+
matchMedia?: (query: string) => {
|
|
1117
|
+
matches: boolean;
|
|
1118
|
+
};
|
|
1119
|
+
PushManager?: unknown;
|
|
1120
|
+
}
|
|
1121
|
+
interface WebPushModuleDeps {
|
|
1122
|
+
remote: WebPushRemotePort;
|
|
1123
|
+
auth: WebPushAuthPort;
|
|
1124
|
+
persistence: PersistenceClient;
|
|
1125
|
+
logger: Logger$1;
|
|
1126
|
+
database: {
|
|
1127
|
+
endpoint?: string;
|
|
1128
|
+
namespace: string;
|
|
1129
|
+
database: string;
|
|
1130
|
+
};
|
|
1131
|
+
/** Transport state; auto-sync waits for `connected`. */
|
|
1132
|
+
connection?: {
|
|
1133
|
+
subscribe(cb: (state: string) => void): () => void;
|
|
1134
|
+
};
|
|
1135
|
+
config?: WebPushConfig;
|
|
1136
|
+
globals?: () => WebPushGlobals;
|
|
1137
|
+
}
|
|
1138
|
+
declare const PUSH_ENDPOINT_KEY = "sp00ky_push_endpoint";
|
|
1139
|
+
/** `table:id` of a record id value, without the SDK's escaping. */
|
|
1140
|
+
|
|
1141
|
+
/** Where Web Push can work, from the given globals. */
|
|
1142
|
+
declare function detectWebPushSupport(g: WebPushGlobals): WebPushSupport;
|
|
1143
|
+
declare class WebPushModule {
|
|
1144
|
+
private readonly deps;
|
|
1145
|
+
private readonly logger;
|
|
1146
|
+
private readonly config;
|
|
1147
|
+
private readonly globals;
|
|
1148
|
+
private infoCache;
|
|
1149
|
+
private lastInfo;
|
|
1150
|
+
private readonly bridges;
|
|
1151
|
+
private readonly disposers;
|
|
1152
|
+
private syncedFor;
|
|
1153
|
+
private syncTimer;
|
|
1154
|
+
private attached;
|
|
1155
|
+
constructor(deps: WebPushModuleDeps);
|
|
1156
|
+
/**
|
|
1157
|
+
* Wire sign-out, auto-sync and the token bridge. Called by the client once;
|
|
1158
|
+
* a no-op beyond the sign-out hook where push is unsupported.
|
|
1159
|
+
*/
|
|
1160
|
+
attach(): () => void;
|
|
1161
|
+
dispose(): void;
|
|
1162
|
+
support(): WebPushSupport;
|
|
1163
|
+
/** Is push on for this deployment, and with which key. Cached; `refresh` re-reads. */
|
|
1164
|
+
info(options?: {
|
|
1165
|
+
refresh?: boolean;
|
|
1166
|
+
}): Promise<WebPushInfo>;
|
|
1167
|
+
/** This browser holds a push subscription registered for the signed-in user. No server round trip. */
|
|
1168
|
+
isSubscribed(): Promise<boolean>;
|
|
1169
|
+
/** The user's devices, this one marked. */
|
|
1170
|
+
devices(): Promise<WebPushDevice[]>;
|
|
1171
|
+
/**
|
|
1172
|
+
* Ask for permission (when still `default`), subscribe this browser with the
|
|
1173
|
+
* deployment's key and register it for the signed-in user. Call it from a
|
|
1174
|
+
* click handler: permission prompts need a user gesture.
|
|
1175
|
+
*/
|
|
1176
|
+
subscribe(options?: WebPushSubscribeOptions): Promise<WebPushDevice>;
|
|
1177
|
+
/** Remove this device (or every device, `all`) for the signed-in user. Returns how many rows went. */
|
|
1178
|
+
unsubscribe(options?: WebPushUnsubscribeOptions): Promise<number>;
|
|
1179
|
+
/**
|
|
1180
|
+
* Reconcile after sign-in (runs by itself with `autoSync`). Needs granted
|
|
1181
|
+
* permission and a user who subscribed on this browser (or
|
|
1182
|
+
* `autoResubscribe`). Repairs a missing browser subscription, one made with
|
|
1183
|
+
* a rotated key, and a missing, disabled or stale server row. Never throws.
|
|
1184
|
+
*/
|
|
1185
|
+
sync(options?: WebPushSyncOptions): Promise<WebPushSyncStatus>;
|
|
1186
|
+
/** Change this device's label, rule filter or meta. */
|
|
1187
|
+
update(options: WebPushUpdateOptions): Promise<WebPushDevice | null>;
|
|
1188
|
+
/** Push to yourself, now or at `sendAt` (reminders). Omit `notification` for a nudge. */
|
|
1189
|
+
notify(message: WebPushMessageInput): Promise<WebPushMessage>;
|
|
1190
|
+
/** Cancel a scheduled message. `true` when it was still pending. */
|
|
1191
|
+
cancel(id: string | RecordId$1): Promise<boolean>;
|
|
1192
|
+
/** A visible test notification to every device of the user. */
|
|
1193
|
+
test(options?: {
|
|
1194
|
+
title?: string;
|
|
1195
|
+
body?: string;
|
|
1196
|
+
}): Promise<WebPushMessage>;
|
|
1197
|
+
/**
|
|
1198
|
+
* Post the session to the service worker now and whenever the token
|
|
1199
|
+
* changes (`{ type: 'sp00ky:token', token, userId, endpoint, namespace,
|
|
1200
|
+
* database }`). Never posts an impersonation token. Returns a disposer.
|
|
1201
|
+
* With `webPush.bridge` (the default) this already runs while notification
|
|
1202
|
+
* permission is granted.
|
|
1203
|
+
*/
|
|
1204
|
+
bridge(registration?: ServiceWorkerRegistration): () => void;
|
|
1205
|
+
/** Messages from the push service worker (`sp00ky:push` while visible, `sp00ky:navigate`, ...). */
|
|
1206
|
+
onMessage(cb: (message: WebPushClientMessage) => void): () => void;
|
|
1207
|
+
private userId;
|
|
1208
|
+
private assertSignedIn;
|
|
1209
|
+
private assertNotImpersonating;
|
|
1210
|
+
private call;
|
|
1211
|
+
private listRaw;
|
|
1212
|
+
private register;
|
|
1213
|
+
private readStored;
|
|
1214
|
+
private writeStored;
|
|
1215
|
+
/** The page's registration: given, existing, registered from config, or `ready` within `waitMs`. */
|
|
1216
|
+
private registration;
|
|
1217
|
+
/** This browser's subscription, without waiting for a worker. */
|
|
1218
|
+
private currentSubscription;
|
|
1219
|
+
private thisEndpoint;
|
|
1220
|
+
private postToWorker;
|
|
1221
|
+
private startBridge;
|
|
1222
|
+
private refreshBridges;
|
|
1223
|
+
private tokenMessage;
|
|
1224
|
+
/** Sign-out hook: tell the worker, and drop this device's row for the user. Bounded. */
|
|
1225
|
+
private beforeSignOut;
|
|
1226
|
+
}
|
|
1227
|
+
//#endregion
|
|
483
1228
|
//#region src/types.d.ts
|
|
484
1229
|
/**
|
|
485
1230
|
* A pino browser transmit object for forwarding logs to an external sink (e.g. OpenTelemetry).
|
|
@@ -833,6 +1578,16 @@ interface Sp00kyConfig<S extends SchemaStructure> {
|
|
|
833
1578
|
* Defaults to 30000; `0` disables the timeout.
|
|
834
1579
|
*/
|
|
835
1580
|
downTimeoutMs?: number;
|
|
1581
|
+
/**
|
|
1582
|
+
* Web Push on this page (`client.webPush`, `db.webPush`). Everything is
|
|
1583
|
+
* optional and the defaults need no service worker configuration here:
|
|
1584
|
+
* `autoSync` (repair this browser's subscription after sign-in),
|
|
1585
|
+
* `unsubscribeOnSignOut` (drop this device's subscription on `signOut()`),
|
|
1586
|
+
* `bridge` (post the session to the service worker for live rendering) are
|
|
1587
|
+
* on; `serviceWorkerUrl` registers a worker when the app does not.
|
|
1588
|
+
* See the `push-notifications` docs page.
|
|
1589
|
+
*/
|
|
1590
|
+
webPush?: WebPushConfig;
|
|
836
1591
|
}
|
|
837
1592
|
/** Tunables for sync-health reporting. See {@link Sp00kyConfig.syncHealth}. */
|
|
838
1593
|
interface SyncHealthConfig {
|
|
@@ -1249,4 +2004,4 @@ interface DebounceOptions {
|
|
|
1249
2004
|
delay?: number;
|
|
1250
2005
|
}
|
|
1251
2006
|
//#endregion
|
|
1252
|
-
export {
|
|
2007
|
+
export { WebPushSubscribeOptions as $, RunOptions as A, LocalStore as At, SyncHealthStatus as B, TokenClaims as Bt, QueryTimeToLive as C, SignOutHook as Ct, RecordVersionArray as D, createAuthEventSystem as Dt, ReconnectConfig as E, AuthEventTypes as Et, StorageHealth as F, ImpersonationInfo as Ft, WebPushConfig as G, UpdateOptions as H, impersonationFromToken as Ht, StorageHealthStatus as I, ImpersonationTimerHost as It, WebPushErrorCode as J, WebPushDevice as K, StoreType as L, ImpersonationTimers as Lt, Sp00kyConfig as M, EventDefinition as Mt, Sp00kyQueryResult as N, EventSystem as Nt, RecordVersionDiff as O, RemoteDatabaseService as Ot, Sp00kyQueryResultPromise as P, IMPERSONATION_ACCESS as Pt, WebPushModule as Q, SyncHealth as R, ORIGIN_TOKEN_KEY as Rt, QueryStatusCallback as S, SignOutContext as St, QueryUpdateCallback as T, AuthEventTypeMap as Tt, PUSH_ENDPOINT_KEY as U, renewDelayMs as Ut, TimingPhase as V, decodeTokenClaims as Vt, PushDeviceKind as W, sameImpersonation as Wt, WebPushMessage as X, WebPushInfo as Y, WebPushMessageInput as Z, QueryConfig as _, isPushPayload as _t, Level$1 as a, WebPushUpdateOptions as at, QueryState as b, ImpersonationCandidate as bt, MutationCallback as c, BridgeTokenMessage as ct, PersistenceClient as d, PushNotificationAction as dt, WebPushSupport as et, PhaseStat as f, PushOp as ft, QueryAuthorityCallback as g, WebPushClientMessage as gt, PreloadRefresh as h, PushUrgency as ht, InlineRow as i, WebPushUnsupportedReason as it, ServerViewMeta as j, SealedQuery as jt, RegistrationTimings as k, Logger$1 as kt, MutationEvent as l, PUSH_PAYLOAD_VERSION as lt, PreloadOptions as m, PushPayloadKind as mt, DebounceOptions as n, WebPushSyncStatus as nt, MATERIALIZATION_SAMPLE_WINDOW as o, detectWebPushSupport as ot, PinoTransmit as p, PushPayload as pt, WebPushError as q, EventSubscriptionOptions as r, WebPushUnsubscribeOptions as rt, MembershipOutcome as s, BridgeSignOutMessage as st, ConnectionState as t, WebPushSyncOptions as tt, MutationEventType as u, PushNotification as ut, QueryConfigRecord as v, ActiveImpersonation as vt, QueryTimings as w, AuthEventSystem as wt, QueryStatus as x, SIGN_OUT_HOOK_TIMEOUT_MS as xt, QueryHash as y, AuthService as yt, SyncHealthConfig as z, SESSION_POLL_MS as zt };
|