@spooky-sync/core 0.0.1-canary.260 → 0.0.1-canary.261
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/dist/index.d.ts +159 -2
- package/dist/index.js +522 -27
- package/dist/types.d.ts +6 -0
- package/package.json +3 -3
- package/src/boot/bucket-switch.saga.test.ts +30 -1
- package/src/boot/bucket-switch.saga.ts +17 -1
- package/src/client/services.ts +4 -0
- package/src/client/sp00ky-client.ts +8 -0
- package/src/kernel/effects.ts +2 -0
- package/src/modules/auth/events/index.ts +7 -1
- package/src/modules/auth/impersonation-banner.ts +112 -0
- package/src/modules/auth/impersonation.test.ts +282 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +240 -36
- package/src/modules/devtools/index.ts +71 -1
- package/src/services/database/cache-engine.ts +6 -0
- package/src/services/database/local.ts +7 -0
- package/src/services/database/sqlite-cache-engine.ts +17 -0
- package/src/testing/fake-services.ts +1 -1
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
|
-
*
|
|
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 };
|