@lacrous/omnicloud 0.1.1 → 0.2.1

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.ts CHANGED
@@ -1,14 +1,84 @@
1
+ import { Readable } from 'node:stream';
1
2
  import { TelegramClient } from 'telegram';
2
3
 
4
+ /**
5
+ * Search query parsing, pagination and sorting primitives.
6
+ *
7
+ * Self-contained (no runtime imports from the package root) so the API, web
8
+ * app and SDK can all share one query language without import cycles.
9
+ *
10
+ * Query language (v0.2):
11
+ * report → name contains "report"
12
+ * name:report → same, explicit
13
+ * type:pdf type:image type:application/zip
14
+ * ext:zip → file extension (no dot)
15
+ * size:>100MB size:<1KB size:5mb
16
+ * folder:Projects → folder-name filter
17
+ * starred:true trashed:true
18
+ * after:2026-01-01 before:2026-06-01
19
+ */
20
+ declare const SORT_FIELDS: readonly ["name", "size", "createdAt", "updatedAt", "type"];
21
+ type SortField = (typeof SORT_FIELDS)[number];
22
+ declare const SORT_ORDERS: readonly ["asc", "desc"];
23
+ type SortOrder = (typeof SORT_ORDERS)[number];
24
+ declare const ITEM_STATUSES: readonly ["active", "trashed", "all"];
25
+ type ItemStatus = (typeof ITEM_STATUSES)[number];
26
+ /** Query parameters accepted by the file/folder listing endpoints. */
27
+ interface ListQuery {
28
+ folderId?: string | null;
29
+ page?: number;
30
+ limit?: number;
31
+ sort?: SortField;
32
+ order?: SortOrder;
33
+ /** MIME type or coarse category (image, video, pdf, document, …). */
34
+ type?: string;
35
+ /** File extension without the dot. */
36
+ ext?: string;
37
+ starred?: boolean;
38
+ status?: ItemStatus;
39
+ /** Name substring filter. */
40
+ q?: string;
41
+ /** Folder-name filter (search parser); resolved to ids by the caller. */
42
+ folderName?: string;
43
+ minSize?: number;
44
+ maxSize?: number;
45
+ /** Inclusive date bounds (ISO strings). */
46
+ from?: string;
47
+ to?: string;
48
+ }
49
+ /** Parses "100MB", "1.5gb", "512" into bytes. Returns null when unparseable. */
50
+ declare function parseSize(value: string): number | null;
51
+ /** Parses a raw user query string into structured filters. */
52
+ declare function parseSearchQuery(input: string, base?: ListQuery): {
53
+ query: ListQuery;
54
+ freeText: string;
55
+ };
56
+ /** Builds the pagination envelope for a result page. */
57
+ declare function paginationMeta(page: number, limit: number, total: number): {
58
+ page: number;
59
+ limit: number;
60
+ total: number;
61
+ hasMore: boolean;
62
+ };
63
+
3
64
  /** Derives a MIME type from the filename extension. Never trusts client input. */
4
65
  declare function mimeFromFilename(filename: string): string;
5
66
  type FileCategory = "image" | "video" | "audio" | "pdf" | "archive" | "document" | "text" | "other";
6
67
  /** Coarse category used for icon selection in the UI. */
7
68
  declare function fileCategory(mimeType: string): FileCategory;
69
+ declare function isFileCategory(value: string): value is FileCategory;
70
+ /**
71
+ * Expands a `type:` filter into a set of MIME matchers.
72
+ *
73
+ * Accepts either a coarse category ("image", "pdf") or an exact MIME type
74
+ * ("application/pdf"); the repository layer turns these into prefix/equality
75
+ * filters. Returns null when the value matches nothing usable.
76
+ */
77
+ declare function mimeMatchersForType(value: string): readonly string[] | null;
8
78
 
9
79
  /**
10
- * Data transfer objects and constants shared between the API, the web app,
11
- * and the @lacrous/omnicloud SDK.
80
+ * Data transfer objects, constants and error codes shared between the API,
81
+ * the web app, and the @lacrous/omnicloud SDK.
12
82
  *
13
83
  * Telegram identifiers that may exceed JavaScript's safe integer range are
14
84
  * transported as strings across the API boundary.
@@ -25,54 +95,197 @@ interface StorageDTO {
25
95
  title: string;
26
96
  createdAt: string;
27
97
  }
98
+ /** Connection state of the underlying storage backend. */
99
+ type ConnectionState$1 = "DISCONNECTED" | "CONNECTING" | "CONNECTED" | "RECONNECTING" | "ERROR" | "AUTH_REQUIRED";
100
+ type HealthStatus = "healthy" | "degraded" | "unavailable" | "unknown";
101
+ interface StorageHealthDTO {
102
+ provider: string;
103
+ state: ConnectionState$1;
104
+ status: HealthStatus;
105
+ latencyMs: number | null;
106
+ message: string | null;
107
+ channelTitle: string | null;
108
+ checkedAt: string;
109
+ }
28
110
  /** Returned by GET /api/auth/me. `user` is null when not signed in. */
29
111
  interface SessionInfo {
30
112
  user: UserDTO | null;
31
113
  storage: StorageDTO | null;
114
+ health: StorageHealthDTO | null;
32
115
  }
33
- interface FileDTO {
116
+ /** POST /api/auth/telegram/verify response. */
117
+ type TelegramVerifyResponse = {
118
+ status: "ok";
119
+ user: UserDTO;
120
+ } | {
121
+ status: "password_required";
122
+ };
123
+ interface FileDTO$1 {
34
124
  id: string;
35
125
  name: string;
36
126
  size: number;
37
127
  mimeType: string;
38
128
  sha256: string;
39
129
  folderId: string | null;
130
+ starred: boolean;
131
+ trashed: boolean;
132
+ deletedAt: string | null;
133
+ versionCount: number;
134
+ currentVersionId: string | null;
40
135
  createdAt: string;
41
136
  updatedAt: string;
42
137
  }
43
- interface FolderDTO {
138
+ interface FolderDTO$1 {
44
139
  id: string;
45
140
  name: string;
46
141
  parentId: string | null;
142
+ starred: boolean;
143
+ trashed: boolean;
144
+ deletedAt: string | null;
47
145
  createdAt: string;
48
146
  updatedAt: string;
49
147
  }
50
- interface SearchResultDTO {
51
- files: FileDTO[];
52
- folders: FolderDTO[];
148
+ interface FileVersionDTO {
149
+ id: string;
150
+ fileId: string;
151
+ versionNumber: number;
152
+ size: number;
153
+ mimeType: string;
154
+ sha256: string;
155
+ telegramMessageId: number;
156
+ isCurrent: boolean;
157
+ createdAt: string;
53
158
  }
54
- interface DeleteFolderResultDTO {
55
- deletedFolders: number;
159
+ interface SearchResultDTO$1 {
160
+ files: FileDTO$1[];
161
+ folders: FolderDTO$1[];
162
+ pagination: PaginationDTO$1;
163
+ }
164
+ /** Result of a recursive folder deletion / trash operation. */
165
+ interface FolderMutationResultDTO {
166
+ affectedFolders: number;
167
+ affectedFiles: number;
168
+ }
169
+ interface EmptyTrashResultDTO {
56
170
  deletedFiles: number;
171
+ deletedFolders: number;
172
+ failedFiles: number;
173
+ }
174
+
175
+ interface PaginationDTO$1 {
176
+ page: number;
177
+ limit: number;
178
+ total: number;
179
+ hasMore: boolean;
180
+ }
181
+ interface FilesPageDTO {
182
+ files: FileDTO$1[];
183
+ pagination: PaginationDTO$1;
184
+ }
185
+ interface FoldersPageDTO {
186
+ folders: FolderDTO$1[];
187
+ pagination: PaginationDTO$1;
188
+ }
189
+ interface TrashPageDTO {
190
+ files: FileDTO$1[];
191
+ folders: FolderDTO$1[];
192
+ pagination: PaginationDTO$1;
193
+ }
194
+ interface RecentItemDTO$1 {
195
+ file: FileDTO$1;
196
+ lastAction: ActivityAction$1;
197
+ lastActionAt: string;
198
+ }
199
+ interface RecentPageDTO$1 {
200
+ items: RecentItemDTO$1[];
201
+ pagination: PaginationDTO$1;
202
+ }
203
+ declare const ACTIVITY_ACTIONS$1: readonly ["upload", "download", "open", "rename", "move", "star", "unstar", "trash", "restore", "delete", "create_folder", "replace"];
204
+ type ActivityAction$1 = (typeof ACTIVITY_ACTIONS$1)[number];
205
+ declare const RESOURCE_TYPES$1: readonly ["file", "folder", "storage"];
206
+ type ResourceType$1 = (typeof RESOURCE_TYPES$1)[number];
207
+ interface ActivityEventDTO {
208
+ id: string;
209
+ action: ActivityAction$1;
210
+ resourceType: ResourceType$1;
211
+ resourceId: string;
212
+ resourceName: string | null;
213
+ metadata: Record<string, unknown> | null;
214
+ createdAt: string;
215
+ }
216
+ interface ActivityPageDTO {
217
+ events: ActivityEventDTO[];
218
+ pagination: PaginationDTO$1;
219
+ }
220
+ interface StorageTypeBreakdownDTO {
221
+ category: string;
222
+ bytes: number;
223
+ count: number;
224
+ }
225
+ interface StorageStatsDTO {
226
+ fileCount: number;
227
+ folderCount: number;
228
+ totalBytes: number;
229
+ trashBytes: number;
230
+ trashFileCount: number;
231
+ starredCount: number;
232
+ byType: StorageTypeBreakdownDTO[];
233
+ largestFiles: FileDTO$1[];
234
+ quotaBytes: number | null;
235
+ }
236
+ interface HealthDTO {
237
+ status: HealthStatus;
238
+ database: HealthStatus;
239
+ storage: HealthStatus;
240
+ uptimeSeconds: number;
241
+ version: string;
242
+ }
243
+ interface IntegrityIssueDTO$1 {
244
+ fileId: string;
245
+ name: string;
246
+ kind: "missing" | "size_mismatch" | "unreadable" | "hash_mismatch";
247
+ detail: string | null;
248
+ }
249
+ interface IntegrityReportDTO$1 {
250
+ checkedAt: string;
251
+ filesChecked: number;
252
+ healthy: number;
253
+ missing: number;
254
+ inconsistent: number;
255
+ unreadable: number;
256
+ issues: IntegrityIssueDTO$1[];
257
+ durationMs: number;
258
+ }
259
+ interface BatchResultDTO {
260
+ requested: number;
261
+ succeeded: number;
262
+ failed: number;
263
+ errors: {
264
+ id: string;
265
+ code: ErrorCode$1;
266
+ message: string;
267
+ }[];
57
268
  }
58
- /** POST /api/auth/telegram/verify response. */
59
- type TelegramVerifyResponse = {
60
- status: "ok";
61
- user: UserDTO;
62
- } | {
63
- status: "password_required";
64
- };
65
269
  /** Stable machine-readable error codes returned in API error bodies. */
66
270
  declare const ERROR_CODES$1: {
67
- readonly VALIDATION: "VALIDATION_ERROR";
68
- readonly UNAUTHENTICATED: "UNAUTHENTICATED";
69
- readonly FORBIDDEN: "FORBIDDEN";
271
+ readonly AUTH_REQUIRED: "AUTH_REQUIRED";
272
+ readonly AUTH_INVALID: "AUTH_INVALID";
273
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
274
+ readonly INVALID_REQUEST: "INVALID_REQUEST";
70
275
  readonly NOT_FOUND: "NOT_FOUND";
276
+ readonly FILE_NOT_FOUND: "FILE_NOT_FOUND";
277
+ readonly FOLDER_NOT_FOUND: "FOLDER_NOT_FOUND";
71
278
  readonly CONFLICT: "CONFLICT";
72
279
  readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
73
280
  readonly RATE_LIMITED: "RATE_LIMITED";
74
281
  readonly STORAGE_NOT_INITIALIZED: "STORAGE_NOT_INITIALIZED";
75
- readonly TELEGRAM_ERROR: "TELEGRAM_ERROR";
282
+ readonly STORAGE_UNAVAILABLE: "STORAGE_UNAVAILABLE";
283
+ readonly TELEGRAM_AUTH_REQUIRED: "TELEGRAM_AUTH_REQUIRED";
284
+ readonly TELEGRAM_CONNECTION_FAILED: "TELEGRAM_CONNECTION_FAILED";
285
+ readonly TELEGRAM_FILE_NOT_FOUND: "TELEGRAM_FILE_NOT_FOUND";
286
+ readonly UPLOAD_FAILED: "UPLOAD_FAILED";
287
+ readonly DOWNLOAD_FAILED: "DOWNLOAD_FAILED";
288
+ readonly INTEGRITY_CHECK_FAILED: "INTEGRITY_CHECK_FAILED";
76
289
  readonly INTERNAL: "INTERNAL_ERROR";
77
290
  };
78
291
  type ErrorCode$1 = (typeof ERROR_CODES$1)[keyof typeof ERROR_CODES$1];
@@ -81,19 +294,38 @@ interface ApiErrorBody {
81
294
  code: ErrorCode$1;
82
295
  message: string;
83
296
  details?: unknown;
297
+ requestId?: string;
84
298
  };
85
299
  }
86
300
 
301
+ /**
302
+ * Errors surfaced by the SDK. `code` mirrors the server's stable error codes
303
+ * and `requestId` correlates with server logs.
304
+ */
87
305
  declare class OmniCloudError extends Error {
88
306
  readonly status: number;
89
307
  readonly code: string;
90
- constructor(status: number, code: string, message: string);
308
+ readonly requestId: string | undefined;
309
+ readonly details: unknown;
310
+ constructor(status: number, code: string, message: string, requestId?: string | undefined, details?: unknown);
311
+ }
312
+ interface RetryOptions {
313
+ /** Total attempts including the first. Default 3. */
314
+ attempts?: number;
315
+ /** Base backoff in ms (doubles per retry). Default 300. */
316
+ baseDelayMs?: number;
91
317
  }
92
318
  interface OmniCloudClientOptions {
93
- /** Base URL of an OmniCloud server. Defaults to same-origin ("" is kept as-is). */
319
+ /** Base URL of an OmniCloud server (e.g. "https://cloud.example"). */
94
320
  baseUrl?: string;
95
- /** Custom fetch implementation (e.g. for tests). */
321
+ /** Bearer token, when the deployment uses token auth instead of cookies. */
322
+ token?: string;
323
+ /** Custom fetch implementation (tests, polyfills). */
96
324
  fetchImpl?: typeof fetch;
325
+ /** Retry policy for transient failures. */
326
+ retry?: RetryOptions;
327
+ /** Extra headers sent with every request. */
328
+ headers?: Record<string, string>;
97
329
  }
98
330
  interface UploadProgress {
99
331
  loaded: number;
@@ -103,22 +335,175 @@ interface UploadProgress {
103
335
  interface UploadOptions {
104
336
  folderId?: string | null;
105
337
  onProgress?: (progress: UploadProgress) => void;
338
+ /** Aborts the upload. */
339
+ signal?: AbortSignal;
340
+ /** Number of retry attempts for failed uploads. Default 2. */
341
+ retry?: number;
106
342
  }
107
343
  interface UploadInput {
108
- /** File content: a File/Blob in the browser, or a Buffer/Uint8Array in Node. */
344
+ /** File content: Blob/File in the browser, Buffer/Uint8Array in Node. */
109
345
  data: Blob | Uint8Array;
346
+ /** Filename; also used to derive the MIME type server-side. */
110
347
  name: string;
111
348
  }
349
+ interface DownloadProgress {
350
+ loaded: number;
351
+ total: number | null;
352
+ percent: number | null;
353
+ }
354
+ interface DownloadResult {
355
+ data: Buffer | Uint8Array;
356
+ contentType: string;
357
+ size: number;
358
+ sha256: string | null;
359
+ integrityVerified: boolean | null;
360
+ }
112
361
  /**
113
- * HTTP client for an OmniCloud server.
362
+ * Naming: every endpoint group is a namespaced sub-client so calls read like
363
+ *
364
+ * cloud.files.list({ folderId })
365
+ * cloud.trash.empty()
366
+ * cloud.storage.stats()
114
367
  *
115
- * Works in browsers (cookie sessions, upload progress via XHR) and Node.js
116
- * (fetch fallback, cookie forwarding is the caller's responsibility).
368
+ * Works in browsers (cookie sessions, XHR upload progress) and Node
369
+ * (fetch, Buffer downloads).
117
370
  */
118
371
  declare class OmniCloudClient {
119
- private readonly baseUrl;
120
- private readonly fetchImpl;
372
+ readonly files: FilesApi;
373
+ readonly folders: FoldersApi;
374
+ readonly trash: TrashApi;
375
+ readonly storage: StorageApi;
376
+ readonly search: SearchApi;
377
+ readonly recent: RecentApi;
378
+ readonly activity: ActivityApi;
379
+ readonly auth: AuthApi;
380
+ /** @internal */
381
+ readonly baseUrl: string;
382
+ /** @internal */
383
+ readonly fetchImpl: typeof fetch;
384
+ /** @internal */
385
+ readonly retry: Required<RetryOptions>;
386
+ /** @internal */
387
+ readonly defaultHeaders: Record<string, string>;
121
388
  constructor(options?: OmniCloudClientOptions);
389
+ /** Low-level request helper (retries transient failures). */
390
+ request<T>(method: string, path: string, body?: unknown): Promise<T>;
391
+ /** @internal */
392
+ rawRequest<T>(method: string, path: string, body?: unknown): Promise<T>;
393
+ /** @internal */
394
+ withRetry<T>(operation: () => Promise<T>): Promise<T>;
395
+ /** @internal */
396
+ requestBinary(path: string, onProgress?: (p: DownloadProgress) => void): Promise<DownloadResult>;
397
+ }
398
+ declare class FilesApi {
399
+ private readonly client;
400
+ constructor(client: OmniCloudClient);
401
+ /** `folderId: null` = root; omit it to list every active file. */
402
+ list(query?: ListQuery): Promise<FilesPageDTO>;
403
+ get(id: string): Promise<FileDTO$1>;
404
+ versions(id: string): Promise<FileVersionDTO[]>;
405
+ /**
406
+ * Uploads a file with progress and retry support. Uses XMLHttpRequest when
407
+ * available (browsers) so progress and cancellation work; falls back to
408
+ * fetch elsewhere.
409
+ */
410
+ upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO$1>;
411
+ private uploadOnce;
412
+ private uploadViaXhr;
413
+ /** Uploads a new version of an existing file. */
414
+ replace(id: string, input: UploadInput): Promise<FileDTO$1>;
415
+ download(id: string, onProgress?: (p: DownloadProgress) => void): Promise<DownloadResult>;
416
+ /** URL suitable for direct download (anchor href). */
417
+ downloadUrl(id: string): string;
418
+ rename(id: string, name: string): Promise<FileDTO$1>;
419
+ star(id: string, starred?: boolean): Promise<FileDTO$1>;
420
+ move(id: string, folderId: string | null): Promise<FileDTO$1>;
421
+ trash(id: string): Promise<FileDTO$1>;
422
+ restore(id: string): Promise<FileDTO$1>;
423
+ /** Permanently deletes a file and its Telegram objects. */
424
+ delete(id: string): Promise<void>;
425
+ /** Batch operation: trash | restore | delete | star | unstar | move. */
426
+ batch(operation: "trash" | "restore" | "delete" | "star" | "unstar" | "move", ids: string[], options?: {
427
+ folderId?: string | null;
428
+ }): Promise<BatchResultDTO>;
429
+ }
430
+ declare class FoldersApi {
431
+ private readonly client;
432
+ constructor(client: OmniCloudClient);
433
+ list(query?: ListQuery & {
434
+ parentId?: string | null;
435
+ }): Promise<FoldersPageDTO>;
436
+ /** The full active folder tree (for move dialogs). */
437
+ tree(): Promise<FolderDTO$1[]>;
438
+ create(name: string, parentId?: string | null): Promise<FolderDTO$1>;
439
+ rename(id: string, name: string): Promise<FolderDTO$1>;
440
+ star(id: string, starred?: boolean): Promise<FolderDTO$1>;
441
+ move(id: string, parentId: string | null): Promise<FolderDTO$1>;
442
+ trash(id: string): Promise<FolderMutationResultDTO>;
443
+ restore(id: string): Promise<FolderMutationResultDTO>;
444
+ /** Permanently deletes the folder subtree and its Telegram objects. */
445
+ delete(id: string): Promise<FolderMutationResultDTO>;
446
+ batch(operation: "trash" | "restore" | "delete" | "star" | "unstar" | "move", ids: string[], options?: {
447
+ parentId?: string | null;
448
+ }): Promise<BatchResultDTO>;
449
+ }
450
+ declare class TrashApi {
451
+ private readonly client;
452
+ constructor(client: OmniCloudClient);
453
+ list(query?: ListQuery): Promise<TrashPageDTO>;
454
+ /** Permanently removes everything in the Trash. */
455
+ empty(): Promise<EmptyTrashResultDTO>;
456
+ }
457
+ declare class StorageApi {
458
+ private readonly client;
459
+ constructor(client: OmniCloudClient);
460
+ /** Creates the private storage channel if it does not exist yet. */
461
+ ensure(): Promise<StorageDTO>;
462
+ stats(): Promise<StorageStatsDTO>;
463
+ health(): Promise<{
464
+ health: StorageHealthDTO;
465
+ stats: StorageStatsDTO;
466
+ }>;
467
+ /** Probes the storage backend; `deep` performs a real Telegram round-trip. */
468
+ check(deep?: boolean): Promise<StorageHealthDTO>;
469
+ /** Read-only integrity scan; nothing is repaired or deleted. */
470
+ integrityCheck(options?: {
471
+ deep?: boolean;
472
+ }): Promise<IntegrityReportDTO$1>;
473
+ /** Public server health (no authentication required). */
474
+ serverHealth(): Promise<HealthDTO>;
475
+ }
476
+ declare class SearchApi {
477
+ private readonly client;
478
+ constructor(client: OmniCloudClient);
479
+ /**
480
+ * Searches with the v0.2 query language:
481
+ * `type:pdf`, `size:>100MB`, `folder:Projects`, `starred:true`, `after:2026-01-01`
482
+ */
483
+ query(q: string, page?: {
484
+ page?: number;
485
+ limit?: number;
486
+ }): Promise<SearchResultDTO$1>;
487
+ }
488
+ declare class RecentApi {
489
+ private readonly client;
490
+ constructor(client: OmniCloudClient);
491
+ list(page?: {
492
+ page?: number;
493
+ limit?: number;
494
+ }): Promise<RecentPageDTO$1>;
495
+ }
496
+ declare class ActivityApi {
497
+ private readonly client;
498
+ constructor(client: OmniCloudClient);
499
+ list(page?: {
500
+ page?: number;
501
+ limit?: number;
502
+ }): Promise<ActivityPageDTO>;
503
+ }
504
+ declare class AuthApi {
505
+ private readonly client;
506
+ constructor(client: OmniCloudClient);
122
507
  me(): Promise<SessionInfo>;
123
508
  startTelegramLogin(phone: string): Promise<void>;
124
509
  verifyTelegramCode(phone: string, code: string): Promise<TelegramVerifyResponse>;
@@ -126,42 +511,96 @@ declare class OmniCloudClient {
126
511
  status: "ok";
127
512
  user: UserDTO;
128
513
  }>;
129
- ensureStorage(): Promise<StorageDTO>;
130
514
  logout(): Promise<void>;
131
- listFolders(parentId: string | null): Promise<FolderDTO[]>;
132
- folderTree(): Promise<FolderDTO[]>;
133
- createFolder(name: string, parentId?: string | null): Promise<FolderDTO>;
134
- renameFolder(id: string, name: string): Promise<FolderDTO>;
135
- moveFolder(id: string, parentId: string | null): Promise<FolderDTO>;
136
- deleteFolder(id: string): Promise<DeleteFolderResultDTO>;
137
- listFiles(folderId: string | null): Promise<FileDTO[]>;
138
- getFile(id: string): Promise<FileDTO>;
139
- renameFile(id: string, name: string): Promise<FileDTO>;
140
- moveFile(id: string, folderId: string | null): Promise<FileDTO>;
141
- deleteFile(id: string): Promise<void>;
142
- /** URL of the download endpoint (usable as href for direct downloads). */
143
- fileDownloadUrl(id: string): string;
144
- /**
145
- * Uploads a file. Uses XMLHttpRequest when available so `onProgress`
146
- * works (browsers); falls back to fetch without progress in Node.
147
- */
148
- uploadFile(input: UploadInput, options?: UploadOptions): Promise<FileDTO>;
149
- search(query: string): Promise<SearchResultDTO>;
150
- private uploadViaXhr;
151
- private request;
152
515
  }
153
516
 
517
+ /** Connection state of the underlying storage backend. */
518
+ type ConnectionState = "DISCONNECTED" | "CONNECTING" | "CONNECTED" | "RECONNECTING" | "ERROR" | "AUTH_REQUIRED";
519
+ interface FileDTO {
520
+ id: string;
521
+ name: string;
522
+ size: number;
523
+ mimeType: string;
524
+ sha256: string;
525
+ folderId: string | null;
526
+ starred: boolean;
527
+ trashed: boolean;
528
+ deletedAt: string | null;
529
+ versionCount: number;
530
+ currentVersionId: string | null;
531
+ createdAt: string;
532
+ updatedAt: string;
533
+ }
534
+ interface FolderDTO {
535
+ id: string;
536
+ name: string;
537
+ parentId: string | null;
538
+ starred: boolean;
539
+ trashed: boolean;
540
+ deletedAt: string | null;
541
+ createdAt: string;
542
+ updatedAt: string;
543
+ }
544
+ interface SearchResultDTO {
545
+ files: FileDTO[];
546
+ folders: FolderDTO[];
547
+ pagination: PaginationDTO;
548
+ }
549
+ interface PaginationDTO {
550
+ page: number;
551
+ limit: number;
552
+ total: number;
553
+ hasMore: boolean;
554
+ }
555
+ interface RecentItemDTO {
556
+ file: FileDTO;
557
+ lastAction: ActivityAction;
558
+ lastActionAt: string;
559
+ }
560
+ interface RecentPageDTO {
561
+ items: RecentItemDTO[];
562
+ pagination: PaginationDTO;
563
+ }
564
+ declare const ACTIVITY_ACTIONS: readonly ["upload", "download", "open", "rename", "move", "star", "unstar", "trash", "restore", "delete", "create_folder", "replace"];
565
+ type ActivityAction = (typeof ACTIVITY_ACTIONS)[number];
566
+ declare const RESOURCE_TYPES: readonly ["file", "folder", "storage"];
567
+ type ResourceType = (typeof RESOURCE_TYPES)[number];
568
+ interface IntegrityIssueDTO {
569
+ fileId: string;
570
+ name: string;
571
+ kind: "missing" | "size_mismatch" | "unreadable" | "hash_mismatch";
572
+ detail: string | null;
573
+ }
574
+ interface IntegrityReportDTO {
575
+ checkedAt: string;
576
+ filesChecked: number;
577
+ healthy: number;
578
+ missing: number;
579
+ inconsistent: number;
580
+ unreadable: number;
581
+ issues: IntegrityIssueDTO[];
582
+ durationMs: number;
583
+ }
154
584
  /** Stable machine-readable error codes returned in API error bodies. */
155
585
  declare const ERROR_CODES: {
156
- readonly VALIDATION: "VALIDATION_ERROR";
157
- readonly UNAUTHENTICATED: "UNAUTHENTICATED";
158
- readonly FORBIDDEN: "FORBIDDEN";
586
+ readonly AUTH_REQUIRED: "AUTH_REQUIRED";
587
+ readonly AUTH_INVALID: "AUTH_INVALID";
588
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
589
+ readonly INVALID_REQUEST: "INVALID_REQUEST";
159
590
  readonly NOT_FOUND: "NOT_FOUND";
591
+ readonly FILE_NOT_FOUND: "FILE_NOT_FOUND";
592
+ readonly FOLDER_NOT_FOUND: "FOLDER_NOT_FOUND";
160
593
  readonly CONFLICT: "CONFLICT";
161
594
  readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
162
595
  readonly RATE_LIMITED: "RATE_LIMITED";
163
596
  readonly STORAGE_NOT_INITIALIZED: "STORAGE_NOT_INITIALIZED";
164
- readonly TELEGRAM_ERROR: "TELEGRAM_ERROR";
597
+ readonly STORAGE_UNAVAILABLE: "STORAGE_UNAVAILABLE";
598
+ readonly TELEGRAM_AUTH_REQUIRED: "TELEGRAM_AUTH_REQUIRED";
599
+ readonly TELEGRAM_CONNECTION_FAILED: "TELEGRAM_CONNECTION_FAILED";
600
+ readonly TELEGRAM_FILE_NOT_FOUND: "TELEGRAM_FILE_NOT_FOUND";
601
+ readonly UPLOAD_FAILED: "UPLOAD_FAILED";
602
+ readonly DOWNLOAD_FAILED: "DOWNLOAD_FAILED";
603
+ readonly INTEGRITY_CHECK_FAILED: "INTEGRITY_CHECK_FAILED";
165
604
  readonly INTERNAL: "INTERNAL_ERROR";
166
605
  };
167
606
  type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
@@ -176,10 +615,11 @@ declare class DomainError extends Error {
176
615
  readonly details?: unknown;
177
616
  constructor(code: ErrorCode, status: number, message: string, details?: unknown);
178
617
  }
179
- /** The underlying storage backend (e.g. Telegram) failed. */
180
- declare class StorageProviderError extends DomainError {
181
- constructor(message: string, details?: unknown);
182
- }
618
+ /**
619
+ * Wraps an arbitrary provider failure as an UploadFailedError (the default
620
+ * storage-operation error), preserving domain errors untouched.
621
+ */
622
+ declare function mapProviderError(message: string, error: unknown): DomainError;
183
623
 
184
624
  /**
185
625
  * Domain records. Repository implementations (e.g. the Prisma layer) map
@@ -189,6 +629,7 @@ declare class StorageProviderError extends DomainError {
189
629
  * Telegram identifiers are represented as strings/numbers that fit in
190
630
  * JavaScript's safe integer range; the persistence layer converts as needed.
191
631
  */
632
+
192
633
  interface UserRecord {
193
634
  id: string;
194
635
  telegramUserId: string;
@@ -216,6 +657,15 @@ interface FolderRecord {
216
657
  userId: string;
217
658
  parentId: string | null;
218
659
  name: string;
660
+ starred: boolean;
661
+ /** Soft-delete marker (null = active). */
662
+ deletedAt: Date | null;
663
+ /**
664
+ * Groups everything moved to the Trash by one operation, so restoring a
665
+ * folder restores exactly what it took (and leaves separately-trashed
666
+ * items alone). Null for items trashed individually.
667
+ */
668
+ trashBatchId: string | null;
219
669
  createdAt: Date;
220
670
  updatedAt: Date;
221
671
  }
@@ -228,9 +678,83 @@ interface FileRecord {
228
678
  mimeType: string;
229
679
  sha256: string;
230
680
  telegramMessageId: number;
681
+ starred: boolean;
682
+ /** Soft-delete marker (null = active). */
683
+ deletedAt: Date | null;
684
+ /** See FolderRecord.trashBatchId. */
685
+ trashBatchId: string | null;
686
+ /** Current version row, if versioning has been initialized for this file. */
687
+ currentVersionId: string | null;
688
+ versionCount: number;
231
689
  createdAt: Date;
232
690
  updatedAt: Date;
233
691
  }
692
+ interface FileVersionRecord {
693
+ id: string;
694
+ fileId: string;
695
+ userId: string;
696
+ versionNumber: number;
697
+ size: number;
698
+ mimeType: string;
699
+ sha256: string;
700
+ telegramMessageId: number;
701
+ createdAt: Date;
702
+ }
703
+ interface ActivityEventRecord {
704
+ id: string;
705
+ userId: string;
706
+ action: ActivityAction;
707
+ resourceType: ResourceType;
708
+ resourceId: string;
709
+ resourceName: string | null;
710
+ metadata: Record<string, unknown> | null;
711
+ createdAt: Date;
712
+ }
713
+ /** Resolved, repository-ready item query (search parser output resolved). */
714
+ interface ItemQuery {
715
+ /** Restrict to a folder (null = root). Undefined = any folder. */
716
+ folderId?: string | null;
717
+ /** Restrict to a set of folder ids (used for `folder:Name` filters). */
718
+ folderIds?: string[];
719
+ nameContains?: string;
720
+ /** MIME matchers: exact values or prefixes ending in "/". */
721
+ mimeMatchers?: readonly string[];
722
+ ext?: string;
723
+ starred?: boolean;
724
+ /** false = active only, true = trashed only, undefined = both. */
725
+ trashed?: boolean;
726
+ minSize?: number;
727
+ maxSize?: number;
728
+ from?: Date;
729
+ to?: Date;
730
+ sort?: SortField;
731
+ order?: SortOrder;
732
+ }
733
+ interface PageRequest {
734
+ page: number;
735
+ limit: number;
736
+ }
737
+ interface Paged<T> {
738
+ items: T[];
739
+ total: number;
740
+ }
741
+ interface FileTypeBucket {
742
+ category: string;
743
+ bytes: number;
744
+ count: number;
745
+ }
746
+ interface StorageStats {
747
+ fileCount: number;
748
+ folderCount: number;
749
+ totalBytes: number;
750
+ trashBytes: number;
751
+ trashFileCount: number;
752
+ starredCount: number;
753
+ byType: FileTypeBucket[];
754
+ largestFiles: FileRecord[];
755
+ /** Optional quota ceiling (null = unlimited, the v0.2 default). */
756
+ quotaBytes: number | null;
757
+ }
234
758
 
235
759
  /**
236
760
  * Repository interfaces. The application depends on these, not on a specific
@@ -263,42 +787,116 @@ interface StorageRepository {
263
787
  telegramAccessHash: string;
264
788
  }): Promise<StorageRecord>;
265
789
  }
790
+ interface FolderCreateInput {
791
+ userId: string;
792
+ parentId: string | null;
793
+ name: string;
794
+ }
266
795
  interface FolderRepository {
267
- create(input: {
268
- userId: string;
269
- parentId: string | null;
270
- name: string;
271
- }): Promise<FolderRecord>;
796
+ create(input: FolderCreateInput): Promise<FolderRecord>;
272
797
  findById(id: string): Promise<FolderRecord | null>;
273
- /** Every folder belonging to the user (used for trees and cascade deletes). */
798
+ /** Every folder belonging to the user (used for trees and cascade logic). */
274
799
  listByUser(userId: string): Promise<FolderRecord[]>;
275
800
  listChildren(userId: string, parentId: string | null): Promise<FolderRecord[]>;
801
+ /** Filtered, sorted, paginated listing. */
802
+ query(userId: string, query: ItemQuery, page: PageRequest): Promise<Paged<FolderRecord>>;
803
+ /** Folder ids matching a name substring (used to resolve `folder:` filters). */
804
+ findIdsByName(userId: string, name: string): Promise<string[]>;
276
805
  update(id: string, patch: {
277
806
  name?: string;
278
807
  parentId?: string | null;
808
+ starred?: boolean;
809
+ deletedAt?: Date | null;
810
+ trashBatchId?: string | null;
279
811
  }): Promise<FolderRecord>;
812
+ /** Bulk trash/restore/star without touching files. */
813
+ updateMany(ids: string[], patch: {
814
+ starred?: boolean;
815
+ deletedAt?: Date | null;
816
+ trashBatchId?: string | null;
817
+ }): Promise<number>;
280
818
  deleteMany(ids: string[]): Promise<void>;
281
- searchByName(userId: string, query: string, limit: number): Promise<FolderRecord[]>;
819
+ countByUser(userId: string): Promise<number>;
820
+ }
821
+ interface FileCreateInput {
822
+ userId: string;
823
+ folderId: string | null;
824
+ name: string;
825
+ size: number;
826
+ mimeType: string;
827
+ sha256: string;
828
+ telegramMessageId: number;
282
829
  }
283
830
  interface FileRepository {
284
- create(input: {
285
- userId: string;
286
- folderId: string | null;
287
- name: string;
288
- size: number;
289
- mimeType: string;
290
- sha256: string;
291
- telegramMessageId: number;
292
- }): Promise<FileRecord>;
831
+ create(input: FileCreateInput): Promise<FileRecord>;
293
832
  findById(id: string): Promise<FileRecord | null>;
833
+ /** Active files directly inside a folder (null = root). */
294
834
  listByFolder(userId: string, folderId: string | null): Promise<FileRecord[]>;
295
835
  listByUser(userId: string): Promise<FileRecord[]>;
836
+ listByIds(userId: string, ids: string[]): Promise<FileRecord[]>;
837
+ query(userId: string, query: ItemQuery, page: PageRequest): Promise<Paged<FileRecord>>;
296
838
  update(id: string, patch: {
297
839
  name?: string;
298
840
  folderId?: string | null;
841
+ starred?: boolean;
842
+ deletedAt?: Date | null;
843
+ trashBatchId?: string | null;
844
+ size?: number;
845
+ mimeType?: string;
846
+ sha256?: string;
847
+ telegramMessageId?: number;
848
+ currentVersionId?: string | null;
849
+ versionCount?: number;
299
850
  }): Promise<FileRecord>;
851
+ updateMany(ids: string[], patch: {
852
+ folderId?: string | null;
853
+ starred?: boolean;
854
+ deletedAt?: Date | null;
855
+ trashBatchId?: string | null;
856
+ }): Promise<number>;
857
+ /** Aggregate counters used by the storage dashboard. */
858
+ statsByUser(userId: string): Promise<{
859
+ fileCount: number;
860
+ folderCount: number;
861
+ totalBytes: number;
862
+ trashBytes: number;
863
+ trashFileCount: number;
864
+ starredCount: number;
865
+ }>;
300
866
  deleteMany(ids: string[]): Promise<void>;
301
- searchByName(userId: string, query: string, limit: number): Promise<FileRecord[]>;
867
+ createVersion(input: {
868
+ fileId: string;
869
+ userId: string;
870
+ size: number;
871
+ mimeType: string;
872
+ sha256: string;
873
+ telegramMessageId: number;
874
+ }): Promise<FileVersionRecord>;
875
+ listVersions(fileId: string): Promise<FileVersionRecord[]>;
876
+ findVersionById(versionId: string): Promise<FileVersionRecord | null>;
877
+ countVersions(fileId: string): Promise<number>;
878
+ deleteVersionsByFileIds(fileIds: string[]): Promise<void>;
879
+ }
880
+ interface ActivityRepository {
881
+ record(input: {
882
+ userId: string;
883
+ action: ActivityAction;
884
+ resourceType: ResourceType;
885
+ resourceId: string;
886
+ resourceName?: string | null;
887
+ metadata?: Record<string, unknown> | null;
888
+ }): Promise<void>;
889
+ /** Newest-first activity for a user. */
890
+ list(userId: string, page: PageRequest): Promise<Paged<ActivityEventRecord>>;
891
+ /**
892
+ * Most recent distinct files for the given actions, newest first. Only
893
+ * events whose action is listed are considered, so a file whose latest
894
+ * event is (say) a star still surfaces via its last meaningful action.
895
+ */
896
+ recentFiles(userId: string, actions: readonly ActivityAction[], limit: number): Promise<ActivityEventRecord[]>;
897
+ countByUser(userId: string): Promise<number>;
898
+ /** Housekeeping: drop events older than the retention window. */
899
+ pruneOlderThan(cutoff: Date): Promise<number>;
302
900
  }
303
901
  interface Repos {
304
902
  users: UserRepository;
@@ -306,11 +904,15 @@ interface Repos {
306
904
  storages: StorageRepository;
307
905
  folders: FolderRepository;
308
906
  files: FileRepository;
907
+ activity: ActivityRepository;
309
908
  }
310
909
 
311
910
  /**
312
911
  * Generic storage abstraction. The application must never depend on
313
912
  * Telegram-specific details — it talks to a StorageProvider.
913
+ *
914
+ * v0.2 adds streaming, progress reporting, cancellation and health checks
915
+ * while keeping the abstraction provider-agnostic.
314
916
  */
315
917
  /** Reference to an object inside the backing store (opaque to callers). */
316
918
  interface StoredRef {
@@ -321,21 +923,57 @@ interface StoredObject extends StoredRef {
321
923
  size: number;
322
924
  mimeType: string;
323
925
  }
926
+ /** Transfer progress emitted during upload/download. */
927
+ interface TransferProgress {
928
+ /** Bytes transferred so far. */
929
+ transferred: number;
930
+ /** Total bytes, when known upfront. */
931
+ total: number | null;
932
+ /** 0–100 when total is known, otherwise null. */
933
+ percent: number | null;
934
+ }
935
+ interface TransferControl {
936
+ /** Aborted when the caller cancels the operation. */
937
+ signal?: AbortSignal;
938
+ onProgress?: (progress: TransferProgress) => void;
939
+ }
940
+ /** Upload input. `data` is a Buffer; `stream` is used for large files. */
324
941
  interface StorageUploadInput {
325
942
  name: string;
326
943
  mimeType: string;
327
- data: Buffer;
944
+ /** Whole-object buffer (small files). */
945
+ data?: Buffer;
946
+ /** Streaming source (large files). Either `data` or `stream` is required. */
947
+ stream?: Readable;
948
+ /** Total byte length, required when streaming. */
949
+ size?: number;
950
+ }
951
+ interface StorageHealth {
952
+ /** Reachable and usable. */
953
+ healthy: boolean;
954
+ /** Round-trip latency of the probe in milliseconds, when measured. */
955
+ latencyMs: number | null;
956
+ message: string | null;
957
+ /** Name of the storage channel/target, when available. */
958
+ targetTitle: string | null;
328
959
  }
329
960
  interface StorageProvider {
330
961
  readonly name: string;
331
- /** Uploads the object and returns the stored reference. */
332
- put(input: StorageUploadInput): Promise<StoredObject>;
333
- /** Downloads the object's content. */
334
- get(ref: StoredRef): Promise<Buffer>;
962
+ /**
963
+ * Uploads an object and returns the stored reference. Reports progress and
964
+ * honors `control.signal` for cancellation.
965
+ */
966
+ put(input: StorageUploadInput, control?: TransferControl): Promise<StoredObject>;
967
+ /** Downloads the object's content as a Buffer. */
968
+ get(ref: StoredRef, control?: TransferControl): Promise<Buffer>;
969
+ /** Opens a stream for the object (large-file friendly). */
970
+ getStream(ref: StoredRef, control?: TransferControl): Promise<Readable>;
335
971
  /** Deletes the object. Deleting an already-missing object must not throw. */
336
972
  delete(ref: StoredRef): Promise<void>;
337
973
  exists(ref: StoredRef): Promise<boolean>;
338
974
  stat(ref: StoredRef): Promise<StoredObject | null>;
975
+ /** Validates that the backing store is reachable and usable. */
976
+ healthCheck(): Promise<StorageHealth>;
339
977
  }
340
978
 
341
979
  declare function sha256Hex(data: Uint8Array): string;
@@ -345,24 +983,82 @@ interface EngineUploadResult {
345
983
  sha256: string;
346
984
  size: number;
347
985
  }
986
+ interface EngineRetryPolicy {
987
+ /** Total attempts, including the first. */
988
+ attempts: number;
989
+ /** Base delay in ms; doubles per retry. */
990
+ baseDelayMs: number;
991
+ }
348
992
  /**
349
993
  * Sits between the application and the raw StorageProvider and adds the
350
- * cross-cutting guarantees: SHA-256 integrity and consistent error mapping.
994
+ * cross-cutting guarantees: SHA-256 integrity, streaming, progress reporting,
995
+ * cancellation and bounded retries.
351
996
  *
352
997
  * The active provider is chosen per user by the host application (each user
353
998
  * has their own Telegram storage channel).
354
999
  */
355
1000
  declare class StorageEngine {
356
1001
  private readonly provider;
357
- constructor(provider: StorageProvider);
1002
+ private readonly retryPolicy;
1003
+ constructor(provider: StorageProvider, retryPolicy?: EngineRetryPolicy);
358
1004
  get providerName(): string;
359
- /** Uploads an object and computes its checksum in one step. */
360
- upload(input: StorageUploadInput): Promise<EngineUploadResult>;
361
- download(ref: StoredRef): Promise<Buffer>;
1005
+ /**
1006
+ * Uploads an object, computing its SHA-256 as it goes and reporting
1007
+ * progress. Retries transient failures with exponential backoff.
1008
+ */
1009
+ upload(input: StorageUploadInput, control?: TransferControl): Promise<EngineUploadResult>;
1010
+ download(ref: StoredRef, control?: TransferControl): Promise<Buffer>;
1011
+ /** Streams the object, computing the checksum on the fly when requested. */
1012
+ downloadStream(ref: StoredRef, control?: TransferControl): Promise<{
1013
+ stream: Readable;
1014
+ hash: () => string | null;
1015
+ }>;
362
1016
  remove(ref: StoredRef): Promise<void>;
1017
+ exists(ref: StoredRef): Promise<boolean>;
1018
+ stat(ref: StoredRef): Promise<StoredObject | null>;
1019
+ health(): Promise<StorageHealth>;
363
1020
  verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
1021
+ /** Materializes an upload input so the checksum can be computed. */
1022
+ private readInput;
1023
+ private withRetry;
1024
+ }
1025
+
1026
+ interface RecordActivityInput {
1027
+ userId: string;
1028
+ action: ActivityAction;
1029
+ resourceType: ResourceType;
1030
+ resourceId: string;
1031
+ resourceName?: string | null;
1032
+ metadata?: Record<string, unknown> | null;
1033
+ }
1034
+ /**
1035
+ * The narrow interface other services depend on for activity logging, so a
1036
+ * no-op recorder can be substituted in tests and activity failures never
1037
+ * break the operation being logged.
1038
+ */
1039
+ interface ActivityRecorder {
1040
+ record(input: RecordActivityInput): Promise<void>;
364
1041
  }
365
- declare function mapProviderError(message: string, error: unknown): StorageProviderError;
1042
+ /** Activity log with best-effort recording and retention pruning. */
1043
+ declare class ActivityService implements ActivityRecorder {
1044
+ private readonly activity;
1045
+ /** Days to retain events; older rows are pruned on demand. */
1046
+ private readonly retentionDays;
1047
+ constructor(activity: ActivityRepository,
1048
+ /** Days to retain events; older rows are pruned on demand. */
1049
+ retentionDays?: number);
1050
+ /**
1051
+ * Records an event. Never throws — a logging failure must not fail the
1052
+ * user-facing operation that produced it.
1053
+ */
1054
+ record(input: RecordActivityInput): Promise<void>;
1055
+ list(userId: string, page: PageRequest): Promise<Paged<ActivityEventRecord>>;
1056
+ /** Most recent distinct files for the given actions, for the Recent view. */
1057
+ recentFiles(userId: string, actions: readonly ActivityAction[], limit: number): Promise<ActivityEventRecord[]>;
1058
+ prune(): Promise<number>;
1059
+ }
1060
+ /** A recorder that discards events (used where activity is not wired up). */
1061
+ declare const noopActivityRecorder: ActivityRecorder;
366
1062
 
367
1063
  interface FileDownload {
368
1064
  record: FileRecord;
@@ -370,64 +1066,239 @@ interface FileDownload {
370
1066
  integrityVerified: boolean;
371
1067
  }
372
1068
  type EngineResolver = (userId: string) => Promise<StorageEngine>;
1069
+ interface FileUploadInput {
1070
+ folderId: string | null;
1071
+ name: string;
1072
+ data: Buffer;
1073
+ /** When set, uploads a new version of this file instead of creating one. */
1074
+ replaceFileId?: string;
1075
+ /** Progress callback forwarded from the transport layer. */
1076
+ onProgress?: (progress: TransferProgress) => void;
1077
+ signal?: AbortSignal;
1078
+ }
373
1079
  /**
374
- * All file metadata operations. Every method verifies ownership: records
1080
+ * File metadata operations. Every method verifies ownership: records
375
1081
  * belonging to other users are reported as missing.
1082
+ *
1083
+ * Reliability rules honored here:
1084
+ * - metadata is written only after the storage upload succeeds;
1085
+ * - permanent deletion removes the remote object before the metadata;
1086
+ * - client-supplied ownership is never trusted (userId is server-derived).
376
1087
  */
377
1088
  declare class FileService {
378
1089
  private readonly files;
379
1090
  private readonly folders;
380
1091
  private readonly engineFor;
381
- constructor(files: FileRepository, folders: FolderRepository, engineFor: EngineResolver);
382
- upload(userId: string, input: {
383
- folderId: string | null;
384
- name: string;
385
- data: Buffer;
386
- }): Promise<FileRecord>;
1092
+ private readonly activity;
1093
+ constructor(files: FileRepository, folders: FolderRepository, engineFor: EngineResolver, activity?: ActivityRecorder);
1094
+ upload(userId: string, input: FileUploadInput): Promise<FileRecord>;
1095
+ /** Uploads a new version of an existing file and makes it current. */
1096
+ private replaceVersion;
387
1097
  list(userId: string, folderId: string | null): Promise<FileRecord[]>;
1098
+ query(userId: string, input: ListQuery, page: PageRequest): Promise<Paged<FileRecord>>;
388
1099
  get(userId: string, id: string): Promise<FileRecord>;
1100
+ /** Like get(), but rejects trashed files (for download/open). */
1101
+ getActive(userId: string, id: string): Promise<FileRecord>;
1102
+ listVersions(userId: string, id: string): Promise<FileVersionRecord[]>;
389
1103
  rename(userId: string, id: string, name: string): Promise<FileRecord>;
390
1104
  move(userId: string, id: string, folderId: string | null): Promise<FileRecord>;
1105
+ setStarred(userId: string, id: string, starred: boolean): Promise<FileRecord>;
1106
+ /**
1107
+ * Moves a file to Trash (soft delete). The remote object is untouched, so
1108
+ * restore is lossless.
1109
+ */
1110
+ trash(userId: string, id: string): Promise<FileRecord>;
1111
+ /** Restores a trashed file to its previous folder (or root). */
1112
+ restore(userId: string, id: string): Promise<FileRecord>;
1113
+ /**
1114
+ * Permanently deletes a file: the Telegram object is removed first, then the
1115
+ * metadata. If the remote delete fails we keep the metadata so the user can
1116
+ * retry, rather than silently orphaning the storage object.
1117
+ */
1118
+ deletePermanently(userId: string, id: string): Promise<void>;
1119
+ /**
1120
+ * v0.1-compatible hard delete used by the current API: permanently removes
1121
+ * the file. Retained so existing clients keep working.
1122
+ */
391
1123
  delete(userId: string, id: string): Promise<void>;
392
- download(userId: string, id: string): Promise<FileDownload>;
1124
+ download(userId: string, id: string, control?: TransferControl): Promise<FileDownload>;
1125
+ /** Records an "open" event (the UI opening the details panel, say). */
1126
+ touch(userId: string, id: string, action?: "open"): Promise<void>;
1127
+ /**
1128
+ * Applies an operation to many files, collecting per-item failures instead
1129
+ * of aborting the whole batch.
1130
+ */
1131
+ batch(userId: string, ids: string[], operation: "trash" | "restore" | "delete" | "star" | "unstar" | "move", options?: {
1132
+ folderId?: string | null;
1133
+ }): Promise<{
1134
+ requested: number;
1135
+ succeeded: number;
1136
+ failed: number;
1137
+ errors: {
1138
+ id: string;
1139
+ message: string;
1140
+ }[];
1141
+ }>;
1142
+ private assertFolder;
393
1143
  }
394
1144
 
395
- interface FolderDeletionResult {
396
- deletedFolders: number;
397
- deletedFiles: number;
1145
+ interface FolderMutationResult {
1146
+ affectedFolders: number;
1147
+ affectedFiles: number;
398
1148
  }
399
1149
  /**
400
- * Folder deletion semantics (v0.1): deleting a folder recursively deletes
401
- * all descendant folders and the files inside them, including the remote
402
- * storage objects. Files directly inside the deleted hierarchy are removed,
403
- * not relocated.
1150
+ * Folder lifecycle (v0.2):
1151
+ *
1152
+ * - Folders are virtual OmniCloud objects with a parent-child hierarchy.
1153
+ * - Trashing a folder trashes its whole subtree (folders + files) without
1154
+ * touching Telegram, so restore is lossless.
1155
+ * - Restoring a folder restores only the nodes that were trashed together
1156
+ * with it; anything the user trashed separately stays in the Trash.
1157
+ * - Permanent deletion removes remote objects first, then metadata.
404
1158
  */
405
1159
  declare class FolderService {
406
1160
  private readonly folders;
407
1161
  private readonly files;
408
1162
  private readonly engineFor;
409
- constructor(folders: FolderRepository, files: FileRepository, engineFor: EngineResolver);
1163
+ private readonly activity;
1164
+ constructor(folders: FolderRepository, files: FileRepository, engineFor: EngineResolver, activity?: ActivityRecorder);
410
1165
  create(userId: string, input: {
411
1166
  name: string;
412
1167
  parentId: string | null;
413
1168
  }): Promise<FolderRecord>;
1169
+ /** Active folders directly inside a folder (null = root). */
414
1170
  listChildren(userId: string, parentId: string | null): Promise<FolderRecord[]>;
1171
+ query(userId: string, input: ListQuery, page: PageRequest): Promise<Paged<FolderRecord>>;
1172
+ /** Every active folder for the user (used to build trees). */
415
1173
  tree(userId: string): Promise<FolderRecord[]>;
416
1174
  get(userId: string, id: string): Promise<FolderRecord>;
417
1175
  rename(userId: string, id: string, name: string): Promise<FolderRecord>;
418
1176
  move(userId: string, id: string, newParentId: string | null): Promise<FolderRecord>;
419
- delete(userId: string, id: string): Promise<FolderDeletionResult>;
1177
+ setStarred(userId: string, id: string, starred: boolean): Promise<FolderRecord>;
1178
+ /**
1179
+ * Moves a folder and its entire subtree to the Trash. Nothing is deleted
1180
+ * from Telegram, so restore is lossless.
1181
+ */
1182
+ trash(userId: string, id: string): Promise<FolderMutationResult>;
1183
+ /**
1184
+ * Restores a folder subtree. Only nodes trashed at the same instant as the
1185
+ * folder itself are restored, so items the user trashed separately stay put.
1186
+ */
1187
+ restore(userId: string, id: string): Promise<FolderMutationResult>;
1188
+ /**
1189
+ * Permanently deletes a folder subtree: remote objects first, then
1190
+ * metadata. Files whose remote delete fails are left in the Trash so the
1191
+ * user can retry rather than losing data silently.
1192
+ */
1193
+ deletePermanently(userId: string, id: string): Promise<FolderMutationResult>;
1194
+ /**
1195
+ * v0.1-compatible recursive delete. Now routes through the v0.2 permanent
1196
+ * delete so remote cleanup and orphan prevention are consistent.
1197
+ */
1198
+ delete(userId: string, id: string): Promise<FolderMutationResult>;
1199
+ /** The folder plus every descendant (BFS), optionally including trashed. */
1200
+ private collectSubtree;
1201
+ /** Ids of files inside the given folders that are not already trashed. */
1202
+ private collectFileIds;
420
1203
  }
421
1204
 
422
- /** Metadata-based search over file and folder names (PostgreSQL-backed). */
1205
+ interface SearchResult {
1206
+ files: FileRecord[];
1207
+ folders: FolderRecord[];
1208
+ pagination: PaginationDTO;
1209
+ }
1210
+ /**
1211
+ * PostgreSQL-backed search (no Elasticsearch, per the v0.2 scope).
1212
+ *
1213
+ * The user query is parsed into structured filters (`type:`, `size:`,
1214
+ * `folder:`, …) and evaluated by the repository layer, so results respect
1215
+ * ownership and stay safely parameterized.
1216
+ */
423
1217
  declare class SearchService {
424
1218
  private readonly files;
425
1219
  private readonly folders;
426
1220
  constructor(files: FileRepository, folders: FolderRepository);
427
- search(userId: string, query: string, limit?: number): Promise<{
428
- files: FileRecord[];
429
- folders: FolderRecord[];
430
- }>;
1221
+ search(userId: string, rawQuery: string, page: PageRequest, overrides?: ListQuery): Promise<SearchResult>;
1222
+ /** Convenience wrapper returning the shared DTO shape. */
1223
+ searchDTO(userId: string, rawQuery: string, page: PageRequest): Promise<Omit<SearchResultDTO, "files" | "folders"> & SearchResult>;
1224
+ }
1225
+
1226
+ /**
1227
+ * Storage dashboard statistics, computed from PostgreSQL metadata only —
1228
+ * no Telegram round-trips, so the page is fast and works offline.
1229
+ */
1230
+ declare class StatsService {
1231
+ private readonly files;
1232
+ private readonly folders;
1233
+ constructor(files: FileRepository, folders: FolderRepository);
1234
+ stats(userId: string, quotaBytes?: number | null): Promise<StorageStats>;
1235
+ }
1236
+
1237
+ interface TrashListing {
1238
+ files: FileRecord[];
1239
+ folders: FolderRecord[];
1240
+ pagination: PaginationDTO;
1241
+ }
1242
+ interface EmptyTrashResult {
1243
+ deletedFiles: number;
1244
+ deletedFolders: number;
1245
+ failedFiles: number;
1246
+ }
1247
+ /**
1248
+ * Trash is a virtual view over soft-deleted rows. Listing shows only the
1249
+ * explicitly trashed items; emptying removes their remote objects before the
1250
+ * metadata, and reports failures instead of hiding them.
1251
+ */
1252
+ declare class TrashService {
1253
+ private readonly files;
1254
+ private readonly folders;
1255
+ private readonly engineFor;
1256
+ constructor(files: FileRepository, folders: FolderRepository, engineFor: EngineResolver);
1257
+ list(userId: string, page: PageRequest, extra?: ListQuery): Promise<TrashListing>;
1258
+ /**
1259
+ * Permanently removes everything in the Trash for a user. Folders are
1260
+ * processed root-first so removing a parent also removes its contents.
1261
+ */
1262
+ empty(userId: string): Promise<EmptyTrashResult>;
1263
+ /** Restores a single trashed item (used by the batch restore endpoint). */
1264
+ verifyTrashedFile(userId: string, id: string): Promise<FileRecord>;
1265
+ }
1266
+
1267
+ interface IntegrityCheckOptions {
1268
+ /** Re-download and re-hash objects (slow but authoritative). */
1269
+ deep?: boolean;
1270
+ /** Maximum files to inspect in one pass. */
1271
+ limit?: number;
1272
+ }
1273
+ /**
1274
+ * Detects drift between PostgreSQL metadata and Telegram storage:
1275
+ *
1276
+ * - `missing` — metadata exists, the Telegram message is gone
1277
+ * - `size_mismatch` — the stored object's size differs from the metadata
1278
+ * - `hash_mismatch` — deep mode only: downloaded bytes don't match sha256
1279
+ * - `unreadable` — the provider errored while inspecting the object
1280
+ *
1281
+ * The checker is strictly read-only: repairs are a separate, explicit step so
1282
+ * nothing is ever deleted silently (see the v0.2 reliability rules).
1283
+ */
1284
+ declare class IntegrityService {
1285
+ private readonly files;
1286
+ private readonly engineFor;
1287
+ constructor(files: FileRepository, engineFor: EngineResolver);
1288
+ check(userId: string, options?: IntegrityCheckOptions): Promise<IntegrityReportDTO>;
1289
+ private inspect;
1290
+ }
1291
+
1292
+ /**
1293
+ * Recent view: the most recently touched active files, newest activity first.
1294
+ * Trashed and permanently-deleted files are filtered out so the view never
1295
+ * surfaces items the user cannot open.
1296
+ */
1297
+ declare class RecentService {
1298
+ private readonly activity;
1299
+ private readonly files;
1300
+ constructor(activity: ActivityRepository, files: FileRepository);
1301
+ list(userId: string, page: PageRequest): Promise<RecentPageDTO>;
431
1302
  }
432
1303
 
433
1304
  /**
@@ -440,26 +1311,53 @@ interface TelegramCredentials {
440
1311
  apiId: number;
441
1312
  apiHash: string;
442
1313
  }
1314
+ interface ConnectionStatus {
1315
+ state: ConnectionState;
1316
+ lastError: string | null;
1317
+ lastCheckAt: Date | null;
1318
+ healthy: boolean | null;
1319
+ }
443
1320
  /**
444
- * Keeps one connected TelegramClient per authenticated user. Clients are
445
- * restored from the persisted MTProto session string and cached for the
446
- * lifetime of the process.
1321
+ * Owns one Telegram (MTProto) connection per authenticated user and tracks an
1322
+ * explicit lifecycle state for each:
1323
+ *
1324
+ * DISCONNECTED → CONNECTING → CONNECTED
1325
+ * ↘ RECONNECTING → CONNECTED
1326
+ * ↘ ERROR | AUTH_REQUIRED
1327
+ *
1328
+ * Responsibilities: connect, disconnect, reconnect, validateSession,
1329
+ * healthCheck, refreshConnection. Sessions never leave the server.
447
1330
  */
448
1331
  declare class TelegramClientManager {
449
1332
  private readonly credentials;
450
1333
  private readonly sessions;
451
- private clients;
1334
+ private connections;
1335
+ private lastReconnectAt;
452
1336
  constructor(credentials: TelegramCredentials, sessions: SessionRepository);
1337
+ /** Returns a connected client, (re)connecting when needed. */
453
1338
  getForUser(userId: string): Promise<TelegramClient>;
454
- /** Registers an already-authenticated client (e.g. from the login flow). */
1339
+ /** Connection status for health reporting and the storage dashboard. */
1340
+ statusForUser(userId: string): ConnectionStatus;
1341
+ /**
1342
+ * Probes a user's connection. Cheap by default (connection state only);
1343
+ * `deep` performs a real Telegram round-trip to validate the session.
1344
+ */
1345
+ healthCheck(userId: string, deep?: boolean): Promise<StorageHealth>;
1346
+ /** Registers an already-authenticated client (from the login flow). */
455
1347
  cacheClient(userId: string, client: TelegramClient): void;
1348
+ /** Drops a user's connection (e.g. after logout) without touching storage. */
1349
+ disconnectUser(userId: string): Promise<void>;
456
1350
  disconnectAll(): Promise<void>;
457
- private connect;
1351
+ private isConnected;
1352
+ private connectUser;
1353
+ private reconnect;
1354
+ private establish;
458
1355
  }
459
1356
 
460
1357
  /**
461
1358
  * Maps a Telegram (GramJS) failure to a structured domain error with an
462
- * appropriate HTTP status and client-facing message.
1359
+ * appropriate HTTP status and a client-facing message that never leaks
1360
+ * credentials or session material.
463
1361
  */
464
1362
  declare function mapTelegramError(error: unknown, fallbackMessage: string): DomainError;
465
1363
 
@@ -468,22 +1366,38 @@ interface TelegramStorageOptions {
468
1366
  chatId: string;
469
1367
  /** Channel access hash required to build the input peer. */
470
1368
  accessHash: string;
1369
+ /** Human-readable channel title, surfaced in health reporting. */
1370
+ title?: string;
471
1371
  }
472
1372
  /**
473
1373
  * StorageProvider implementation backed by the user's private Telegram
474
- * channel. Files are sent as force-downloaded documents (no compression),
475
- * so every upload is a document message in the channel.
1374
+ * channel. Files are sent as force-downloaded documents (no compression), so
1375
+ * every upload is a document message in the channel.
1376
+ *
1377
+ * v0.2 adds progress reporting, cancellation, streamed downloads and a
1378
+ * health check, all translated into the provider-agnostic core vocabulary.
476
1379
  */
477
1380
  declare class TelegramStorageProvider implements StorageProvider {
478
1381
  private readonly client;
479
1382
  readonly name = "telegram";
480
1383
  private readonly peer;
1384
+ private readonly title;
481
1385
  constructor(client: TelegramClient, options: TelegramStorageOptions);
482
- put(input: StorageUploadInput): Promise<StoredObject>;
483
- get(ref: StoredRef): Promise<Buffer>;
1386
+ put(input: StorageUploadInput, control?: TransferControl): Promise<StoredObject>;
1387
+ get(ref: StoredRef, control?: TransferControl): Promise<Buffer>;
1388
+ getStream(ref: StoredRef, control?: TransferControl): Promise<Readable>;
484
1389
  delete(ref: StoredRef): Promise<void>;
485
1390
  exists(ref: StoredRef): Promise<boolean>;
486
1391
  stat(ref: StoredRef): Promise<StoredObject | null>;
1392
+ /**
1393
+ * Validates that the client is connected and the storage channel is
1394
+ * reachable (a real Telegram round-trip on the channel peer).
1395
+ */
1396
+ healthCheck(): Promise<StorageHealth>;
1397
+ private throwIfAborted;
1398
+ private materialize;
1399
+ private documentSize;
1400
+ private requireMessage;
487
1401
  private getMessage;
488
1402
  }
489
1403
 
@@ -525,4 +1439,4 @@ declare class TelegramConnectionService {
525
1439
  private disconnectQuietly;
526
1440
  }
527
1441
 
528
- export { type ApiErrorBody, type DeleteFolderResultDTO, DomainError, ERROR_CODES$1 as ERROR_CODES, type EngineUploadResult, type ErrorCode$1 as ErrorCode, type FileCategory, type FileDTO, type FileDownload, type FileRecord, FileService, type FolderDTO, type FolderDeletionResult, type FolderRecord, FolderService, OmniCloudClient, type OmniCloudClientOptions, OmniCloudError, type Repos, type SearchResultDTO, SearchService, type SessionInfo, type StorageDTO, StorageEngine, type StorageProvider, type StorageRecord, type StorageUploadInput, type StoredObject, type StoredRef, TELEGRAM_PROVIDER, TelegramClientManager, TelegramConnectionService, type TelegramCredentials, type TelegramStorageOptions, TelegramStorageProvider, type TelegramVerifyResponse, type UploadInput, type UploadOptions, type UploadProgress, type UserDTO, type UserRecord, fileCategory, mapProviderError, mapTelegramError, mimeFromFilename, sanitizeFileName, sha256Hex };
1442
+ export { type ActivityEventDTO, type ActivityEventRecord, type ActivityPageDTO, type ActivityRecorder, ActivityService, type ApiErrorBody, type BatchResultDTO, type ConnectionState, type ConnectionStatus, DomainError, type DownloadProgress, type DownloadResult, ERROR_CODES$1 as ERROR_CODES, type EmptyTrashResultDTO, type EngineResolver, type EngineRetryPolicy, type EngineUploadResult, type ErrorCode$1 as ErrorCode, type FileCategory, type FileDTO$1 as FileDTO, type FileDownload, type FileRecord, FileService, type FileTypeBucket, type FileVersionDTO, type FileVersionRecord, type FilesPageDTO, type FolderDTO$1 as FolderDTO, type FolderMutationResult, type FolderMutationResultDTO, type FolderRecord, FolderService, type FoldersPageDTO, type HealthDTO, type HealthStatus, type IntegrityCheckOptions, type IntegrityIssueDTO$1 as IntegrityIssueDTO, type IntegrityReportDTO$1 as IntegrityReportDTO, IntegrityService, type ItemQuery, type ItemStatus, type ListQuery, OmniCloudClient, type OmniCloudClientOptions, OmniCloudError, type PageRequest, type Paged, type PaginationDTO$1 as PaginationDTO, type RecentItemDTO$1 as RecentItemDTO, type RecentPageDTO$1 as RecentPageDTO, RecentService, type Repos, type RetryOptions, type SearchResultDTO$1 as SearchResultDTO, SearchService, type SessionInfo, type SortField, type SortOrder, StatsService, type StorageDTO, StorageEngine, type StorageHealth, type StorageHealthDTO, type StorageProvider, type StorageRecord, type StorageStats, type StorageStatsDTO, type StorageTypeBreakdownDTO, type StorageUploadInput, type StoredObject, type StoredRef, TELEGRAM_PROVIDER, TelegramClientManager, TelegramConnectionService, type TelegramCredentials, type TelegramStorageOptions, TelegramStorageProvider, type TelegramVerifyResponse, type TransferControl, type TransferProgress, type TrashPageDTO, TrashService, type UploadInput, type UploadOptions, type UploadProgress, type UserDTO, type UserRecord, fileCategory, isFileCategory, mapProviderError, mapTelegramError, mimeFromFilename, mimeMatchersForType, noopActivityRecorder, paginationMeta, parseSearchQuery, parseSize, sanitizeFileName, sha256Hex };