@spooky-sync/core 0.0.1-canary.260 → 0.0.1-canary.262

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 CHANGED
@@ -33,7 +33,7 @@ re-materialized from state, never patched in place.
33
33
 
34
34
  ## Key exports (`src/index.ts`)
35
35
 
36
- - `Sp00kyClient<S>` — main class. Methods: `init()`, `create(id, payload)`, `update(table, id, payload, options?)`, `delete(table, id)`, `query(table, opts?)`, `preload(query, { signal? })`, `run(backend, route, payload)`, `bucket(name)`, `remoteQuery(sql, vars?)`, `useRemote(fn)`, `authenticate(token)`, `auth.signOut()`. Traffic indicators: `pendingMutationCount` / `subscribeToPendingMutations(cb)` (outbox depth), `fetchingQueryCount` / `subscribeToFetchActivity(cb)`. Failed-writes tray: `failedMutationCount`, `subscribeToFailedMutations(cb)`, `listFailedMutations()`, `retryFailedMutation(id)`, `discardFailedMutation(id)`.
36
+ - `Sp00kyClient<S>` — main class. Methods: `init()`, `create(id, payload)`, `update(table, id, payload, options?)`, `delete(table, id)`, `query(table, opts?)`, `preload(query, { signal? })`, `run(backend, route, payload)`, `bucket(name)`, `remoteQuery(sql, vars?)`, `useRemote(fn)`, `authenticate(token)`, `auth.signOut()`. Admin impersonation (`impersonation.enabled` in sp00ky.yml, admins only, server-enforced): `auth.impersonate(targetId, reason)`, `auth.stopImpersonating()`, `auth.impersonation`, `auth.subscribeImpersonation(cb)`, `auth.searchImpersonationTargets(search)`; while active the client injects a warning bar with a Stop button (see `/docs/client/impersonation`). Traffic indicators: `pendingMutationCount` / `subscribeToPendingMutations(cb)` (outbox depth), `fetchingQueryCount` / `subscribeToFetchActivity(cb)`. Failed-writes tray: `failedMutationCount`, `subscribeToFailedMutations(cb)`, `listFailedMutations()`, `retryFailedMutation(id)`, `discardFailedMutation(id)`.
37
37
  - `FailedMutation` — a row of the `_00_failed_mutations` tray (`mutationType`, `recordId`, `data`, `beforeRecord`, `error`, `revert`).
38
38
  - `BucketHandle` — file storage handle (`put`, `get`, `delete`, `exists`), plus the cache-aware `read`, `url`, `pin`/`unpin`, `evict`, `prefetch`. `get` is always remote; `read`/`url` go through the blob cache.
39
39
  - `services/blobs/` — durable cache for bucket bytes: OPFS holds the files, `_00_blob` holds a manifest that reconcile rebuilds from disk (so a wiped local store costs metadata, not the offline cache). Nothing expires on a timer; eviction is LRU under a byte budget only, skipping pinned and on-screen entries. Config: `blobCache` in `types.ts`.
package/dist/index.d.ts CHANGED
@@ -674,17 +674,120 @@ declare class StreamProcessorService {
674
674
  private normalizeValue;
675
675
  }
676
676
  //#endregion
677
+ //#region src/modules/auth/impersonation.d.ts
678
+ /**
679
+ * Admin impersonation, client side.
680
+ *
681
+ * The server does the enforcing (see `impersonation_remote.surql` in the CLI):
682
+ * an admin calls `fn::_00_impersonate::start`, receives a token for the
683
+ * `_00_impersonate` access method, and authenticates with it, so `$auth.id` is
684
+ * the target user. Everything here is state and timers around that:
685
+ *
686
+ * - the admin's own token is kept under {@link ORIGIN_TOKEN_KEY} so Stop (and a
687
+ * reload in between) can return to it;
688
+ * - the impersonation state is derived from the token's claims, never stored
689
+ * separately, so it cannot disagree with the session actually in use;
690
+ * - the token is re-signed before it expires, and the session is polled so a
691
+ * stop or revocation made elsewhere reaches an open WebSocket quickly.
692
+ */
693
+ /** Access method impersonation tokens are issued for. */
694
+ declare const IMPERSONATION_ACCESS = "_00_impersonate";
695
+ /** Persistence key holding the admin's own token while impersonating. */
696
+ declare const ORIGIN_TOKEN_KEY = "sp00ky_auth_token_origin";
697
+ /** How often an active impersonation re-checks its session on the server. */
698
+ declare const SESSION_POLL_MS = 60000;
699
+ interface ImpersonationInfo {
700
+ /** `_00_impersonation:<id>` the session is bound to. */
701
+ session: string;
702
+ /** Record id being impersonated. */
703
+ target: string;
704
+ /** Record id of the admin. */
705
+ admin: string;
706
+ /** When the current token expires (it is renewed before then). */
707
+ tokenExpiresAt: Date | null;
708
+ }
709
+ interface TokenClaims {
710
+ access: string | null;
711
+ userId: string | null;
712
+ exp: number | null;
713
+ impersonation: {
714
+ session: string;
715
+ admin: string;
716
+ } | null;
717
+ }
718
+ /**
719
+ * Read the claims of a SurrealDB record-access JWT WITHOUT verifying it. The
720
+ * server still enforces the token on every request; this is only so the client
721
+ * can act on what it already holds before a round trip completes.
722
+ *
723
+ * Returns nulls on any malformed input.
724
+ */
725
+ declare function decodeTokenClaims(token: string): TokenClaims;
726
+ /** The impersonation a token represents, or null for a regular session. */
727
+ declare function impersonationFromToken(token: string | null): ImpersonationInfo | null;
728
+ /** Same impersonation, for change detection (renewals only move the expiry). */
729
+ declare function sameImpersonation(a: ImpersonationInfo | null, b: ImpersonationInfo | null): boolean;
730
+ /**
731
+ * When to renew a token expiring at `expSecs`: at 80% of its remaining life,
732
+ * never sooner than 5s from now, and immediately once it is inside that.
733
+ */
734
+ declare function renewDelayMs(expSecs: number, nowMs: number): number;
735
+ interface ImpersonationTimerHost {
736
+ /** Current impersonation, or null. */
737
+ current(): ImpersonationInfo | null;
738
+ /** Re-sign the token. Rejects when the session has ended. */
739
+ renew(): Promise<void>;
740
+ /** Whether the server still considers the session live. */
741
+ stillActive(): Promise<boolean>;
742
+ /** Leave the impersonation because the server ended it. */
743
+ endedRemotely(reason: string): Promise<void>;
744
+ /** Whether a failure was a reachability problem (retry) rather than an answer. */
745
+ isNetworkError(error: unknown): boolean;
746
+ }
747
+ /**
748
+ * The two timers an active impersonation needs. `sync()` is called whenever
749
+ * the auth state changes and arms or clears them to match.
750
+ */
751
+ declare class ImpersonationTimers {
752
+ private host;
753
+ private renewTimer;
754
+ private pollTimer;
755
+ private armedFor;
756
+ constructor(host: ImpersonationTimerHost);
757
+ sync(): void;
758
+ clear(): void;
759
+ private renew;
760
+ private poll;
761
+ }
762
+ //#endregion
677
763
  //#region src/modules/auth/events/index.d.ts
678
764
  declare const AuthEventTypes: {
679
765
  readonly AuthStateChanged: "AUTH_STATE_CHANGED";
766
+ readonly ImpersonationChanged: "IMPERSONATION_CHANGED";
680
767
  };
681
768
  type AuthEventTypeMap = {
682
769
  [AuthEventTypes.AuthStateChanged]: EventDefinition<typeof AuthEventTypes.AuthStateChanged, string | null>;
770
+ [AuthEventTypes.ImpersonationChanged]: EventDefinition<typeof AuthEventTypes.ImpersonationChanged, ImpersonationInfo | null>;
683
771
  };
684
772
  type AuthEventSystem = EventSystem<AuthEventTypeMap>;
685
773
  declare function createAuthEventSystem(): AuthEventSystem;
686
774
  //#endregion
687
775
  //#region src/modules/auth/index.d.ts
776
+ /** What `fn::_00_impersonate::list_users` returns per row. */
777
+ interface ImpersonationCandidate {
778
+ id: string;
779
+ is_admin: boolean;
780
+ [field: string]: unknown;
781
+ }
782
+ /** An open session, as `fn::_00_impersonate::active` lists it. */
783
+ interface ActiveImpersonation {
784
+ session: string;
785
+ target: string;
786
+ admin: string;
787
+ reason: string;
788
+ started_at: unknown;
789
+ expires_at: unknown;
790
+ }
688
791
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
689
792
  type MapColumnType<T extends ColumnSchema> = T['optional'] extends true ? TypeNameToTypeMap[T['type']] | undefined : TypeNameToTypeMap[T['type']];
690
793
  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;
@@ -705,6 +808,12 @@ declare class AuthService<S extends SchemaStructure> {
705
808
  access: string | null;
706
809
  isLoading: boolean;
707
810
  private events;
811
+ /** The impersonation last announced, for change detection. */
812
+ private announcedImpersonation;
813
+ /** Target of an impersonation that just ended, until the client purges its
814
+ * local bucket (see `consumeEndedImpersonation`). */
815
+ private endedImpersonationTarget;
816
+ private impersonationTimers;
708
817
  get eventSystem(): AuthEventSystem;
709
818
  constructor(schema: S, remote: RemoteDatabaseService, persistenceClient: PersistenceClient, logger: Logger$1);
710
819
  init(): Promise<void>;
@@ -715,6 +824,43 @@ declare class AuthService<S extends SchemaStructure> {
715
824
  */
716
825
  subscribe(cb: (userId: string | null) => void): () => void;
717
826
  private notifyListeners;
827
+ /**
828
+ * The impersonation the current session is, or null. Derived from the
829
+ * token in use, so it always describes the identity the server sees.
830
+ */
831
+ get impersonation(): ImpersonationInfo | null;
832
+ /** Subscribe to impersonation changes; called immediately with the current value. */
833
+ subscribeImpersonation(cb: (info: ImpersonationInfo | null) => void): () => void;
834
+ /**
835
+ * Act as `targetId` (a record id such as `user:abc`). Admins only, and only
836
+ * when the project enabled `impersonation` in sp00ky.yml: the server refuses
837
+ * everything else. `reason` is recorded in the audit log.
838
+ *
839
+ * The admin's session is kept aside and restored by
840
+ * {@link stopImpersonating}. Local data switches to the target's bucket
841
+ * exactly as on a sign-in, and that bucket is removed on stop.
842
+ */
843
+ impersonate(targetId: string, reason: string): Promise<ImpersonationInfo>;
844
+ /**
845
+ * Return to the admin session. Always leaves the impersonated identity,
846
+ * even when the server cannot be reached: the admin token is restored
847
+ * locally first and verified afterwards, and a rejected admin token ends in
848
+ * a full sign-out rather than staying on the target.
849
+ */
850
+ stopImpersonating(opts?: {
851
+ serverEnded?: boolean;
852
+ }): Promise<void>;
853
+ /** Re-sign the impersonation token before it expires. */
854
+ renewImpersonation(): Promise<void>;
855
+ /** Users an admin may pick from. Admin-only on the server. */
856
+ searchImpersonationTargets(search: string): Promise<ImpersonationCandidate[]>;
857
+ /** Open impersonation sessions. Admin-only on the server. */
858
+ listActiveImpersonations(): Promise<ActiveImpersonation[]>;
859
+ /**
860
+ * The target of an impersonation that ended, once. The bucket switch calls
861
+ * this after leaving a bucket so it can delete that user's local data.
862
+ */
863
+ consumeEndedImpersonation(): string | null;
718
864
  /**
719
865
  * Restore a session from the locally cached JWT, with NO network.
720
866
  *
@@ -738,7 +884,14 @@ declare class AuthService<S extends SchemaStructure> {
738
884
  */
739
885
  check(accessToken?: string): Promise<void>;
740
886
  /**
741
- * Sign out and clear session
887
+ * The server refused `token`. A refused impersonation token (stopped,
888
+ * expired, or revoked) returns to the admin session instead of signing the
889
+ * admin out; anything else signs out.
890
+ */
891
+ private rejectToken;
892
+ /**
893
+ * Sign out and clear session. While impersonating this ends the
894
+ * impersonation and signs the admin out too.
742
895
  */
743
896
  signOut(): Promise<void>;
744
897
  private setSession;
@@ -1403,6 +1556,7 @@ interface ServiceCalls {
1403
1556
  'local.beginSwitch': () => () => void;
1404
1557
  'local.currentBucketId': () => string;
1405
1558
  'local.usesSurqlSchema': () => boolean;
1559
+ 'local.dropBucket': (bucketId: string) => Promise<void>;
1406
1560
  'migrator.provision': () => Promise<void>;
1407
1561
  'blobs.start': (bucketId: string) => Promise<void>;
1408
1562
  'blobs.setNamespace': (bucketId: string) => Promise<void>;
@@ -1419,6 +1573,7 @@ interface ServiceCalls {
1419
1573
  'auth.access': () => string | null;
1420
1574
  'auth.token': () => string | null;
1421
1575
  'auth.currentUser': () => Record<string, unknown> | null;
1576
+ 'auth.consumeEndedImpersonation': () => string | null;
1422
1577
  'remote.connect': () => Promise<void>;
1423
1578
  'remote.releaseViews': (ids: unknown[]) => void;
1424
1579
  'supervisor.start': () => void;
@@ -2250,6 +2405,8 @@ declare class Sp00kyClient<S extends SchemaStructure> {
2250
2405
  private readonly runtime;
2251
2406
  private readonly featureFlags;
2252
2407
  private readonly appReleases;
2408
+ /** Teardown for subscriptions the client itself owns. */
2409
+ private readonly disposers;
2253
2410
  private readonly devTools;
2254
2411
  private hub;
2255
2412
  private forwarder;
@@ -2376,4 +2533,4 @@ declare function textToHtml(text: string): string;
2376
2533
  */
2377
2534
 
2378
2535
  //#endregion
2379
- export { AppReleaseHandle, AppReleaseModule, type AppReleaseOptions, type AppReleaseSnapshot, AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, type BlobCacheStats, type BlobEntry, type BlobKey, type BlobReadOptions, type BlobUrlLease, type BlurhashEncodeOptions, type BlurhashSetting, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, type FailedMutationRow as FailedMutation, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, InlineRow, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MembershipOutcome, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, QueryAuthorityCallback, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, ServerViewMeta, Sp00kyClient, Sp00kyClientDeps, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, UpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
2536
+ export { ActiveImpersonation, AppReleaseHandle, AppReleaseModule, type AppReleaseOptions, type AppReleaseSnapshot, AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, type BlobCacheStats, type BlobEntry, type BlobKey, type BlobReadOptions, type BlobUrlLease, type BlurhashEncodeOptions, type BlurhashSetting, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, type FailedMutationRow as FailedMutation, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, IMPERSONATION_ACCESS, ImpersonationCandidate, ImpersonationInfo, ImpersonationTimerHost, ImpersonationTimers, InlineRow, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MembershipOutcome, MutationCallback, MutationEvent, MutationEventType, ORIGIN_TOKEN_KEY, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, QueryAuthorityCallback, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, SESSION_POLL_MS, ServerViewMeta, Sp00kyClient, Sp00kyClientDeps, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, TokenClaims, UpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, decodeTokenClaims, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, impersonationFromToken, isBlurhashValid, isImagePath, renewDelayMs, sameImpersonation, semverGt, textToHtml };