@spacelr/sdk 0.9.1 → 0.10.0
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/dist/index.d.mts +65 -20
- package/dist/index.d.ts +65 -20
- package/dist/index.js +94 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +94 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -372,12 +372,13 @@ type AuthLostListener = (reason: AuthLostReason) => void;
|
|
|
372
372
|
declare class TokenManager {
|
|
373
373
|
private storage;
|
|
374
374
|
private refreshBufferSeconds;
|
|
375
|
+
private refreshLockName;
|
|
375
376
|
private refreshCallback;
|
|
376
377
|
private refreshPromise;
|
|
377
378
|
private tokenRefreshedListeners;
|
|
378
379
|
private authLostListeners;
|
|
379
380
|
private authLostEmitted;
|
|
380
|
-
constructor(storage?: TokenStorage, refreshBufferSeconds?: number);
|
|
381
|
+
constructor(storage?: TokenStorage, refreshBufferSeconds?: number, refreshLockName?: string);
|
|
381
382
|
setRefreshCallback(callback: RefreshCallback): void;
|
|
382
383
|
getAccessToken(): Promise<string | null>;
|
|
383
384
|
setTokens(tokens: StoredTokens): Promise<void>;
|
|
@@ -398,6 +399,9 @@ declare class TokenManager {
|
|
|
398
399
|
private shouldRefresh;
|
|
399
400
|
private tryRefresh;
|
|
400
401
|
private executeRefresh;
|
|
402
|
+
private doRefresh;
|
|
403
|
+
private safeGetTokens;
|
|
404
|
+
private withCrossTabLock;
|
|
401
405
|
private emitTokenRefreshed;
|
|
402
406
|
}
|
|
403
407
|
|
|
@@ -512,6 +516,28 @@ declare class SpacelrEmailVerificationRequiredError extends SpacelrError {
|
|
|
512
516
|
}
|
|
513
517
|
|
|
514
518
|
type ConnectionState = 'connected' | 'reconnecting' | 'disconnected';
|
|
519
|
+
/** A single primitive `where` candidate. */
|
|
520
|
+
type WhereScalar = string | number | boolean;
|
|
521
|
+
/**
|
|
522
|
+
* Multi-value `where` operators (Firestore-style) for realtime subscriptions:
|
|
523
|
+
* - `in`: the document field is a **scalar** equal to any candidate.
|
|
524
|
+
* - `array-contains-any`: the document field is an **array** sharing at least
|
|
525
|
+
* one element with the candidates.
|
|
526
|
+
*
|
|
527
|
+
* The single-value form (`where: { field: scalar }`) stays polymorphic — it
|
|
528
|
+
* matches a scalar field by equality OR an array field by element membership.
|
|
529
|
+
*
|
|
530
|
+
* Mirrors the server definition in `@spacelr-workspace/shared-types`
|
|
531
|
+
* (`realtime-where.ts`); the two must stay structurally identical.
|
|
532
|
+
*/
|
|
533
|
+
type WhereOperator = {
|
|
534
|
+
in: WhereScalar[];
|
|
535
|
+
} | {
|
|
536
|
+
'array-contains-any': WhereScalar[];
|
|
537
|
+
};
|
|
538
|
+
type WhereValue = WhereScalar | WhereOperator;
|
|
539
|
+
/** A realtime subscription `where` filter: field name → scalar or operator. */
|
|
540
|
+
type WhereFilter = Record<string, WhereValue>;
|
|
515
541
|
interface DatabaseChangeEvent {
|
|
516
542
|
type: 'insert' | 'update' | 'delete';
|
|
517
543
|
projectId: string;
|
|
@@ -549,7 +575,7 @@ interface StreamSubscriptionOptions {
|
|
|
549
575
|
projectId: string;
|
|
550
576
|
collectionName: string;
|
|
551
577
|
sinceId?: string;
|
|
552
|
-
where?:
|
|
578
|
+
where?: WhereFilter;
|
|
553
579
|
/**
|
|
554
580
|
* Called for each delivered event. Awaited — the cursor does NOT advance
|
|
555
581
|
* until this resolves. If the callback throws, the cursor is NOT advanced
|
|
@@ -580,7 +606,7 @@ declare class RealtimeClient {
|
|
|
580
606
|
private connectionStateListeners;
|
|
581
607
|
private streamSubscriptions;
|
|
582
608
|
constructor(config: RealtimeConfig);
|
|
583
|
-
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error) => void, where?:
|
|
609
|
+
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error) => void, where?: WhereFilter): Promise<() => void>;
|
|
584
610
|
/**
|
|
585
611
|
* Subscribe to a stream-mode collection using Redis Streams replay +
|
|
586
612
|
* cursor-based delivery. Parallel to `subscribe()` (which targets
|
|
@@ -1085,16 +1111,18 @@ interface SubscribeWithSnapshotOptions<T> {
|
|
|
1085
1111
|
* **Snapshot semantics** (full MongoDB query): supports operators like
|
|
1086
1112
|
* `{ status: { $in: [...] } }`, `{ ts: { $gt: ... } }`, etc.
|
|
1087
1113
|
*
|
|
1088
|
-
* **Live-stream semantics** (
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1114
|
+
* **Live-stream semantics** (top-level primitives only): this helper
|
|
1115
|
+
* forwards only top-level primitive values (`string | number | boolean`)
|
|
1116
|
+
* from the Mongo-query `where` to the live filter. MongoDB operators
|
|
1117
|
+
* (`$in`, `$gt`, …) are silently dropped at handshake time and are NOT
|
|
1118
|
+
* translated into the gateway's realtime `in` / `array-contains-any`
|
|
1119
|
+
* operators — the remaining primitive entries still apply, so a typical
|
|
1120
|
+
* chat-style `{ chatId: 'c1' }` filter works identically on both sides. If
|
|
1121
|
+
* you pass a mixed `{ chatId: 'c1', status: { $in: [...] } }`, the snapshot
|
|
1122
|
+
* is fully filtered but the live stream filters by `chatId` only; you may
|
|
1123
|
+
* receive `onChange` events for documents whose `status` is outside your
|
|
1124
|
+
* `$in` set. Re-check in the handler, or use `subscribeEvents()` directly
|
|
1125
|
+
* with a realtime `{ status: { in: [...] } }` operator for live filtering.
|
|
1098
1126
|
*/
|
|
1099
1127
|
where?: Record<string, unknown>;
|
|
1100
1128
|
sort?: Record<string, 1 | -1>;
|
|
@@ -1155,7 +1183,7 @@ interface SubscribeWithSnapshotOptions<T> {
|
|
|
1155
1183
|
interface SubscribeEventsHandlers<T = Record<string, unknown>> {
|
|
1156
1184
|
/** Cursor to resume from. Undefined = fresh subscription, deliver only new events. */
|
|
1157
1185
|
sinceId?: string;
|
|
1158
|
-
where?:
|
|
1186
|
+
where?: WhereFilter;
|
|
1159
1187
|
/**
|
|
1160
1188
|
* Optional resume-cursor store. When set, the SDK loads the previous cursor
|
|
1161
1189
|
* before subscribing and persists the new cursor after each delivered
|
|
@@ -1212,7 +1240,7 @@ interface StreamSubscription {
|
|
|
1212
1240
|
getCursor(): string | undefined;
|
|
1213
1241
|
}
|
|
1214
1242
|
interface SubscribeHandlers<T = Record<string, unknown>> {
|
|
1215
|
-
where?:
|
|
1243
|
+
where?: WhereFilter;
|
|
1216
1244
|
onInsert?: (doc: T & {
|
|
1217
1245
|
_id: string;
|
|
1218
1246
|
}) => void;
|
|
@@ -1542,6 +1570,11 @@ interface FunctionInvokeOptions {
|
|
|
1542
1570
|
*
|
|
1543
1571
|
* If `true` but the user is not signed in, the header is simply omitted —
|
|
1544
1572
|
* safe for `public` invokeMode.
|
|
1573
|
+
*
|
|
1574
|
+
* Note: for `authenticated`/`hybrid` invokeMode, the server additionally
|
|
1575
|
+
* requires the token's user to be a member of the target project — this
|
|
1576
|
+
* flag only controls whether the header is attached, not who the server
|
|
1577
|
+
* accepts.
|
|
1545
1578
|
*/
|
|
1546
1579
|
authenticated?: boolean;
|
|
1547
1580
|
payload?: Record<string, unknown>;
|
|
@@ -1560,10 +1593,14 @@ declare class FunctionsModule {
|
|
|
1560
1593
|
* `config.apiUrl` (which already carries the `/api/v1` prefix).
|
|
1561
1594
|
*
|
|
1562
1595
|
* Auth defaults, based on `invokeMode` semantics:
|
|
1563
|
-
* - webhook:
|
|
1564
|
-
* - authenticated:
|
|
1565
|
-
*
|
|
1566
|
-
* -
|
|
1596
|
+
* - webhook: pass `secret` → Authorization is NOT attached
|
|
1597
|
+
* - authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
1598
|
+
* caller must be a member of the target project
|
|
1599
|
+
* - public: pass nothing → Authorization is attached if logged in, else omitted
|
|
1600
|
+
* - hybrid: pass both `secret` and `authenticated: true`; JWT path
|
|
1601
|
+
* requires project membership like `authenticated`
|
|
1602
|
+
* - platform-authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
1603
|
+
* any signed-in user is accepted, still just attaches the bearer token
|
|
1567
1604
|
*
|
|
1568
1605
|
* To force a specific behaviour, set `authenticated` explicitly — it wins
|
|
1569
1606
|
* over the `secret`-based default.
|
|
@@ -1702,6 +1739,14 @@ interface SpacelrClient {
|
|
|
1702
1739
|
setTokens(tokens: StoredTokens): Promise<void>;
|
|
1703
1740
|
/** Clear stored tokens and reset auth-loss state. */
|
|
1704
1741
|
clearTokens(): Promise<void>;
|
|
1742
|
+
/**
|
|
1743
|
+
* Whether a session (a stored token set) currently exists in the configured
|
|
1744
|
+
* TokenStorage. Use this to distinguish "genuinely logged out" from
|
|
1745
|
+
* "maybe-restorable" without reading the storage backend directly — so it
|
|
1746
|
+
* works with any TokenStorage (memory, IndexedDB, …), not just the default
|
|
1747
|
+
* localStorage one.
|
|
1748
|
+
*/
|
|
1749
|
+
hasStoredSession(): Promise<boolean>;
|
|
1705
1750
|
/** Disconnect realtime WebSocket (if connected) */
|
|
1706
1751
|
disconnect(): void;
|
|
1707
1752
|
/**
|
|
@@ -1751,4 +1796,4 @@ interface SpacelrClient {
|
|
|
1751
1796
|
}
|
|
1752
1797
|
declare function createClient(config: SpacelrClientConfig): SpacelrClient;
|
|
1753
1798
|
|
|
1754
|
-
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, CursorInvalidError, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, ForbiddenError, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, NotFoundError, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PasskeyAuthenticationOptionsJSON, type PasskeyAuthenticationResponseJSON, type PasskeyCredential, type PasskeyCredentialDescriptorJSON, type PasskeyLoginResponse, type PasskeyRegistrationOptionsJSON, type PasskeyRegistrationResponseJSON, type PasskeyRegistrationResult, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type Schedule, type ScheduleInvokeOptions, type ScheduleListOptions, type ScheduleStatus, type SearchOptions, ServerConfigError, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrSearchFilterRequiredError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type SubscribeWithSnapshotOptions, type TimelineAndFilter, TimelineError, type TimelineFieldFilter, type TimelineFilter, type TimelineLeafFilter, TimelineModule, type TimelineOrderBy, type TimelineQueryOptions, type TimelineQueryResponse, type TimelineScalar, type TimelineSourceStats, TimeoutError, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, ValidationError, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
|
1799
|
+
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, CursorInvalidError, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, ForbiddenError, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, NotFoundError, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PasskeyAuthenticationOptionsJSON, type PasskeyAuthenticationResponseJSON, type PasskeyCredential, type PasskeyCredentialDescriptorJSON, type PasskeyLoginResponse, type PasskeyRegistrationOptionsJSON, type PasskeyRegistrationResponseJSON, type PasskeyRegistrationResult, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type Schedule, type ScheduleInvokeOptions, type ScheduleListOptions, type ScheduleStatus, type SearchOptions, ServerConfigError, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrSearchFilterRequiredError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type SubscribeWithSnapshotOptions, type TimelineAndFilter, TimelineError, type TimelineFieldFilter, type TimelineFilter, type TimelineLeafFilter, TimelineModule, type TimelineOrderBy, type TimelineQueryOptions, type TimelineQueryResponse, type TimelineScalar, type TimelineSourceStats, TimeoutError, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, ValidationError, type VapidKeyResponse, type WhereFilter, type WhereOperator, type WhereScalar, type WhereValue, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
package/dist/index.d.ts
CHANGED
|
@@ -372,12 +372,13 @@ type AuthLostListener = (reason: AuthLostReason) => void;
|
|
|
372
372
|
declare class TokenManager {
|
|
373
373
|
private storage;
|
|
374
374
|
private refreshBufferSeconds;
|
|
375
|
+
private refreshLockName;
|
|
375
376
|
private refreshCallback;
|
|
376
377
|
private refreshPromise;
|
|
377
378
|
private tokenRefreshedListeners;
|
|
378
379
|
private authLostListeners;
|
|
379
380
|
private authLostEmitted;
|
|
380
|
-
constructor(storage?: TokenStorage, refreshBufferSeconds?: number);
|
|
381
|
+
constructor(storage?: TokenStorage, refreshBufferSeconds?: number, refreshLockName?: string);
|
|
381
382
|
setRefreshCallback(callback: RefreshCallback): void;
|
|
382
383
|
getAccessToken(): Promise<string | null>;
|
|
383
384
|
setTokens(tokens: StoredTokens): Promise<void>;
|
|
@@ -398,6 +399,9 @@ declare class TokenManager {
|
|
|
398
399
|
private shouldRefresh;
|
|
399
400
|
private tryRefresh;
|
|
400
401
|
private executeRefresh;
|
|
402
|
+
private doRefresh;
|
|
403
|
+
private safeGetTokens;
|
|
404
|
+
private withCrossTabLock;
|
|
401
405
|
private emitTokenRefreshed;
|
|
402
406
|
}
|
|
403
407
|
|
|
@@ -512,6 +516,28 @@ declare class SpacelrEmailVerificationRequiredError extends SpacelrError {
|
|
|
512
516
|
}
|
|
513
517
|
|
|
514
518
|
type ConnectionState = 'connected' | 'reconnecting' | 'disconnected';
|
|
519
|
+
/** A single primitive `where` candidate. */
|
|
520
|
+
type WhereScalar = string | number | boolean;
|
|
521
|
+
/**
|
|
522
|
+
* Multi-value `where` operators (Firestore-style) for realtime subscriptions:
|
|
523
|
+
* - `in`: the document field is a **scalar** equal to any candidate.
|
|
524
|
+
* - `array-contains-any`: the document field is an **array** sharing at least
|
|
525
|
+
* one element with the candidates.
|
|
526
|
+
*
|
|
527
|
+
* The single-value form (`where: { field: scalar }`) stays polymorphic — it
|
|
528
|
+
* matches a scalar field by equality OR an array field by element membership.
|
|
529
|
+
*
|
|
530
|
+
* Mirrors the server definition in `@spacelr-workspace/shared-types`
|
|
531
|
+
* (`realtime-where.ts`); the two must stay structurally identical.
|
|
532
|
+
*/
|
|
533
|
+
type WhereOperator = {
|
|
534
|
+
in: WhereScalar[];
|
|
535
|
+
} | {
|
|
536
|
+
'array-contains-any': WhereScalar[];
|
|
537
|
+
};
|
|
538
|
+
type WhereValue = WhereScalar | WhereOperator;
|
|
539
|
+
/** A realtime subscription `where` filter: field name → scalar or operator. */
|
|
540
|
+
type WhereFilter = Record<string, WhereValue>;
|
|
515
541
|
interface DatabaseChangeEvent {
|
|
516
542
|
type: 'insert' | 'update' | 'delete';
|
|
517
543
|
projectId: string;
|
|
@@ -549,7 +575,7 @@ interface StreamSubscriptionOptions {
|
|
|
549
575
|
projectId: string;
|
|
550
576
|
collectionName: string;
|
|
551
577
|
sinceId?: string;
|
|
552
|
-
where?:
|
|
578
|
+
where?: WhereFilter;
|
|
553
579
|
/**
|
|
554
580
|
* Called for each delivered event. Awaited — the cursor does NOT advance
|
|
555
581
|
* until this resolves. If the callback throws, the cursor is NOT advanced
|
|
@@ -580,7 +606,7 @@ declare class RealtimeClient {
|
|
|
580
606
|
private connectionStateListeners;
|
|
581
607
|
private streamSubscriptions;
|
|
582
608
|
constructor(config: RealtimeConfig);
|
|
583
|
-
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error) => void, where?:
|
|
609
|
+
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error) => void, where?: WhereFilter): Promise<() => void>;
|
|
584
610
|
/**
|
|
585
611
|
* Subscribe to a stream-mode collection using Redis Streams replay +
|
|
586
612
|
* cursor-based delivery. Parallel to `subscribe()` (which targets
|
|
@@ -1085,16 +1111,18 @@ interface SubscribeWithSnapshotOptions<T> {
|
|
|
1085
1111
|
* **Snapshot semantics** (full MongoDB query): supports operators like
|
|
1086
1112
|
* `{ status: { $in: [...] } }`, `{ ts: { $gt: ... } }`, etc.
|
|
1087
1113
|
*
|
|
1088
|
-
* **Live-stream semantics** (
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1114
|
+
* **Live-stream semantics** (top-level primitives only): this helper
|
|
1115
|
+
* forwards only top-level primitive values (`string | number | boolean`)
|
|
1116
|
+
* from the Mongo-query `where` to the live filter. MongoDB operators
|
|
1117
|
+
* (`$in`, `$gt`, …) are silently dropped at handshake time and are NOT
|
|
1118
|
+
* translated into the gateway's realtime `in` / `array-contains-any`
|
|
1119
|
+
* operators — the remaining primitive entries still apply, so a typical
|
|
1120
|
+
* chat-style `{ chatId: 'c1' }` filter works identically on both sides. If
|
|
1121
|
+
* you pass a mixed `{ chatId: 'c1', status: { $in: [...] } }`, the snapshot
|
|
1122
|
+
* is fully filtered but the live stream filters by `chatId` only; you may
|
|
1123
|
+
* receive `onChange` events for documents whose `status` is outside your
|
|
1124
|
+
* `$in` set. Re-check in the handler, or use `subscribeEvents()` directly
|
|
1125
|
+
* with a realtime `{ status: { in: [...] } }` operator for live filtering.
|
|
1098
1126
|
*/
|
|
1099
1127
|
where?: Record<string, unknown>;
|
|
1100
1128
|
sort?: Record<string, 1 | -1>;
|
|
@@ -1155,7 +1183,7 @@ interface SubscribeWithSnapshotOptions<T> {
|
|
|
1155
1183
|
interface SubscribeEventsHandlers<T = Record<string, unknown>> {
|
|
1156
1184
|
/** Cursor to resume from. Undefined = fresh subscription, deliver only new events. */
|
|
1157
1185
|
sinceId?: string;
|
|
1158
|
-
where?:
|
|
1186
|
+
where?: WhereFilter;
|
|
1159
1187
|
/**
|
|
1160
1188
|
* Optional resume-cursor store. When set, the SDK loads the previous cursor
|
|
1161
1189
|
* before subscribing and persists the new cursor after each delivered
|
|
@@ -1212,7 +1240,7 @@ interface StreamSubscription {
|
|
|
1212
1240
|
getCursor(): string | undefined;
|
|
1213
1241
|
}
|
|
1214
1242
|
interface SubscribeHandlers<T = Record<string, unknown>> {
|
|
1215
|
-
where?:
|
|
1243
|
+
where?: WhereFilter;
|
|
1216
1244
|
onInsert?: (doc: T & {
|
|
1217
1245
|
_id: string;
|
|
1218
1246
|
}) => void;
|
|
@@ -1542,6 +1570,11 @@ interface FunctionInvokeOptions {
|
|
|
1542
1570
|
*
|
|
1543
1571
|
* If `true` but the user is not signed in, the header is simply omitted —
|
|
1544
1572
|
* safe for `public` invokeMode.
|
|
1573
|
+
*
|
|
1574
|
+
* Note: for `authenticated`/`hybrid` invokeMode, the server additionally
|
|
1575
|
+
* requires the token's user to be a member of the target project — this
|
|
1576
|
+
* flag only controls whether the header is attached, not who the server
|
|
1577
|
+
* accepts.
|
|
1545
1578
|
*/
|
|
1546
1579
|
authenticated?: boolean;
|
|
1547
1580
|
payload?: Record<string, unknown>;
|
|
@@ -1560,10 +1593,14 @@ declare class FunctionsModule {
|
|
|
1560
1593
|
* `config.apiUrl` (which already carries the `/api/v1` prefix).
|
|
1561
1594
|
*
|
|
1562
1595
|
* Auth defaults, based on `invokeMode` semantics:
|
|
1563
|
-
* - webhook:
|
|
1564
|
-
* - authenticated:
|
|
1565
|
-
*
|
|
1566
|
-
* -
|
|
1596
|
+
* - webhook: pass `secret` → Authorization is NOT attached
|
|
1597
|
+
* - authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
1598
|
+
* caller must be a member of the target project
|
|
1599
|
+
* - public: pass nothing → Authorization is attached if logged in, else omitted
|
|
1600
|
+
* - hybrid: pass both `secret` and `authenticated: true`; JWT path
|
|
1601
|
+
* requires project membership like `authenticated`
|
|
1602
|
+
* - platform-authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
1603
|
+
* any signed-in user is accepted, still just attaches the bearer token
|
|
1567
1604
|
*
|
|
1568
1605
|
* To force a specific behaviour, set `authenticated` explicitly — it wins
|
|
1569
1606
|
* over the `secret`-based default.
|
|
@@ -1702,6 +1739,14 @@ interface SpacelrClient {
|
|
|
1702
1739
|
setTokens(tokens: StoredTokens): Promise<void>;
|
|
1703
1740
|
/** Clear stored tokens and reset auth-loss state. */
|
|
1704
1741
|
clearTokens(): Promise<void>;
|
|
1742
|
+
/**
|
|
1743
|
+
* Whether a session (a stored token set) currently exists in the configured
|
|
1744
|
+
* TokenStorage. Use this to distinguish "genuinely logged out" from
|
|
1745
|
+
* "maybe-restorable" without reading the storage backend directly — so it
|
|
1746
|
+
* works with any TokenStorage (memory, IndexedDB, …), not just the default
|
|
1747
|
+
* localStorage one.
|
|
1748
|
+
*/
|
|
1749
|
+
hasStoredSession(): Promise<boolean>;
|
|
1705
1750
|
/** Disconnect realtime WebSocket (if connected) */
|
|
1706
1751
|
disconnect(): void;
|
|
1707
1752
|
/**
|
|
@@ -1751,4 +1796,4 @@ interface SpacelrClient {
|
|
|
1751
1796
|
}
|
|
1752
1797
|
declare function createClient(config: SpacelrClientConfig): SpacelrClient;
|
|
1753
1798
|
|
|
1754
|
-
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, CursorInvalidError, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, ForbiddenError, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, NotFoundError, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PasskeyAuthenticationOptionsJSON, type PasskeyAuthenticationResponseJSON, type PasskeyCredential, type PasskeyCredentialDescriptorJSON, type PasskeyLoginResponse, type PasskeyRegistrationOptionsJSON, type PasskeyRegistrationResponseJSON, type PasskeyRegistrationResult, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type Schedule, type ScheduleInvokeOptions, type ScheduleListOptions, type ScheduleStatus, type SearchOptions, ServerConfigError, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrSearchFilterRequiredError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type SubscribeWithSnapshotOptions, type TimelineAndFilter, TimelineError, type TimelineFieldFilter, type TimelineFilter, type TimelineLeafFilter, TimelineModule, type TimelineOrderBy, type TimelineQueryOptions, type TimelineQueryResponse, type TimelineScalar, type TimelineSourceStats, TimeoutError, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, ValidationError, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
|
1799
|
+
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, CursorInvalidError, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, ForbiddenError, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, NotFoundError, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PasskeyAuthenticationOptionsJSON, type PasskeyAuthenticationResponseJSON, type PasskeyCredential, type PasskeyCredentialDescriptorJSON, type PasskeyLoginResponse, type PasskeyRegistrationOptionsJSON, type PasskeyRegistrationResponseJSON, type PasskeyRegistrationResult, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type Schedule, type ScheduleInvokeOptions, type ScheduleListOptions, type ScheduleStatus, type SearchOptions, ServerConfigError, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrSearchFilterRequiredError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type SubscribeWithSnapshotOptions, type TimelineAndFilter, TimelineError, type TimelineFieldFilter, type TimelineFilter, type TimelineLeafFilter, TimelineModule, type TimelineOrderBy, type TimelineQueryOptions, type TimelineQueryResponse, type TimelineScalar, type TimelineSourceStats, TimeoutError, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, ValidationError, type VapidKeyResponse, type WhereFilter, type WhereOperator, type WhereScalar, type WhereValue, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
package/dist/index.js
CHANGED
|
@@ -428,7 +428,7 @@ var BrowserTokenStorage = class {
|
|
|
428
428
|
|
|
429
429
|
// libs/sdk/src/core/token-manager.ts
|
|
430
430
|
var TokenManager = class {
|
|
431
|
-
constructor(storage, refreshBufferSeconds = 60) {
|
|
431
|
+
constructor(storage, refreshBufferSeconds = 60, refreshLockName = "spacelr_token_refresh") {
|
|
432
432
|
this.refreshCallback = null;
|
|
433
433
|
this.refreshPromise = null;
|
|
434
434
|
this.tokenRefreshedListeners = /* @__PURE__ */ new Set();
|
|
@@ -439,6 +439,7 @@ var TokenManager = class {
|
|
|
439
439
|
this.authLostEmitted = false;
|
|
440
440
|
this.storage = storage ?? new MemoryTokenStorage();
|
|
441
441
|
this.refreshBufferSeconds = refreshBufferSeconds;
|
|
442
|
+
this.refreshLockName = refreshLockName;
|
|
442
443
|
}
|
|
443
444
|
setRefreshCallback(callback) {
|
|
444
445
|
this.refreshCallback = callback;
|
|
@@ -468,7 +469,7 @@ var TokenManager = class {
|
|
|
468
469
|
this.authLostEmitted = false;
|
|
469
470
|
}
|
|
470
471
|
async getStoredTokens() {
|
|
471
|
-
return this.
|
|
472
|
+
return this.safeGetTokens();
|
|
472
473
|
}
|
|
473
474
|
/**
|
|
474
475
|
* Force a refresh using the current stored refresh token.
|
|
@@ -535,9 +536,18 @@ var TokenManager = class {
|
|
|
535
536
|
}
|
|
536
537
|
}
|
|
537
538
|
async executeRefresh(refreshToken) {
|
|
539
|
+
return this.withCrossTabLock(() => this.doRefresh(refreshToken));
|
|
540
|
+
}
|
|
541
|
+
async doRefresh(refreshToken) {
|
|
538
542
|
const callback = this.refreshCallback;
|
|
543
|
+
const current = await this.safeGetTokens();
|
|
544
|
+
if (current && current.refreshToken && current.refreshToken !== refreshToken && !this.isTokenExpired(current)) {
|
|
545
|
+
this.emitTokenRefreshed(current);
|
|
546
|
+
return current;
|
|
547
|
+
}
|
|
548
|
+
const activeRefreshToken = current?.refreshToken || refreshToken;
|
|
539
549
|
try {
|
|
540
|
-
const newTokens = await callback(
|
|
550
|
+
const newTokens = await callback(activeRefreshToken);
|
|
541
551
|
await this.storage.setTokens(newTokens);
|
|
542
552
|
this.emitTokenRefreshed(newTokens);
|
|
543
553
|
return newTokens;
|
|
@@ -546,6 +556,22 @@ var TokenManager = class {
|
|
|
546
556
|
throw error;
|
|
547
557
|
}
|
|
548
558
|
}
|
|
559
|
+
async safeGetTokens() {
|
|
560
|
+
try {
|
|
561
|
+
return await this.storage.getTokens();
|
|
562
|
+
} catch {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
withCrossTabLock(fn) {
|
|
567
|
+
const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
568
|
+
if (!locks) return fn();
|
|
569
|
+
try {
|
|
570
|
+
return locks.request(this.refreshLockName, fn);
|
|
571
|
+
} catch {
|
|
572
|
+
return fn();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
549
575
|
emitTokenRefreshed(tokens) {
|
|
550
576
|
for (const listener of this.tokenRefreshedListeners) {
|
|
551
577
|
try {
|
|
@@ -631,6 +657,57 @@ var PERMANENT_STREAM_ACK_ERRORS = /* @__PURE__ */ new Set([
|
|
|
631
657
|
"Not a member of this project",
|
|
632
658
|
"Subscribe denied"
|
|
633
659
|
]);
|
|
660
|
+
function isWhereScalar(value) {
|
|
661
|
+
const t = typeof value;
|
|
662
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
663
|
+
}
|
|
664
|
+
function isWhereOperator(value) {
|
|
665
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
const keys = Object.keys(value);
|
|
669
|
+
if (keys.length !== 1) return false;
|
|
670
|
+
const op = keys[0];
|
|
671
|
+
if (op !== "in" && op !== "array-contains-any") return false;
|
|
672
|
+
const candidates = value[op];
|
|
673
|
+
return Array.isArray(candidates) && candidates.every(isWhereScalar);
|
|
674
|
+
}
|
|
675
|
+
var WHERE_PATH_ABSENT = /* @__PURE__ */ Symbol("where-path-absent");
|
|
676
|
+
var WHERE_PATH_UNRESOLVABLE = /* @__PURE__ */ Symbol("where-path-unresolvable");
|
|
677
|
+
var RESERVED_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
678
|
+
var MAX_WHERE_PATH_SEGMENTS = 16;
|
|
679
|
+
function resolveWherePath(document2, key) {
|
|
680
|
+
const segments = key.split(".");
|
|
681
|
+
if (segments.length > MAX_WHERE_PATH_SEGMENTS) return WHERE_PATH_ABSENT;
|
|
682
|
+
let cur = document2;
|
|
683
|
+
for (const segment of segments) {
|
|
684
|
+
if (RESERVED_PATH_SEGMENTS.has(segment)) return WHERE_PATH_ABSENT;
|
|
685
|
+
if (cur === null || cur === void 0) return WHERE_PATH_ABSENT;
|
|
686
|
+
if (Array.isArray(cur)) return WHERE_PATH_UNRESOLVABLE;
|
|
687
|
+
if (typeof cur !== "object") return WHERE_PATH_ABSENT;
|
|
688
|
+
if (!Object.prototype.hasOwnProperty.call(cur, segment)) {
|
|
689
|
+
return WHERE_PATH_ABSENT;
|
|
690
|
+
}
|
|
691
|
+
cur = cur[segment];
|
|
692
|
+
}
|
|
693
|
+
return cur === void 0 ? WHERE_PATH_ABSENT : cur;
|
|
694
|
+
}
|
|
695
|
+
function whereValueMatches(actual, expected) {
|
|
696
|
+
if (actual === WHERE_PATH_ABSENT || actual === WHERE_PATH_UNRESOLVABLE) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
if (isWhereOperator(expected)) {
|
|
700
|
+
if ("in" in expected) {
|
|
701
|
+
if (Array.isArray(actual)) return false;
|
|
702
|
+
return expected.in.includes(actual);
|
|
703
|
+
}
|
|
704
|
+
if (!Array.isArray(actual)) return false;
|
|
705
|
+
const candidates = expected["array-contains-any"];
|
|
706
|
+
return actual.some((el) => candidates.includes(el));
|
|
707
|
+
}
|
|
708
|
+
if (Array.isArray(actual)) return actual.includes(expected);
|
|
709
|
+
return actual === expected;
|
|
710
|
+
}
|
|
634
711
|
var RealtimeClient = class {
|
|
635
712
|
constructor(config) {
|
|
636
713
|
this.socket = null;
|
|
@@ -1019,12 +1096,7 @@ var RealtimeClient = class {
|
|
|
1019
1096
|
if (!where) return false;
|
|
1020
1097
|
if (!event.document) return false;
|
|
1021
1098
|
for (const [key, value] of Object.entries(where)) {
|
|
1022
|
-
|
|
1023
|
-
if (Array.isArray(docValue)) {
|
|
1024
|
-
if (!docValue.includes(value)) {
|
|
1025
|
-
return false;
|
|
1026
|
-
}
|
|
1027
|
-
} else if (docValue !== value) {
|
|
1099
|
+
if (!whereValueMatches(resolveWherePath(event.document, key), value)) {
|
|
1028
1100
|
return false;
|
|
1029
1101
|
}
|
|
1030
1102
|
}
|
|
@@ -2985,10 +3057,14 @@ var FunctionsModule = class {
|
|
|
2985
3057
|
* `config.apiUrl` (which already carries the `/api/v1` prefix).
|
|
2986
3058
|
*
|
|
2987
3059
|
* Auth defaults, based on `invokeMode` semantics:
|
|
2988
|
-
* - webhook:
|
|
2989
|
-
* - authenticated:
|
|
2990
|
-
*
|
|
2991
|
-
* -
|
|
3060
|
+
* - webhook: pass `secret` → Authorization is NOT attached
|
|
3061
|
+
* - authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
3062
|
+
* caller must be a member of the target project
|
|
3063
|
+
* - public: pass nothing → Authorization is attached if logged in, else omitted
|
|
3064
|
+
* - hybrid: pass both `secret` and `authenticated: true`; JWT path
|
|
3065
|
+
* requires project membership like `authenticated`
|
|
3066
|
+
* - platform-authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
3067
|
+
* any signed-in user is accepted, still just attaches the bearer token
|
|
2992
3068
|
*
|
|
2993
3069
|
* To force a specific behaviour, set `authenticated` explicitly — it wins
|
|
2994
3070
|
* over the `secret`-based default.
|
|
@@ -3074,7 +3150,8 @@ function createClient(config) {
|
|
|
3074
3150
|
const tokenStorage = config.tokenStorage ?? (typeof window !== "undefined" && typeof window.localStorage !== "undefined" ? new BrowserTokenStorage() : new MemoryTokenStorage());
|
|
3075
3151
|
const tokenManager = new TokenManager(
|
|
3076
3152
|
tokenStorage,
|
|
3077
|
-
config.refreshBufferSeconds ?? 60
|
|
3153
|
+
config.refreshBufferSeconds ?? 60,
|
|
3154
|
+
`spacelr_token_refresh:${config.projectId}`
|
|
3078
3155
|
);
|
|
3079
3156
|
const httpClient = new HttpClient(config, tokenManager);
|
|
3080
3157
|
const realtime = new RealtimeClient({
|
|
@@ -3101,6 +3178,9 @@ function createClient(config) {
|
|
|
3101
3178
|
clearTokens() {
|
|
3102
3179
|
return tokenManager.clearTokens();
|
|
3103
3180
|
},
|
|
3181
|
+
hasStoredSession() {
|
|
3182
|
+
return tokenManager.getStoredTokens().then((tokens) => !!tokens);
|
|
3183
|
+
},
|
|
3104
3184
|
disconnect() {
|
|
3105
3185
|
realtime.disconnect();
|
|
3106
3186
|
},
|