@spacelr/sdk 0.5.0 → 0.5.2

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/README.md CHANGED
@@ -42,9 +42,41 @@ spacelr.db.subscribe('my-collection', {
42
42
  | --- | --- |
43
43
  | `auth` | Login, registration, OAuth2 PKCE flow, token management, 2FA |
44
44
  | `storage` | File upload (including multipart), download, sharing, quota |
45
- | `db` | Database operations with realtime subscriptions via WebSocket |
45
+ | `db` | Database operations with realtime subscriptions via WebSocket; `db.timeline` for cold-tier history reads |
46
46
  | `notifications` | Web Push notification subscription management |
47
47
 
48
+ ## Cold-tier history (`db.timeline`)
49
+
50
+ Collections can enable **cold-tier** archival: aged documents are moved to
51
+ object storage and purged from the live (hot) MongoDB tier. The normal query
52
+ methods (`find`, `findById`, `count`, `search`, `paginate`) operate on the **hot
53
+ tier only** — archived documents will not appear in their results.
54
+
55
+ To read archived history, use the timeline API, which transparently merges hot
56
+ and cold data for a partition (e.g. a chat room) and paginates with an opaque
57
+ cursor:
58
+
59
+ ```ts
60
+ const page = await spacelr.db.timeline.query({
61
+ collection: 'chat_messages',
62
+ partitionValue: 'room-123', // the cold-tier partition (e.g. room id)
63
+ where: { authorId: { $eq: 'user-1' } }, // optional, allow-listed operators only
64
+ orderBy: { field: 'createdAt', direction: 'desc' },
65
+ limit: 50, // 1–200, default 50
66
+ });
67
+
68
+ page.items; // merged hot + cold documents
69
+ page.nextCursor; // pass back as `cursor` for the next page (null = end)
70
+ page.sourceStats; // { hot, cold, segmentsScanned? } — where the rows came from
71
+ ```
72
+
73
+ Notes:
74
+ - Timeline paginates by partition + timestamp, not arbitrary Mongo queries;
75
+ `where` accepts only the allow-listed operators (`$eq`, `$ne`, `$lt`, `$lte`,
76
+ `$gt`, `$gte`, `$in`, `$nin`, `$and`).
77
+ - Enabling/configuring cold-tier on a collection is an **admin** operation and
78
+ is not part of this client SDK.
79
+
48
80
  ## Requirements
49
81
 
50
82
  - Node.js >= 18
package/dist/index.d.mts CHANGED
@@ -738,6 +738,98 @@ declare class StorageModule {
738
738
  getPublicFileUrl(fileId: string, projectId?: string): Promise<string>;
739
739
  }
740
740
 
741
+ /**
742
+ * Scalar values permitted inside a timeline filter. Matches the server
743
+ * filter allow-list (libs/shared-dtos/.../timeline-filter.dto.ts).
744
+ */
745
+ type TimelineScalar = string | number | boolean | null;
746
+ /**
747
+ * Single-field filter expression. Either implicit equality (a scalar) or
748
+ * an explicit operator object. Mongo-style allow-list — the server rejects
749
+ * anything outside this set.
750
+ */
751
+ type TimelineLeafFilter = TimelineScalar | {
752
+ $eq?: TimelineScalar;
753
+ $ne?: TimelineScalar;
754
+ $lt?: TimelineScalar;
755
+ $lte?: TimelineScalar;
756
+ $gt?: TimelineScalar;
757
+ $gte?: TimelineScalar;
758
+ $in?: TimelineScalar[];
759
+ $nin?: TimelineScalar[];
760
+ };
761
+ /**
762
+ * Filter document for a timeline query. Top-level fields apply implicit
763
+ * equality (or operator expression). $and supports nested filter
764
+ * composition; $or / $regex / etc. are intentionally NOT exposed and
765
+ * will be rejected server-side.
766
+ *
767
+ * Two named branches in the union so TypeScript doesn't treat `$and` as
768
+ * just another key under the field index signature — the array shape
769
+ * needs its own type or downstream usage breaks.
770
+ */
771
+ interface TimelineAndFilter {
772
+ $and: TimelineFilter[];
773
+ }
774
+ type TimelineFieldFilter = Record<string, TimelineLeafFilter>;
775
+ type TimelineFilter = TimelineAndFilter | TimelineFieldFilter;
776
+ interface TimelineOrderBy {
777
+ field: string;
778
+ direction: 'asc' | 'desc';
779
+ }
780
+ interface TimelineQueryOptions {
781
+ /** Collection name to read from. */
782
+ collection: string;
783
+ /** Partition value (e.g. roomId, documentId — collection-configured). */
784
+ partitionValue: string;
785
+ /** Optional filter constraints (allow-listed operators only). */
786
+ where?: TimelineFilter;
787
+ /**
788
+ * Sort order. Defaults to DESC by the collection's configured timeline
789
+ * field; PR A only supports DESC. Passing direction='asc' is reserved
790
+ * for a future phase and currently returns BAD_REQUEST.
791
+ */
792
+ orderBy?: TimelineOrderBy;
793
+ /** Page size (1..200). Default 50. */
794
+ limit?: number;
795
+ /**
796
+ * Opaque cursor from a previous response. The SDK does NOT decode this —
797
+ * pass the value of `nextCursor` from the last response verbatim.
798
+ */
799
+ cursor?: string;
800
+ }
801
+ interface TimelineSourceStats {
802
+ hot: number;
803
+ cold: number;
804
+ /** Number of cold archive segments scanned (only present when cold path ran). */
805
+ segmentsScanned?: number;
806
+ }
807
+ interface TimelineQueryResponse {
808
+ items: Record<string, unknown>[];
809
+ /** Opaque continuation token. `null` when the page is the last one. */
810
+ nextCursor: string | null;
811
+ sourceStats: TimelineSourceStats;
812
+ }
813
+
814
+ /**
815
+ * Low-level timeline SDK module. One method: query(opts) → page.
816
+ *
817
+ * The cursor is opaque from the SDK's perspective — it's stored as a
818
+ * string, never decoded, never inspected. Callers pass the value of
819
+ * `nextCursor` from the previous response verbatim into the next request.
820
+ *
821
+ * Errors thrown from query() extend SpacelrError so they integrate with
822
+ * the SDK's existing error hierarchy. Use one of the typed subclasses
823
+ * (CursorInvalidError, ForbiddenError, etc.) to catch specific cases or
824
+ * `catch (e instanceof TimelineError)` for any timeline-related failure.
825
+ */
826
+ declare class TimelineModule {
827
+ private readonly http;
828
+ constructor(http: HttpClient);
829
+ query(opts: TimelineQueryOptions): Promise<TimelineQueryResponse>;
830
+ private translateError;
831
+ }
832
+
741
833
  interface PopulateOption {
742
834
  field: string;
743
835
  collection: string;
@@ -1106,6 +1198,16 @@ declare class CollectionRef<T = Record<string, unknown>> {
1106
1198
  insertMany(documents: (Partial<T> & {
1107
1199
  _id?: string;
1108
1200
  })[]): Promise<InsertResult>;
1201
+ /**
1202
+ * Query the collection's **hot tier** (live MongoDB).
1203
+ *
1204
+ * **Cold-tier note:** if the collection has cold-tier archival enabled, aged
1205
+ * documents are moved to object storage and purged from the hot tier — they
1206
+ * will NOT appear in `find()` results (nor `findById`, `count`, `search`,
1207
+ * `paginate`, which are all hot-tier only). To read archived history, use
1208
+ * {@link TimelineModule.query} via `db.timeline.query(...)`, which transparently
1209
+ * merges hot and cold results for a partition.
1210
+ */
1109
1211
  find(filter?: Record<string, unknown>): QueryBuilder<T>;
1110
1212
  /**
1111
1213
  * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`
@@ -1136,13 +1238,30 @@ declare class CollectionRef<T = Record<string, unknown>> {
1136
1238
  *
1137
1239
  * Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching
1138
1240
  * `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.
1241
+ *
1242
+ * **Cold-tier note:** searches the hot tier only; archived documents are not
1243
+ * included. See {@link CollectionRef.find} and `db.timeline.query(...)`.
1139
1244
  */
1140
1245
  search(opts: SearchOptions): Promise<SearchResult<T>>;
1246
+ /**
1247
+ * Fetch a single document by `_id` from the **hot tier**.
1248
+ *
1249
+ * **Cold-tier note:** returns null/throws for documents that have been
1250
+ * archived and purged from the hot tier. Archived history is reachable only
1251
+ * via `db.timeline.query(...)`. See {@link CollectionRef.find}.
1252
+ */
1141
1253
  findById(id: string, options?: FindByIdOptions): Promise<T & {
1142
1254
  _id: string;
1143
1255
  }>;
1144
1256
  update(id: string, update: Partial<T>): Promise<UpdateResult>;
1145
1257
  delete(id: string): Promise<DeleteResult>;
1258
+ /**
1259
+ * Count documents in the **hot tier** matching `filter`.
1260
+ *
1261
+ * **Cold-tier note:** counts hot-tier documents only — archived/purged
1262
+ * documents are not included, so this is not a total-history count. See
1263
+ * {@link CollectionRef.find} and `db.timeline.query(...)`.
1264
+ */
1146
1265
  count(filter?: Record<string, unknown>): Promise<number>;
1147
1266
  subscribe(handlers: SubscribeHandlers<T>): () => void;
1148
1267
  subscribeEvents(handlers: SubscribeEventsHandlers<T>): StreamSubscription;
@@ -1161,13 +1280,20 @@ declare class DatabaseModule {
1161
1280
  private http;
1162
1281
  private realtime;
1163
1282
  private projectId;
1283
+ readonly timeline: TimelineModule;
1164
1284
  constructor(http: HttpClient, projectId: string, realtime?: RealtimeClient);
1165
1285
  collection<T = Record<string, unknown>>(name: string): CollectionRef<T>;
1166
1286
  }
1167
1287
 
1288
+ /**
1289
+ * Push subscription platform. `web` = VAPID WebPush; `android`/`ios` = native
1290
+ * device tokens; `web-fcm` = a web Firebase Cloud Messaging registration token.
1291
+ * Defined locally so `@spacelr/sdk` stays a standalone, dependency-free package.
1292
+ */
1293
+ type PushPlatform = 'web' | 'android' | 'ios' | 'web-fcm';
1168
1294
  interface PushSubscriptionInfo {
1169
1295
  id: string;
1170
- platform: 'web' | 'android' | 'ios';
1296
+ platform: PushPlatform;
1171
1297
  endpoint?: string;
1172
1298
  deviceToken?: string;
1173
1299
  deviceId?: string;
@@ -1198,9 +1324,9 @@ declare class NotificationsModule {
1198
1324
  private detectDeviceName;
1199
1325
  /** Get the VAPID public key for Web Push setup */
1200
1326
  getVapidPublicKey(): Promise<VapidKeyResponse>;
1201
- /** Register a push subscription (web, android, or ios) */
1327
+ /** Register a push subscription (web, android, ios, or web-fcm) */
1202
1328
  subscribe(subscription: {
1203
- platform: 'web' | 'android' | 'ios';
1329
+ platform: PushPlatform;
1204
1330
  endpoint?: string;
1205
1331
  keys?: {
1206
1332
  p256dh: string;
@@ -1209,7 +1335,7 @@ declare class NotificationsModule {
1209
1335
  deviceToken?: string;
1210
1336
  }, deviceName?: string): Promise<PushSubscriptionInfo>;
1211
1337
  /** Unregister a push subscription */
1212
- unsubscribe(platform: 'web' | 'android' | 'ios', identifier: string): Promise<{
1338
+ unsubscribe(platform: PushPlatform, identifier: string): Promise<{
1213
1339
  deleted: boolean;
1214
1340
  }>;
1215
1341
  /** Get all subscriptions for the current user */
@@ -1224,6 +1350,12 @@ declare class NotificationsModule {
1224
1350
  * Helper: Register a mobile device push token (FCM or APNs).
1225
1351
  */
1226
1352
  registerDevicePush(deviceToken: string, platform: 'android' | 'ios', deviceName?: string): Promise<PushSubscriptionInfo>;
1353
+ /**
1354
+ * Registers a web Firebase Cloud Messaging registration token (obtained via
1355
+ * the Firebase JS SDK `getToken()`), so `notifications.send` delivers to this
1356
+ * browser through the project's FCM provider instead of VAPID WebPush.
1357
+ */
1358
+ registerWebFcm(fcmToken: string, deviceName?: string): Promise<PushSubscriptionInfo>;
1227
1359
  private urlBase64ToUint8Array;
1228
1360
  }
1229
1361
 
@@ -1334,6 +1466,55 @@ declare class ScheduleModule {
1334
1466
  private toIsoString;
1335
1467
  }
1336
1468
 
1469
+ /**
1470
+ * Base class for all timeline-related errors thrown by the SDK. Extends
1471
+ * SpacelrError so it fits the SDK's existing `catch (e instanceof
1472
+ * SpacelrError)` pattern. Subclasses give callers fine-grained control
1473
+ * (e.g. catch only `CursorInvalidError` and restart pagination).
1474
+ */
1475
+ declare class TimelineError extends SpacelrError {
1476
+ constructor(message: string, opts?: {
1477
+ statusCode?: number;
1478
+ code?: string;
1479
+ });
1480
+ }
1481
+ declare class CursorInvalidError extends TimelineError {
1482
+ constructor(message: string, opts?: {
1483
+ statusCode?: number;
1484
+ code?: string;
1485
+ });
1486
+ }
1487
+ declare class ValidationError extends TimelineError {
1488
+ constructor(message: string, opts?: {
1489
+ statusCode?: number;
1490
+ code?: string;
1491
+ });
1492
+ }
1493
+ declare class ForbiddenError extends TimelineError {
1494
+ constructor(message: string, opts?: {
1495
+ statusCode?: number;
1496
+ code?: string;
1497
+ });
1498
+ }
1499
+ declare class NotFoundError extends TimelineError {
1500
+ constructor(message: string, opts?: {
1501
+ statusCode?: number;
1502
+ code?: string;
1503
+ });
1504
+ }
1505
+ declare class ServerConfigError extends TimelineError {
1506
+ constructor(message: string, opts?: {
1507
+ statusCode?: number;
1508
+ code?: string;
1509
+ });
1510
+ }
1511
+ declare class TimeoutError extends TimelineError {
1512
+ constructor(message: string, opts?: {
1513
+ statusCode?: number;
1514
+ code?: string;
1515
+ });
1516
+ }
1517
+
1337
1518
  interface SpacelrClient {
1338
1519
  readonly auth: AuthModule;
1339
1520
  readonly storage: StorageModule;
@@ -1392,4 +1573,4 @@ interface SpacelrClient {
1392
1573
  }
1393
1574
  declare function createClient(config: SpacelrClientConfig): SpacelrClient;
1394
1575
 
1395
- export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type Schedule, type ScheduleInvokeOptions, type ScheduleListOptions, type ScheduleStatus, type SearchOptions, 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 TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
1576
+ 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 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 };
package/dist/index.d.ts CHANGED
@@ -738,6 +738,98 @@ declare class StorageModule {
738
738
  getPublicFileUrl(fileId: string, projectId?: string): Promise<string>;
739
739
  }
740
740
 
741
+ /**
742
+ * Scalar values permitted inside a timeline filter. Matches the server
743
+ * filter allow-list (libs/shared-dtos/.../timeline-filter.dto.ts).
744
+ */
745
+ type TimelineScalar = string | number | boolean | null;
746
+ /**
747
+ * Single-field filter expression. Either implicit equality (a scalar) or
748
+ * an explicit operator object. Mongo-style allow-list — the server rejects
749
+ * anything outside this set.
750
+ */
751
+ type TimelineLeafFilter = TimelineScalar | {
752
+ $eq?: TimelineScalar;
753
+ $ne?: TimelineScalar;
754
+ $lt?: TimelineScalar;
755
+ $lte?: TimelineScalar;
756
+ $gt?: TimelineScalar;
757
+ $gte?: TimelineScalar;
758
+ $in?: TimelineScalar[];
759
+ $nin?: TimelineScalar[];
760
+ };
761
+ /**
762
+ * Filter document for a timeline query. Top-level fields apply implicit
763
+ * equality (or operator expression). $and supports nested filter
764
+ * composition; $or / $regex / etc. are intentionally NOT exposed and
765
+ * will be rejected server-side.
766
+ *
767
+ * Two named branches in the union so TypeScript doesn't treat `$and` as
768
+ * just another key under the field index signature — the array shape
769
+ * needs its own type or downstream usage breaks.
770
+ */
771
+ interface TimelineAndFilter {
772
+ $and: TimelineFilter[];
773
+ }
774
+ type TimelineFieldFilter = Record<string, TimelineLeafFilter>;
775
+ type TimelineFilter = TimelineAndFilter | TimelineFieldFilter;
776
+ interface TimelineOrderBy {
777
+ field: string;
778
+ direction: 'asc' | 'desc';
779
+ }
780
+ interface TimelineQueryOptions {
781
+ /** Collection name to read from. */
782
+ collection: string;
783
+ /** Partition value (e.g. roomId, documentId — collection-configured). */
784
+ partitionValue: string;
785
+ /** Optional filter constraints (allow-listed operators only). */
786
+ where?: TimelineFilter;
787
+ /**
788
+ * Sort order. Defaults to DESC by the collection's configured timeline
789
+ * field; PR A only supports DESC. Passing direction='asc' is reserved
790
+ * for a future phase and currently returns BAD_REQUEST.
791
+ */
792
+ orderBy?: TimelineOrderBy;
793
+ /** Page size (1..200). Default 50. */
794
+ limit?: number;
795
+ /**
796
+ * Opaque cursor from a previous response. The SDK does NOT decode this —
797
+ * pass the value of `nextCursor` from the last response verbatim.
798
+ */
799
+ cursor?: string;
800
+ }
801
+ interface TimelineSourceStats {
802
+ hot: number;
803
+ cold: number;
804
+ /** Number of cold archive segments scanned (only present when cold path ran). */
805
+ segmentsScanned?: number;
806
+ }
807
+ interface TimelineQueryResponse {
808
+ items: Record<string, unknown>[];
809
+ /** Opaque continuation token. `null` when the page is the last one. */
810
+ nextCursor: string | null;
811
+ sourceStats: TimelineSourceStats;
812
+ }
813
+
814
+ /**
815
+ * Low-level timeline SDK module. One method: query(opts) → page.
816
+ *
817
+ * The cursor is opaque from the SDK's perspective — it's stored as a
818
+ * string, never decoded, never inspected. Callers pass the value of
819
+ * `nextCursor` from the previous response verbatim into the next request.
820
+ *
821
+ * Errors thrown from query() extend SpacelrError so they integrate with
822
+ * the SDK's existing error hierarchy. Use one of the typed subclasses
823
+ * (CursorInvalidError, ForbiddenError, etc.) to catch specific cases or
824
+ * `catch (e instanceof TimelineError)` for any timeline-related failure.
825
+ */
826
+ declare class TimelineModule {
827
+ private readonly http;
828
+ constructor(http: HttpClient);
829
+ query(opts: TimelineQueryOptions): Promise<TimelineQueryResponse>;
830
+ private translateError;
831
+ }
832
+
741
833
  interface PopulateOption {
742
834
  field: string;
743
835
  collection: string;
@@ -1106,6 +1198,16 @@ declare class CollectionRef<T = Record<string, unknown>> {
1106
1198
  insertMany(documents: (Partial<T> & {
1107
1199
  _id?: string;
1108
1200
  })[]): Promise<InsertResult>;
1201
+ /**
1202
+ * Query the collection's **hot tier** (live MongoDB).
1203
+ *
1204
+ * **Cold-tier note:** if the collection has cold-tier archival enabled, aged
1205
+ * documents are moved to object storage and purged from the hot tier — they
1206
+ * will NOT appear in `find()` results (nor `findById`, `count`, `search`,
1207
+ * `paginate`, which are all hot-tier only). To read archived history, use
1208
+ * {@link TimelineModule.query} via `db.timeline.query(...)`, which transparently
1209
+ * merges hot and cold results for a partition.
1210
+ */
1109
1211
  find(filter?: Record<string, unknown>): QueryBuilder<T>;
1110
1212
  /**
1111
1213
  * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`
@@ -1136,13 +1238,30 @@ declare class CollectionRef<T = Record<string, unknown>> {
1136
1238
  *
1137
1239
  * Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching
1138
1240
  * `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.
1241
+ *
1242
+ * **Cold-tier note:** searches the hot tier only; archived documents are not
1243
+ * included. See {@link CollectionRef.find} and `db.timeline.query(...)`.
1139
1244
  */
1140
1245
  search(opts: SearchOptions): Promise<SearchResult<T>>;
1246
+ /**
1247
+ * Fetch a single document by `_id` from the **hot tier**.
1248
+ *
1249
+ * **Cold-tier note:** returns null/throws for documents that have been
1250
+ * archived and purged from the hot tier. Archived history is reachable only
1251
+ * via `db.timeline.query(...)`. See {@link CollectionRef.find}.
1252
+ */
1141
1253
  findById(id: string, options?: FindByIdOptions): Promise<T & {
1142
1254
  _id: string;
1143
1255
  }>;
1144
1256
  update(id: string, update: Partial<T>): Promise<UpdateResult>;
1145
1257
  delete(id: string): Promise<DeleteResult>;
1258
+ /**
1259
+ * Count documents in the **hot tier** matching `filter`.
1260
+ *
1261
+ * **Cold-tier note:** counts hot-tier documents only — archived/purged
1262
+ * documents are not included, so this is not a total-history count. See
1263
+ * {@link CollectionRef.find} and `db.timeline.query(...)`.
1264
+ */
1146
1265
  count(filter?: Record<string, unknown>): Promise<number>;
1147
1266
  subscribe(handlers: SubscribeHandlers<T>): () => void;
1148
1267
  subscribeEvents(handlers: SubscribeEventsHandlers<T>): StreamSubscription;
@@ -1161,13 +1280,20 @@ declare class DatabaseModule {
1161
1280
  private http;
1162
1281
  private realtime;
1163
1282
  private projectId;
1283
+ readonly timeline: TimelineModule;
1164
1284
  constructor(http: HttpClient, projectId: string, realtime?: RealtimeClient);
1165
1285
  collection<T = Record<string, unknown>>(name: string): CollectionRef<T>;
1166
1286
  }
1167
1287
 
1288
+ /**
1289
+ * Push subscription platform. `web` = VAPID WebPush; `android`/`ios` = native
1290
+ * device tokens; `web-fcm` = a web Firebase Cloud Messaging registration token.
1291
+ * Defined locally so `@spacelr/sdk` stays a standalone, dependency-free package.
1292
+ */
1293
+ type PushPlatform = 'web' | 'android' | 'ios' | 'web-fcm';
1168
1294
  interface PushSubscriptionInfo {
1169
1295
  id: string;
1170
- platform: 'web' | 'android' | 'ios';
1296
+ platform: PushPlatform;
1171
1297
  endpoint?: string;
1172
1298
  deviceToken?: string;
1173
1299
  deviceId?: string;
@@ -1198,9 +1324,9 @@ declare class NotificationsModule {
1198
1324
  private detectDeviceName;
1199
1325
  /** Get the VAPID public key for Web Push setup */
1200
1326
  getVapidPublicKey(): Promise<VapidKeyResponse>;
1201
- /** Register a push subscription (web, android, or ios) */
1327
+ /** Register a push subscription (web, android, ios, or web-fcm) */
1202
1328
  subscribe(subscription: {
1203
- platform: 'web' | 'android' | 'ios';
1329
+ platform: PushPlatform;
1204
1330
  endpoint?: string;
1205
1331
  keys?: {
1206
1332
  p256dh: string;
@@ -1209,7 +1335,7 @@ declare class NotificationsModule {
1209
1335
  deviceToken?: string;
1210
1336
  }, deviceName?: string): Promise<PushSubscriptionInfo>;
1211
1337
  /** Unregister a push subscription */
1212
- unsubscribe(platform: 'web' | 'android' | 'ios', identifier: string): Promise<{
1338
+ unsubscribe(platform: PushPlatform, identifier: string): Promise<{
1213
1339
  deleted: boolean;
1214
1340
  }>;
1215
1341
  /** Get all subscriptions for the current user */
@@ -1224,6 +1350,12 @@ declare class NotificationsModule {
1224
1350
  * Helper: Register a mobile device push token (FCM or APNs).
1225
1351
  */
1226
1352
  registerDevicePush(deviceToken: string, platform: 'android' | 'ios', deviceName?: string): Promise<PushSubscriptionInfo>;
1353
+ /**
1354
+ * Registers a web Firebase Cloud Messaging registration token (obtained via
1355
+ * the Firebase JS SDK `getToken()`), so `notifications.send` delivers to this
1356
+ * browser through the project's FCM provider instead of VAPID WebPush.
1357
+ */
1358
+ registerWebFcm(fcmToken: string, deviceName?: string): Promise<PushSubscriptionInfo>;
1227
1359
  private urlBase64ToUint8Array;
1228
1360
  }
1229
1361
 
@@ -1334,6 +1466,55 @@ declare class ScheduleModule {
1334
1466
  private toIsoString;
1335
1467
  }
1336
1468
 
1469
+ /**
1470
+ * Base class for all timeline-related errors thrown by the SDK. Extends
1471
+ * SpacelrError so it fits the SDK's existing `catch (e instanceof
1472
+ * SpacelrError)` pattern. Subclasses give callers fine-grained control
1473
+ * (e.g. catch only `CursorInvalidError` and restart pagination).
1474
+ */
1475
+ declare class TimelineError extends SpacelrError {
1476
+ constructor(message: string, opts?: {
1477
+ statusCode?: number;
1478
+ code?: string;
1479
+ });
1480
+ }
1481
+ declare class CursorInvalidError extends TimelineError {
1482
+ constructor(message: string, opts?: {
1483
+ statusCode?: number;
1484
+ code?: string;
1485
+ });
1486
+ }
1487
+ declare class ValidationError extends TimelineError {
1488
+ constructor(message: string, opts?: {
1489
+ statusCode?: number;
1490
+ code?: string;
1491
+ });
1492
+ }
1493
+ declare class ForbiddenError extends TimelineError {
1494
+ constructor(message: string, opts?: {
1495
+ statusCode?: number;
1496
+ code?: string;
1497
+ });
1498
+ }
1499
+ declare class NotFoundError extends TimelineError {
1500
+ constructor(message: string, opts?: {
1501
+ statusCode?: number;
1502
+ code?: string;
1503
+ });
1504
+ }
1505
+ declare class ServerConfigError extends TimelineError {
1506
+ constructor(message: string, opts?: {
1507
+ statusCode?: number;
1508
+ code?: string;
1509
+ });
1510
+ }
1511
+ declare class TimeoutError extends TimelineError {
1512
+ constructor(message: string, opts?: {
1513
+ statusCode?: number;
1514
+ code?: string;
1515
+ });
1516
+ }
1517
+
1337
1518
  interface SpacelrClient {
1338
1519
  readonly auth: AuthModule;
1339
1520
  readonly storage: StorageModule;
@@ -1392,4 +1573,4 @@ interface SpacelrClient {
1392
1573
  }
1393
1574
  declare function createClient(config: SpacelrClientConfig): SpacelrClient;
1394
1575
 
1395
- export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type Schedule, type ScheduleInvokeOptions, type ScheduleListOptions, type ScheduleStatus, type SearchOptions, 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 TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
1576
+ 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 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 };