@lacrous/omnicloud 0.2.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,8 +1,80 @@
1
- import { ListQuery } from './query';
2
- export { ItemStatus, ListQuery, SortField, SortOrder } from './query';
3
- export { ActivityEventRecord, ActivityRecorder, ActivityService, DomainError, EngineResolver, EngineRetryPolicy, EngineUploadResult, FileDownload, FileRecord, FileService, FileTypeBucket, FileVersionRecord, FolderMutationResult, FolderRecord, FolderService, IntegrityCheckOptions, IntegrityService, ItemQuery, PageRequest, Paged, RecentService, Repos, SearchService, StatsService, StorageEngine, StorageHealth, StorageProvider, StorageRecord, StorageStats, StorageUploadInput, StoredObject, StoredRef, TransferControl, TransferProgress, TrashService, UserRecord, mapProviderError, noopActivityRecorder, sanitizeFileName, sha256Hex } from './errors';
4
- export { ConnectionStatus, TELEGRAM_PROVIDER, TelegramClientManager, TelegramConnectionService, TelegramCredentials, TelegramStorageOptions, TelegramStorageProvider, mapTelegramError } from './client';
5
- export { FileCategory, fileCategory, isFileCategory, mimeFromFilename, mimeMatchersForType, paginationMeta, parseSearchQuery, parseSize } from './mime';
1
+ import { Readable } from 'node:stream';
2
+ import { TelegramClient } from 'telegram';
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
+
64
+ /** Derives a MIME type from the filename extension. Never trusts client input. */
65
+ declare function mimeFromFilename(filename: string): string;
66
+ type FileCategory = "image" | "video" | "audio" | "pdf" | "archive" | "document" | "text" | "other";
67
+ /** Coarse category used for icon selection in the UI. */
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;
6
78
 
7
79
  /**
8
80
  * Data transfer objects, constants and error codes shared between the API,
@@ -48,7 +120,7 @@ type TelegramVerifyResponse = {
48
120
  } | {
49
121
  status: "password_required";
50
122
  };
51
- interface FileDTO {
123
+ interface FileDTO$1 {
52
124
  id: string;
53
125
  name: string;
54
126
  size: number;
@@ -63,7 +135,7 @@ interface FileDTO {
63
135
  createdAt: string;
64
136
  updatedAt: string;
65
137
  }
66
- interface FolderDTO {
138
+ interface FolderDTO$1 {
67
139
  id: string;
68
140
  name: string;
69
141
  parentId: string | null;
@@ -84,10 +156,10 @@ interface FileVersionDTO {
84
156
  isCurrent: boolean;
85
157
  createdAt: string;
86
158
  }
87
- interface SearchResultDTO {
88
- files: FileDTO[];
89
- folders: FolderDTO[];
90
- pagination: PaginationDTO;
159
+ interface SearchResultDTO$1 {
160
+ files: FileDTO$1[];
161
+ folders: FolderDTO$1[];
162
+ pagination: PaginationDTO$1;
91
163
  }
92
164
  /** Result of a recursive folder deletion / trash operation. */
93
165
  interface FolderMutationResultDTO {
@@ -100,42 +172,42 @@ interface EmptyTrashResultDTO {
100
172
  failedFiles: number;
101
173
  }
102
174
 
103
- interface PaginationDTO {
175
+ interface PaginationDTO$1 {
104
176
  page: number;
105
177
  limit: number;
106
178
  total: number;
107
179
  hasMore: boolean;
108
180
  }
109
181
  interface FilesPageDTO {
110
- files: FileDTO[];
111
- pagination: PaginationDTO;
182
+ files: FileDTO$1[];
183
+ pagination: PaginationDTO$1;
112
184
  }
113
185
  interface FoldersPageDTO {
114
- folders: FolderDTO[];
115
- pagination: PaginationDTO;
186
+ folders: FolderDTO$1[];
187
+ pagination: PaginationDTO$1;
116
188
  }
117
189
  interface TrashPageDTO {
118
- files: FileDTO[];
119
- folders: FolderDTO[];
120
- pagination: PaginationDTO;
190
+ files: FileDTO$1[];
191
+ folders: FolderDTO$1[];
192
+ pagination: PaginationDTO$1;
121
193
  }
122
- interface RecentItemDTO {
123
- file: FileDTO;
124
- lastAction: ActivityAction;
194
+ interface RecentItemDTO$1 {
195
+ file: FileDTO$1;
196
+ lastAction: ActivityAction$1;
125
197
  lastActionAt: string;
126
198
  }
127
- interface RecentPageDTO {
128
- items: RecentItemDTO[];
129
- pagination: PaginationDTO;
199
+ interface RecentPageDTO$1 {
200
+ items: RecentItemDTO$1[];
201
+ pagination: PaginationDTO$1;
130
202
  }
131
- declare const ACTIVITY_ACTIONS: readonly ["upload", "download", "open", "rename", "move", "star", "unstar", "trash", "restore", "delete", "create_folder", "replace"];
132
- type ActivityAction = (typeof ACTIVITY_ACTIONS)[number];
133
- declare const RESOURCE_TYPES: readonly ["file", "folder", "storage"];
134
- type ResourceType = (typeof RESOURCE_TYPES)[number];
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];
135
207
  interface ActivityEventDTO {
136
208
  id: string;
137
- action: ActivityAction;
138
- resourceType: ResourceType;
209
+ action: ActivityAction$1;
210
+ resourceType: ResourceType$1;
139
211
  resourceId: string;
140
212
  resourceName: string | null;
141
213
  metadata: Record<string, unknown> | null;
@@ -143,7 +215,7 @@ interface ActivityEventDTO {
143
215
  }
144
216
  interface ActivityPageDTO {
145
217
  events: ActivityEventDTO[];
146
- pagination: PaginationDTO;
218
+ pagination: PaginationDTO$1;
147
219
  }
148
220
  interface StorageTypeBreakdownDTO {
149
221
  category: string;
@@ -158,7 +230,7 @@ interface StorageStatsDTO {
158
230
  trashFileCount: number;
159
231
  starredCount: number;
160
232
  byType: StorageTypeBreakdownDTO[];
161
- largestFiles: FileDTO[];
233
+ largestFiles: FileDTO$1[];
162
234
  quotaBytes: number | null;
163
235
  }
164
236
  interface HealthDTO {
@@ -168,20 +240,22 @@ interface HealthDTO {
168
240
  uptimeSeconds: number;
169
241
  version: string;
170
242
  }
171
- interface IntegrityIssueDTO {
243
+ interface IntegrityIssueDTO$1 {
172
244
  fileId: string;
245
+ /** Set when the issue is with a historical version rather than the current object. */
246
+ versionId?: string;
173
247
  name: string;
174
248
  kind: "missing" | "size_mismatch" | "unreadable" | "hash_mismatch";
175
249
  detail: string | null;
176
250
  }
177
- interface IntegrityReportDTO {
251
+ interface IntegrityReportDTO$1 {
178
252
  checkedAt: string;
179
253
  filesChecked: number;
180
254
  healthy: number;
181
255
  missing: number;
182
256
  inconsistent: number;
183
257
  unreadable: number;
184
- issues: IntegrityIssueDTO[];
258
+ issues: IntegrityIssueDTO$1[];
185
259
  durationMs: number;
186
260
  }
187
261
  interface BatchResultDTO {
@@ -190,12 +264,12 @@ interface BatchResultDTO {
190
264
  failed: number;
191
265
  errors: {
192
266
  id: string;
193
- code: ErrorCode;
267
+ code: ErrorCode$1;
194
268
  message: string;
195
269
  }[];
196
270
  }
197
271
  /** Stable machine-readable error codes returned in API error bodies. */
198
- declare const ERROR_CODES: {
272
+ declare const ERROR_CODES$1: {
199
273
  readonly AUTH_REQUIRED: "AUTH_REQUIRED";
200
274
  readonly AUTH_INVALID: "AUTH_INVALID";
201
275
  readonly PERMISSION_DENIED: "PERMISSION_DENIED";
@@ -216,10 +290,10 @@ declare const ERROR_CODES: {
216
290
  readonly INTEGRITY_CHECK_FAILED: "INTEGRITY_CHECK_FAILED";
217
291
  readonly INTERNAL: "INTERNAL_ERROR";
218
292
  };
219
- type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
293
+ type ErrorCode$1 = (typeof ERROR_CODES$1)[keyof typeof ERROR_CODES$1];
220
294
  interface ApiErrorBody {
221
295
  error: {
222
- code: ErrorCode;
296
+ code: ErrorCode$1;
223
297
  message: string;
224
298
  details?: unknown;
225
299
  requestId?: string;
@@ -269,8 +343,14 @@ interface UploadOptions {
269
343
  retry?: number;
270
344
  }
271
345
  interface UploadInput {
272
- /** File content: Blob/File in the browser, Buffer/Uint8Array in Node. */
273
- data: Blob | Uint8Array;
346
+ /**
347
+ * File content: a Blob/File, a Uint8Array/Buffer, a web ReadableStream, or (in
348
+ * Node) a Readable stream. Streams are read in chunks; the caller never has to
349
+ * build one large buffer.
350
+ */
351
+ data?: Blob | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
352
+ /** Node only: a path to read the file from. Use instead of `data`. */
353
+ path?: string;
274
354
  /** Filename; also used to derive the MIME type server-side. */
275
355
  name: string;
276
356
  }
@@ -279,6 +359,13 @@ interface DownloadProgress {
279
359
  total: number | null;
280
360
  percent: number | null;
281
361
  }
362
+ interface StreamedDownload {
363
+ stream: ReadableStream<Uint8Array>;
364
+ contentType: string;
365
+ /** Declared byte length, or null when the server did not send one. */
366
+ size: number | null;
367
+ sha256: string | null;
368
+ }
282
369
  interface DownloadResult {
283
370
  data: Buffer | Uint8Array;
284
371
  contentType: string;
@@ -321,6 +408,12 @@ declare class OmniCloudClient {
321
408
  /** @internal */
322
409
  withRetry<T>(operation: () => Promise<T>): Promise<T>;
323
410
  /** @internal */
411
+ /**
412
+ * Opens a binary response as a stream. Bytes are delivered as they arrive, so
413
+ * memory does not grow with the file. The stream errors with DOWNLOAD_INCOMPLETE
414
+ * if the body ends before Content-Length.
415
+ */
416
+ requestStream(path: string, onProgress?: (p: DownloadProgress) => void): Promise<StreamedDownload>;
324
417
  requestBinary(path: string, onProgress?: (p: DownloadProgress) => void): Promise<DownloadResult>;
325
418
  }
326
419
  declare class FilesApi {
@@ -328,26 +421,42 @@ declare class FilesApi {
328
421
  constructor(client: OmniCloudClient);
329
422
  /** `folderId: null` = root; omit it to list every active file. */
330
423
  list(query?: ListQuery): Promise<FilesPageDTO>;
331
- get(id: string): Promise<FileDTO>;
424
+ get(id: string): Promise<FileDTO$1>;
332
425
  versions(id: string): Promise<FileVersionDTO[]>;
333
426
  /**
334
427
  * Uploads a file with progress and retry support. Uses XMLHttpRequest when
335
428
  * available (browsers) so progress and cancellation work; falls back to
336
429
  * fetch elsewhere.
337
430
  */
338
- upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO>;
431
+ upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO$1>;
339
432
  private uploadOnce;
340
433
  private uploadViaXhr;
341
434
  /** Uploads a new version of an existing file. */
342
- replace(id: string, input: UploadInput): Promise<FileDTO>;
435
+ replace(id: string, input: UploadInput): Promise<FileDTO$1>;
343
436
  download(id: string, onProgress?: (p: DownloadProgress) => void): Promise<DownloadResult>;
437
+ /**
438
+ * Streams a file's bytes without holding the whole file in memory. Use this for
439
+ * large files. Read `stream` to the end: a truncated transfer errors instead of
440
+ * ending quietly.
441
+ */
442
+ downloadStream(id: string, onProgress?: (p: DownloadProgress) => void): Promise<StreamedDownload>;
443
+ /**
444
+ * Node only. Streams a file to `targetPath` through a `.part` file that is
445
+ * renamed into place only after the full length is verified. A truncated or
446
+ * failed transfer leaves nothing at `targetPath`.
447
+ */
448
+ downloadToFile(id: string, targetPath: string, onProgress?: (p: DownloadProgress) => void): Promise<{
449
+ path: string;
450
+ bytes: number;
451
+ sha256: string | null;
452
+ }>;
344
453
  /** URL suitable for direct download (anchor href). */
345
454
  downloadUrl(id: string): string;
346
- rename(id: string, name: string): Promise<FileDTO>;
347
- star(id: string, starred?: boolean): Promise<FileDTO>;
348
- move(id: string, folderId: string | null): Promise<FileDTO>;
349
- trash(id: string): Promise<FileDTO>;
350
- restore(id: string): Promise<FileDTO>;
455
+ rename(id: string, name: string): Promise<FileDTO$1>;
456
+ star(id: string, starred?: boolean): Promise<FileDTO$1>;
457
+ move(id: string, folderId: string | null): Promise<FileDTO$1>;
458
+ trash(id: string): Promise<FileDTO$1>;
459
+ restore(id: string): Promise<FileDTO$1>;
351
460
  /** Permanently deletes a file and its Telegram objects. */
352
461
  delete(id: string): Promise<void>;
353
462
  /** Batch operation: trash | restore | delete | star | unstar | move. */
@@ -362,11 +471,11 @@ declare class FoldersApi {
362
471
  parentId?: string | null;
363
472
  }): Promise<FoldersPageDTO>;
364
473
  /** The full active folder tree (for move dialogs). */
365
- tree(): Promise<FolderDTO[]>;
366
- create(name: string, parentId?: string | null): Promise<FolderDTO>;
367
- rename(id: string, name: string): Promise<FolderDTO>;
368
- star(id: string, starred?: boolean): Promise<FolderDTO>;
369
- move(id: string, parentId: string | null): Promise<FolderDTO>;
474
+ tree(): Promise<FolderDTO$1[]>;
475
+ create(name: string, parentId?: string | null): Promise<FolderDTO$1>;
476
+ rename(id: string, name: string): Promise<FolderDTO$1>;
477
+ star(id: string, starred?: boolean): Promise<FolderDTO$1>;
478
+ move(id: string, parentId: string | null): Promise<FolderDTO$1>;
370
479
  trash(id: string): Promise<FolderMutationResultDTO>;
371
480
  restore(id: string): Promise<FolderMutationResultDTO>;
372
481
  /** Permanently deletes the folder subtree and its Telegram objects. */
@@ -397,7 +506,7 @@ declare class StorageApi {
397
506
  /** Read-only integrity scan; nothing is repaired or deleted. */
398
507
  integrityCheck(options?: {
399
508
  deep?: boolean;
400
- }): Promise<IntegrityReportDTO>;
509
+ }): Promise<IntegrityReportDTO$1>;
401
510
  /** Public server health (no authentication required). */
402
511
  serverHealth(): Promise<HealthDTO>;
403
512
  }
@@ -411,7 +520,7 @@ declare class SearchApi {
411
520
  query(q: string, page?: {
412
521
  page?: number;
413
522
  limit?: number;
414
- }): Promise<SearchResultDTO>;
523
+ }): Promise<SearchResultDTO$1>;
415
524
  }
416
525
  declare class RecentApi {
417
526
  private readonly client;
@@ -419,7 +528,7 @@ declare class RecentApi {
419
528
  list(page?: {
420
529
  page?: number;
421
530
  limit?: number;
422
- }): Promise<RecentPageDTO>;
531
+ }): Promise<RecentPageDTO$1>;
423
532
  }
424
533
  declare class ActivityApi {
425
534
  private readonly client;
@@ -444,5 +553,1092 @@ declare class AuthApi {
444
553
 
445
554
  /** Connection state of the underlying storage backend. */
446
555
  type ConnectionState = "DISCONNECTED" | "CONNECTING" | "CONNECTED" | "RECONNECTING" | "ERROR" | "AUTH_REQUIRED";
556
+ interface FileDTO {
557
+ id: string;
558
+ name: string;
559
+ size: number;
560
+ mimeType: string;
561
+ sha256: string;
562
+ folderId: string | null;
563
+ starred: boolean;
564
+ trashed: boolean;
565
+ deletedAt: string | null;
566
+ versionCount: number;
567
+ currentVersionId: string | null;
568
+ createdAt: string;
569
+ updatedAt: string;
570
+ }
571
+ interface FolderDTO {
572
+ id: string;
573
+ name: string;
574
+ parentId: string | null;
575
+ starred: boolean;
576
+ trashed: boolean;
577
+ deletedAt: string | null;
578
+ createdAt: string;
579
+ updatedAt: string;
580
+ }
581
+ interface SearchResultDTO {
582
+ files: FileDTO[];
583
+ folders: FolderDTO[];
584
+ pagination: PaginationDTO;
585
+ }
586
+ interface PaginationDTO {
587
+ page: number;
588
+ limit: number;
589
+ total: number;
590
+ hasMore: boolean;
591
+ }
592
+ interface RecentItemDTO {
593
+ file: FileDTO;
594
+ lastAction: ActivityAction;
595
+ lastActionAt: string;
596
+ }
597
+ interface RecentPageDTO {
598
+ items: RecentItemDTO[];
599
+ pagination: PaginationDTO;
600
+ }
601
+ declare const ACTIVITY_ACTIONS: readonly ["upload", "download", "open", "rename", "move", "star", "unstar", "trash", "restore", "delete", "create_folder", "replace"];
602
+ type ActivityAction = (typeof ACTIVITY_ACTIONS)[number];
603
+ declare const RESOURCE_TYPES: readonly ["file", "folder", "storage"];
604
+ type ResourceType = (typeof RESOURCE_TYPES)[number];
605
+ interface IntegrityIssueDTO {
606
+ fileId: string;
607
+ /** Set when the issue is with a historical version rather than the current object. */
608
+ versionId?: string;
609
+ name: string;
610
+ kind: "missing" | "size_mismatch" | "unreadable" | "hash_mismatch";
611
+ detail: string | null;
612
+ }
613
+ interface IntegrityReportDTO {
614
+ checkedAt: string;
615
+ filesChecked: number;
616
+ healthy: number;
617
+ missing: number;
618
+ inconsistent: number;
619
+ unreadable: number;
620
+ issues: IntegrityIssueDTO[];
621
+ durationMs: number;
622
+ }
623
+ /** Stable machine-readable error codes returned in API error bodies. */
624
+ declare const ERROR_CODES: {
625
+ readonly AUTH_REQUIRED: "AUTH_REQUIRED";
626
+ readonly AUTH_INVALID: "AUTH_INVALID";
627
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
628
+ readonly INVALID_REQUEST: "INVALID_REQUEST";
629
+ readonly NOT_FOUND: "NOT_FOUND";
630
+ readonly FILE_NOT_FOUND: "FILE_NOT_FOUND";
631
+ readonly FOLDER_NOT_FOUND: "FOLDER_NOT_FOUND";
632
+ readonly CONFLICT: "CONFLICT";
633
+ readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
634
+ readonly RATE_LIMITED: "RATE_LIMITED";
635
+ readonly STORAGE_NOT_INITIALIZED: "STORAGE_NOT_INITIALIZED";
636
+ readonly STORAGE_UNAVAILABLE: "STORAGE_UNAVAILABLE";
637
+ readonly TELEGRAM_AUTH_REQUIRED: "TELEGRAM_AUTH_REQUIRED";
638
+ readonly TELEGRAM_CONNECTION_FAILED: "TELEGRAM_CONNECTION_FAILED";
639
+ readonly TELEGRAM_FILE_NOT_FOUND: "TELEGRAM_FILE_NOT_FOUND";
640
+ readonly UPLOAD_FAILED: "UPLOAD_FAILED";
641
+ readonly DOWNLOAD_FAILED: "DOWNLOAD_FAILED";
642
+ readonly INTEGRITY_CHECK_FAILED: "INTEGRITY_CHECK_FAILED";
643
+ readonly INTERNAL: "INTERNAL_ERROR";
644
+ };
645
+ type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
646
+
647
+ /**
648
+ * Base class for all domain errors. The API maps these to structured HTTP
649
+ * responses; everything else becomes a 500.
650
+ */
651
+ declare class DomainError extends Error {
652
+ readonly code: ErrorCode;
653
+ readonly status: number;
654
+ readonly details?: unknown;
655
+ /** Whether repeating the same operation may succeed. Defaults to false. */
656
+ readonly retryable: boolean;
657
+ /** Server-requested delay before a retry, in seconds, when the provider gave one. */
658
+ readonly retryAfterSeconds?: number;
659
+ constructor(code: ErrorCode, status: number, message: string, details?: unknown, retry?: {
660
+ retryable?: boolean;
661
+ retryAfterSeconds?: number;
662
+ });
663
+ }
664
+ /**
665
+ * Wraps an arbitrary provider failure as an UploadFailedError (the default
666
+ * storage-operation error), preserving domain errors untouched.
667
+ */
668
+ declare function mapProviderError(message: string, error: unknown): DomainError;
669
+
670
+ /**
671
+ * Domain records. Repository implementations (e.g. the Prisma layer) map
672
+ * their persistence models onto these records; the rest of the application
673
+ * only ever works with them.
674
+ *
675
+ * Telegram identifiers are represented as strings/numbers that fit in
676
+ * JavaScript's safe integer range; the persistence layer converts as needed.
677
+ */
678
+
679
+ interface UserRecord {
680
+ id: string;
681
+ telegramUserId: string;
682
+ username: string | null;
683
+ firstName: string | null;
684
+ lastName: string | null;
685
+ phone: string | null;
686
+ createdAt: Date;
687
+ }
688
+ interface TelegramSessionRecord {
689
+ userId: string;
690
+ stringSession: string;
691
+ }
692
+ interface StorageRecord {
693
+ id: string;
694
+ userId: string;
695
+ provider: string;
696
+ title: string;
697
+ telegramChatId: string;
698
+ telegramAccessHash: string;
699
+ createdAt: Date;
700
+ }
701
+ interface FolderRecord {
702
+ id: string;
703
+ userId: string;
704
+ parentId: string | null;
705
+ name: string;
706
+ starred: boolean;
707
+ /** Soft-delete marker (null = active). */
708
+ deletedAt: Date | null;
709
+ /**
710
+ * Groups everything moved to the Trash by one operation, so restoring a
711
+ * folder restores exactly what it took (and leaves separately-trashed
712
+ * items alone). Null for items trashed individually.
713
+ */
714
+ trashBatchId: string | null;
715
+ createdAt: Date;
716
+ updatedAt: Date;
717
+ }
718
+ interface FileRecord {
719
+ id: string;
720
+ userId: string;
721
+ folderId: string | null;
722
+ name: string;
723
+ size: number;
724
+ mimeType: string;
725
+ sha256: string;
726
+ telegramMessageId: number;
727
+ starred: boolean;
728
+ /** Soft-delete marker (null = active). */
729
+ deletedAt: Date | null;
730
+ /** See FolderRecord.trashBatchId. */
731
+ trashBatchId: string | null;
732
+ /** Current version row, if versioning has been initialized for this file. */
733
+ currentVersionId: string | null;
734
+ versionCount: number;
735
+ createdAt: Date;
736
+ updatedAt: Date;
737
+ }
738
+ interface FileVersionRecord {
739
+ id: string;
740
+ fileId: string;
741
+ userId: string;
742
+ versionNumber: number;
743
+ size: number;
744
+ mimeType: string;
745
+ sha256: string;
746
+ telegramMessageId: number;
747
+ createdAt: Date;
748
+ }
749
+ interface ActivityEventRecord {
750
+ id: string;
751
+ userId: string;
752
+ action: ActivityAction;
753
+ resourceType: ResourceType;
754
+ resourceId: string;
755
+ resourceName: string | null;
756
+ metadata: Record<string, unknown> | null;
757
+ createdAt: Date;
758
+ }
759
+ /** Resolved, repository-ready item query (search parser output resolved). */
760
+ interface ItemQuery {
761
+ /** Restrict to a folder (null = root). Undefined = any folder. */
762
+ folderId?: string | null;
763
+ /** Restrict to a set of folder ids (used for `folder:Name` filters). */
764
+ folderIds?: string[];
765
+ nameContains?: string;
766
+ /** MIME matchers: exact values or prefixes ending in "/". */
767
+ mimeMatchers?: readonly string[];
768
+ ext?: string;
769
+ starred?: boolean;
770
+ /** false = active only, true = trashed only, undefined = both. */
771
+ trashed?: boolean;
772
+ minSize?: number;
773
+ maxSize?: number;
774
+ from?: Date;
775
+ to?: Date;
776
+ sort?: SortField;
777
+ order?: SortOrder;
778
+ }
779
+ interface PageRequest {
780
+ page: number;
781
+ limit: number;
782
+ }
783
+ interface Paged<T> {
784
+ items: T[];
785
+ total: number;
786
+ }
787
+ interface FileTypeBucket {
788
+ category: string;
789
+ bytes: number;
790
+ count: number;
791
+ }
792
+ interface StorageStats {
793
+ fileCount: number;
794
+ folderCount: number;
795
+ totalBytes: number;
796
+ trashBytes: number;
797
+ trashFileCount: number;
798
+ starredCount: number;
799
+ byType: FileTypeBucket[];
800
+ largestFiles: FileRecord[];
801
+ /** Optional quota ceiling (null = unlimited, the v0.2 default). */
802
+ quotaBytes: number | null;
803
+ }
804
+
805
+ /**
806
+ * Upload operations are the durable record of one logical upload. They exist so
807
+ * that a retried request (same operation id) can resume or replay instead of
808
+ * writing a second Telegram object and a second file record.
809
+ *
810
+ * PENDING accepted, Telegram write not started
811
+ * UPLOADING Telegram write in progress or its outcome unknown
812
+ * COMPLETED Telegram object stored and file record committed; result is final
813
+ * FAILED Telegram write definitively failed; the operation may be retried
814
+ * under a new id
815
+ */
816
+ type UploadOperationStatus = "PENDING" | "UPLOADING" | "COMPLETED" | "FAILED";
817
+
818
+ /**
819
+ * Repository interfaces. The application depends on these, not on a specific
820
+ * database — the Prisma implementation lives in @omnicloud/database, and
821
+ * tests use in-memory implementations.
822
+ */
823
+ interface UserRepository {
824
+ findById(id: string): Promise<UserRecord | null>;
825
+ findByTelegramId(telegramUserId: string): Promise<UserRecord | null>;
826
+ upsertFromTelegram(input: {
827
+ telegramUserId: string;
828
+ username: string | null;
829
+ firstName: string | null;
830
+ lastName: string | null;
831
+ phone: string | null;
832
+ }): Promise<UserRecord>;
833
+ }
834
+ interface SessionRepository {
835
+ get(userId: string): Promise<TelegramSessionRecord | null>;
836
+ save(userId: string, stringSession: string): Promise<void>;
837
+ delete(userId: string): Promise<void>;
838
+ }
839
+ interface BrowserSessionRecord {
840
+ id: string;
841
+ userId: string;
842
+ tokenHash: string;
843
+ createdAt: Date;
844
+ expiresAt: Date;
845
+ revokedAt: Date | null;
846
+ lastUsedAt: Date | null;
847
+ userAgent: string | null;
848
+ ip: string | null;
849
+ }
850
+ /** Server-side browser sessions. Holds only token hashes, never raw tokens. */
851
+ interface BrowserSessionRepository {
852
+ create(input: {
853
+ userId: string;
854
+ tokenHash: string;
855
+ expiresAt: Date;
856
+ userAgent: string | null;
857
+ ip: string | null;
858
+ }): Promise<BrowserSessionRecord>;
859
+ findByTokenHash(tokenHash: string): Promise<BrowserSessionRecord | null>;
860
+ touch(id: string, at: Date): Promise<void>;
861
+ revoke(id: string, at: Date): Promise<void>;
862
+ revokeAllForUser(userId: string, at: Date): Promise<number>;
863
+ listActiveForUser(userId: string, now: Date): Promise<BrowserSessionRecord[]>;
864
+ deleteExpiredBefore(cutoff: Date): Promise<number>;
865
+ }
866
+ interface StorageRepository {
867
+ findByUserAndProvider(userId: string, provider: string): Promise<StorageRecord | null>;
868
+ create(input: {
869
+ userId: string;
870
+ provider: string;
871
+ title: string;
872
+ telegramChatId: string;
873
+ telegramAccessHash: string;
874
+ }): Promise<StorageRecord>;
875
+ }
876
+ interface FolderCreateInput {
877
+ userId: string;
878
+ parentId: string | null;
879
+ name: string;
880
+ }
881
+ interface FolderRepository {
882
+ create(input: FolderCreateInput): Promise<FolderRecord>;
883
+ /**
884
+ * Re-parents a folder only if the result stays acyclic. The ancestor check and
885
+ * the write are one atomic step, so two concurrent moves cannot each pass the
886
+ * check and together create a cycle. Throws ConflictError on a cycle.
887
+ */
888
+ moveSafely(id: string, newParentId: string | null): Promise<FolderRecord>;
889
+ findById(id: string): Promise<FolderRecord | null>;
890
+ /** Every folder belonging to the user (used for trees and cascade logic). */
891
+ listByUser(userId: string): Promise<FolderRecord[]>;
892
+ listChildren(userId: string, parentId: string | null): Promise<FolderRecord[]>;
893
+ /** Filtered, sorted, paginated listing. */
894
+ query(userId: string, query: ItemQuery, page: PageRequest): Promise<Paged<FolderRecord>>;
895
+ /** Folder ids matching a name substring (used to resolve `folder:` filters). */
896
+ findIdsByName(userId: string, name: string): Promise<string[]>;
897
+ update(id: string, patch: {
898
+ name?: string;
899
+ parentId?: string | null;
900
+ starred?: boolean;
901
+ deletedAt?: Date | null;
902
+ trashBatchId?: string | null;
903
+ }): Promise<FolderRecord>;
904
+ /** Bulk trash/restore/star without touching files. */
905
+ updateMany(ids: string[], patch: {
906
+ starred?: boolean;
907
+ deletedAt?: Date | null;
908
+ trashBatchId?: string | null;
909
+ }): Promise<number>;
910
+ deleteMany(ids: string[]): Promise<void>;
911
+ countByUser(userId: string): Promise<number>;
912
+ }
913
+ interface FileCreateInput {
914
+ userId: string;
915
+ folderId: string | null;
916
+ name: string;
917
+ size: number;
918
+ mimeType: string;
919
+ sha256: string;
920
+ telegramMessageId: number;
921
+ }
922
+ interface UploadOperationRecord {
923
+ id: string;
924
+ userId: string;
925
+ operationId: string;
926
+ status: UploadOperationStatus;
927
+ /** Set once the Telegram object exists, so recovery can commit without re-uploading. */
928
+ telegramMessageId: number | null;
929
+ sha256: string | null;
930
+ size: number | null;
931
+ fileId: string | null;
932
+ error: string | null;
933
+ createdAt: Date;
934
+ updatedAt: Date;
935
+ }
936
+ /** Durable upload attempts, scoped to the owning user. */
937
+ interface UploadOperationRepository {
938
+ findByOperationId(userId: string, operationId: string): Promise<UploadOperationRecord | null>;
939
+ /** Operations for a user in the given states (used by reconciliation). */
940
+ listByStatus(userId: string, status: UploadOperationStatus): Promise<UploadOperationRecord[]>;
941
+ create(input: {
942
+ userId: string;
943
+ operationId: string;
944
+ }): Promise<UploadOperationRecord>;
945
+ /**
946
+ * Atomically moves an operation from `from` to `to` if, and only if, it is
947
+ * still in `from`. Returns false when another request already moved it, so
948
+ * exactly one request performs the upload.
949
+ */
950
+ claim(id: string, from: UploadOperationStatus, to: UploadOperationStatus): Promise<boolean>;
951
+ update(id: string, patch: Partial<Pick<UploadOperationRecord, "status" | "telegramMessageId" | "sha256" | "size" | "fileId" | "error">>): Promise<UploadOperationRecord>;
952
+ }
953
+ interface FileRepository {
954
+ create(input: FileCreateInput): Promise<FileRecord>;
955
+ findById(id: string): Promise<FileRecord | null>;
956
+ /** Active files directly inside a folder (null = root). */
957
+ listByFolder(userId: string, folderId: string | null): Promise<FileRecord[]>;
958
+ listByUser(userId: string): Promise<FileRecord[]>;
959
+ listByIds(userId: string, ids: string[]): Promise<FileRecord[]>;
960
+ query(userId: string, query: ItemQuery, page: PageRequest): Promise<Paged<FileRecord>>;
961
+ update(id: string, patch: {
962
+ name?: string;
963
+ folderId?: string | null;
964
+ starred?: boolean;
965
+ deletedAt?: Date | null;
966
+ trashBatchId?: string | null;
967
+ size?: number;
968
+ mimeType?: string;
969
+ sha256?: string;
970
+ telegramMessageId?: number;
971
+ currentVersionId?: string | null;
972
+ versionCount?: number;
973
+ }): Promise<FileRecord>;
974
+ updateMany(ids: string[], patch: {
975
+ folderId?: string | null;
976
+ starred?: boolean;
977
+ deletedAt?: Date | null;
978
+ trashBatchId?: string | null;
979
+ }): Promise<number>;
980
+ /** Aggregate counters used by the storage dashboard. */
981
+ statsByUser(userId: string): Promise<{
982
+ fileCount: number;
983
+ folderCount: number;
984
+ totalBytes: number;
985
+ trashBytes: number;
986
+ trashFileCount: number;
987
+ starredCount: number;
988
+ }>;
989
+ deleteMany(ids: string[]): Promise<void>;
990
+ createVersion(input: {
991
+ fileId: string;
992
+ userId: string;
993
+ size: number;
994
+ mimeType: string;
995
+ sha256: string;
996
+ telegramMessageId: number;
997
+ }): Promise<FileVersionRecord>;
998
+ listVersions(fileId: string): Promise<FileVersionRecord[]>;
999
+ /** All versions for many files in one query, grouped by file id. */
1000
+ listVersionsForFiles(fileIds: string[]): Promise<Map<string, FileVersionRecord[]>>;
1001
+ findVersionById(versionId: string): Promise<FileVersionRecord | null>;
1002
+ countVersions(fileId: string): Promise<number>;
1003
+ deleteVersionsByFileIds(fileIds: string[]): Promise<void>;
1004
+ }
1005
+ interface ActivityRepository {
1006
+ record(input: {
1007
+ userId: string;
1008
+ action: ActivityAction;
1009
+ resourceType: ResourceType;
1010
+ resourceId: string;
1011
+ resourceName?: string | null;
1012
+ metadata?: Record<string, unknown> | null;
1013
+ }): Promise<void>;
1014
+ /** Newest-first activity for a user. */
1015
+ list(userId: string, page: PageRequest): Promise<Paged<ActivityEventRecord>>;
1016
+ /**
1017
+ * Most recent distinct files for the given actions, newest first. Only
1018
+ * events whose action is listed are considered, so a file whose latest
1019
+ * event is (say) a star still surfaces via its last meaningful action.
1020
+ */
1021
+ recentFiles(userId: string, actions: readonly ActivityAction[], limit: number): Promise<ActivityEventRecord[]>;
1022
+ countByUser(userId: string): Promise<number>;
1023
+ /** Housekeeping: drop events older than the retention window. */
1024
+ pruneOlderThan(cutoff: Date): Promise<number>;
1025
+ }
1026
+ interface Repos {
1027
+ users: UserRepository;
1028
+ sessions: SessionRepository;
1029
+ browserSessions: BrowserSessionRepository;
1030
+ uploadOperations: UploadOperationRepository;
1031
+ storages: StorageRepository;
1032
+ folders: FolderRepository;
1033
+ files: FileRepository;
1034
+ activity: ActivityRepository;
1035
+ }
1036
+
1037
+ /**
1038
+ * Generic storage abstraction. The application must never depend on
1039
+ * Telegram-specific details — it talks to a StorageProvider.
1040
+ *
1041
+ * v0.2 adds streaming, progress reporting, cancellation and health checks
1042
+ * while keeping the abstraction provider-agnostic.
1043
+ */
1044
+ /** Reference to an object inside the backing store (opaque to callers). */
1045
+ interface StoredRef {
1046
+ messageId: string;
1047
+ }
1048
+ interface StoredObject extends StoredRef {
1049
+ name: string;
1050
+ size: number;
1051
+ mimeType: string;
1052
+ }
1053
+ /** Transfer progress emitted during upload/download. */
1054
+ interface TransferProgress {
1055
+ /** Bytes transferred so far. */
1056
+ transferred: number;
1057
+ /** Total bytes, when known upfront. */
1058
+ total: number | null;
1059
+ /** 0–100 when total is known, otherwise null. */
1060
+ percent: number | null;
1061
+ }
1062
+ interface TransferControl {
1063
+ /** Aborted when the caller cancels the operation. */
1064
+ signal?: AbortSignal;
1065
+ onProgress?: (progress: TransferProgress) => void;
1066
+ }
1067
+ /**
1068
+ * Upload input. Exactly one of `data` (small, in memory) or `path` (a spooled
1069
+ * file on disk, read in slices by the provider) must be given.
1070
+ */
1071
+ interface StorageUploadInput {
1072
+ name: string;
1073
+ mimeType: string;
1074
+ /** Whole-object buffer (small files). */
1075
+ data?: Buffer;
1076
+ /** Path to a spooled file (large files). Its bytes are never fully loaded. */
1077
+ path?: string;
1078
+ /** Total byte length of the object. Required with `path`. */
1079
+ size?: number;
1080
+ /** SHA-256 of the spooled bytes, computed while they were written. Required with `path`. */
1081
+ sha256?: string;
1082
+ }
1083
+ interface StorageHealth {
1084
+ /** Reachable and usable. */
1085
+ healthy: boolean;
1086
+ /** Round-trip latency of the probe in milliseconds, when measured. */
1087
+ latencyMs: number | null;
1088
+ message: string | null;
1089
+ /** Name of the storage channel/target, when available. */
1090
+ targetTitle: string | null;
1091
+ }
1092
+ interface StorageProvider {
1093
+ readonly name: string;
1094
+ /**
1095
+ * Uploads an object and returns the stored reference. Reports progress and
1096
+ * honors `control.signal` for cancellation.
1097
+ */
1098
+ put(input: StorageUploadInput, control?: TransferControl): Promise<StoredObject>;
1099
+ /** Downloads the object's content as a Buffer. */
1100
+ get(ref: StoredRef, control?: TransferControl): Promise<Buffer>;
1101
+ /** Opens a stream for the object (large-file friendly). */
1102
+ getStream(ref: StoredRef, control?: TransferControl): Promise<Readable>;
1103
+ /** Deletes the object. Deleting an already-missing object must not throw. */
1104
+ delete(ref: StoredRef): Promise<void>;
1105
+ exists(ref: StoredRef): Promise<boolean>;
1106
+ stat(ref: StoredRef): Promise<StoredObject | null>;
1107
+ /**
1108
+ * Lists the objects the backing store holds, newest first, read-only. Used by
1109
+ * reconciliation; it must never modify anything. Implementations page through
1110
+ * the store and stop once `limit` objects are returned.
1111
+ */
1112
+ listObjects(limit: number): Promise<StoredObject[]>;
1113
+ /** Validates that the backing store is reachable and usable. */
1114
+ healthCheck(): Promise<StorageHealth>;
1115
+ }
1116
+
1117
+ declare function sha256Hex(data: Uint8Array): string;
1118
+
1119
+ interface EngineUploadResult {
1120
+ stored: StoredObject;
1121
+ sha256: string;
1122
+ size: number;
1123
+ }
1124
+ interface EngineRetryPolicy {
1125
+ /** Total attempts, including the first. */
1126
+ attempts: number;
1127
+ /** Base delay in ms; doubles per retry. */
1128
+ baseDelayMs: number;
1129
+ }
1130
+ /**
1131
+ * Sits between the application and the raw StorageProvider and adds the
1132
+ * cross-cutting guarantees: SHA-256 integrity, streaming, progress reporting,
1133
+ * cancellation and bounded retries.
1134
+ *
1135
+ * The active provider is chosen per user by the host application (each user
1136
+ * has their own Telegram storage channel).
1137
+ */
1138
+ declare class StorageEngine {
1139
+ private readonly provider;
1140
+ private readonly retryPolicy;
1141
+ constructor(provider: StorageProvider, retryPolicy?: EngineRetryPolicy);
1142
+ get providerName(): string;
1143
+ /**
1144
+ * Uploads an object, computing its SHA-256 as it goes and reporting
1145
+ * progress. Retries transient failures with exponential backoff.
1146
+ */
1147
+ upload(input: StorageUploadInput, control?: TransferControl): Promise<EngineUploadResult>;
1148
+ download(ref: StoredRef, control?: TransferControl): Promise<Buffer>;
1149
+ /**
1150
+ * Streams the object and verifies its SHA-256 against `expectedSha256`.
1151
+ *
1152
+ * The last chunk is held back until the digest of everything before it has
1153
+ * been checked, so a corrupt object errors the stream before its final bytes
1154
+ * are released. A consumer therefore never sees a complete-looking body that
1155
+ * fails the checksum. Memory stays bounded to about two provider chunks.
1156
+ */
1157
+ downloadStream(ref: StoredRef, expectedSha256: string, control?: TransferControl): Promise<Readable>;
1158
+ remove(ref: StoredRef): Promise<void>;
1159
+ exists(ref: StoredRef): Promise<boolean>;
1160
+ /** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
1161
+ list(limit: number): Promise<StoredObject[]>;
1162
+ stat(ref: StoredRef): Promise<StoredObject | null>;
1163
+ health(): Promise<StorageHealth>;
1164
+ verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
1165
+ private withRetry;
1166
+ }
1167
+
1168
+ interface OperationSink {
1169
+ info(record: Record<string, unknown>, message: string): void;
1170
+ warn(record: Record<string, unknown>, message: string): void;
1171
+ }
1172
+
1173
+ interface RecordActivityInput {
1174
+ userId: string;
1175
+ action: ActivityAction;
1176
+ resourceType: ResourceType;
1177
+ resourceId: string;
1178
+ resourceName?: string | null;
1179
+ metadata?: Record<string, unknown> | null;
1180
+ }
1181
+ /**
1182
+ * The narrow interface other services depend on for activity logging, so a
1183
+ * no-op recorder can be substituted in tests and activity failures never
1184
+ * break the operation being logged.
1185
+ */
1186
+ interface ActivityRecorder {
1187
+ record(input: RecordActivityInput): Promise<void>;
1188
+ }
1189
+ /** Activity log with best-effort recording and retention pruning. */
1190
+ declare class ActivityService implements ActivityRecorder {
1191
+ private readonly activity;
1192
+ /** Days to retain events; older rows are pruned on demand. */
1193
+ private readonly retentionDays;
1194
+ constructor(activity: ActivityRepository,
1195
+ /** Days to retain events; older rows are pruned on demand. */
1196
+ retentionDays?: number);
1197
+ /**
1198
+ * Records an event. Never throws — a logging failure must not fail the
1199
+ * user-facing operation that produced it.
1200
+ */
1201
+ record(input: RecordActivityInput): Promise<void>;
1202
+ list(userId: string, page: PageRequest): Promise<Paged<ActivityEventRecord>>;
1203
+ /** Most recent distinct files for the given actions, for the Recent view. */
1204
+ recentFiles(userId: string, actions: readonly ActivityAction[], limit: number): Promise<ActivityEventRecord[]>;
1205
+ prune(): Promise<number>;
1206
+ }
1207
+ /** A recorder that discards events (used where activity is not wired up). */
1208
+ declare const noopActivityRecorder: ActivityRecorder;
1209
+
1210
+ interface FileDownload {
1211
+ record: FileRecord;
1212
+ data: Buffer;
1213
+ integrityVerified: boolean;
1214
+ }
1215
+ interface FileStreamDownload {
1216
+ record: FileRecord;
1217
+ /** Errors with IntegrityCheckError instead of ending if the bytes do not match. */
1218
+ stream: Readable;
1219
+ }
1220
+ type EngineResolver = (userId: string) => Promise<StorageEngine>;
1221
+ interface FileUploadInput {
1222
+ folderId: string | null;
1223
+ name: string;
1224
+ /** In-memory content. Use `spooled` for anything large. */
1225
+ data?: Buffer;
1226
+ /** A file already written to disk by `spoolToFile`, with its checksum. */
1227
+ spooled?: {
1228
+ path: string;
1229
+ size: number;
1230
+ sha256: string;
1231
+ };
1232
+ /** When set, uploads a new version of this file instead of creating one. */
1233
+ replaceFileId?: string;
1234
+ /** Client-supplied id that makes the upload idempotent across retries. */
1235
+ operationId?: string;
1236
+ /** Progress callback forwarded from the transport layer. */
1237
+ onProgress?: (progress: TransferProgress) => void;
1238
+ signal?: AbortSignal;
1239
+ }
1240
+ /**
1241
+ * File metadata operations. Every method verifies ownership: records
1242
+ * belonging to other users are reported as missing.
1243
+ *
1244
+ * Reliability rules honored here:
1245
+ * - metadata is written only after the storage upload succeeds;
1246
+ * - permanent deletion removes the remote object before the metadata;
1247
+ * - client-supplied ownership is never trusted (userId is server-derived).
1248
+ */
1249
+ declare class FileService {
1250
+ private readonly files;
1251
+ private readonly folders;
1252
+ private readonly engineFor;
1253
+ private readonly activity;
1254
+ private readonly operations;
1255
+ private readonly log;
1256
+ constructor(files: FileRepository, folders: FolderRepository, engineFor: EngineResolver, activity?: ActivityRecorder, operations?: UploadOperationRepository | null, log?: OperationSink | null);
1257
+ upload(userId: string, input: FileUploadInput): Promise<FileRecord>;
1258
+ /**
1259
+ * Idempotent upload keyed by a client-supplied operation id. A retry never
1260
+ * writes a second Telegram object: a completed operation returns its file, and
1261
+ * an operation whose object was already stored is committed without uploading.
1262
+ */
1263
+ private uploadOnce;
1264
+ /**
1265
+ * Follows an operation another request is driving, for a bounded time. Returns
1266
+ * the finished file if the winner completes, or the stored object's commit if
1267
+ * it has stored one. Returns null when the winner is still in progress after the
1268
+ * budget, so the caller can report a conflict instead of waiting forever.
1269
+ */
1270
+ private followWinner;
1271
+ /** Commits metadata for an object that is already in Telegram. */
1272
+ private commitStoredObject;
1273
+ private performUpload;
1274
+ /** Persists the file (or the new version of a file) for an object already stored. */
1275
+ private commitRecord;
1276
+ /** Uploads a new version of an existing file and makes it current. */
1277
+ private replaceVersion;
1278
+ list(userId: string, folderId: string | null): Promise<FileRecord[]>;
1279
+ query(userId: string, input: ListQuery, page: PageRequest): Promise<Paged<FileRecord>>;
1280
+ get(userId: string, id: string): Promise<FileRecord>;
1281
+ /** Like get(), but rejects trashed files (for download/open). */
1282
+ getActive(userId: string, id: string): Promise<FileRecord>;
1283
+ listVersions(userId: string, id: string): Promise<FileVersionRecord[]>;
1284
+ rename(userId: string, id: string, name: string): Promise<FileRecord>;
1285
+ move(userId: string, id: string, folderId: string | null): Promise<FileRecord>;
1286
+ setStarred(userId: string, id: string, starred: boolean): Promise<FileRecord>;
1287
+ /**
1288
+ * Moves a file to Trash (soft delete). The remote object is untouched, so
1289
+ * restore is lossless.
1290
+ */
1291
+ trash(userId: string, id: string): Promise<FileRecord>;
1292
+ /** Restores a trashed file to its previous folder (or root). */
1293
+ restore(userId: string, id: string): Promise<FileRecord>;
1294
+ /**
1295
+ * Permanently deletes a file: the Telegram object is removed first, then the
1296
+ * metadata. If the remote delete fails we keep the metadata so the user can
1297
+ * retry, rather than silently orphaning the storage object.
1298
+ */
1299
+ deletePermanently(userId: string, id: string): Promise<void>;
1300
+ private performPermanentDelete;
1301
+ /**
1302
+ * v0.1-compatible hard delete used by the current API: permanently removes
1303
+ * the file. Retained so existing clients keep working.
1304
+ */
1305
+ delete(userId: string, id: string): Promise<void>;
1306
+ download(userId: string, id: string, control?: TransferControl): Promise<FileDownload>;
1307
+ /**
1308
+ * Streaming download for large files. The caller must consume `stream` to
1309
+ * the end and then await `verified` to learn whether the bytes match the
1310
+ * stored checksum. The download activity is recorded when the stream opens.
1311
+ */
1312
+ downloadStreamed(userId: string, id: string, control?: TransferControl): Promise<FileStreamDownload>;
1313
+ private performStreamedDownload;
1314
+ /** Records an "open" event (the UI opening the details panel, say). */
1315
+ touch(userId: string, id: string, action?: "open"): Promise<void>;
1316
+ /**
1317
+ * Applies an operation to many files, collecting per-item failures instead
1318
+ * of aborting the whole batch.
1319
+ */
1320
+ batch(userId: string, ids: string[], operation: "trash" | "restore" | "delete" | "star" | "unstar" | "move", options?: {
1321
+ folderId?: string | null;
1322
+ operationId?: string;
1323
+ }): Promise<{
1324
+ operationId?: string;
1325
+ requested: number;
1326
+ succeeded: number;
1327
+ failed: number;
1328
+ errors: {
1329
+ id: string;
1330
+ message: string;
1331
+ }[];
1332
+ }>;
1333
+ private assertFolder;
1334
+ }
1335
+
1336
+ interface FolderMutationResult {
1337
+ affectedFolders: number;
1338
+ affectedFiles: number;
1339
+ }
1340
+ /**
1341
+ * Folder lifecycle (v0.2):
1342
+ *
1343
+ * - Folders are virtual OmniCloud objects with a parent-child hierarchy.
1344
+ * - Trashing a folder trashes its whole subtree (folders + files) without
1345
+ * touching Telegram, so restore is lossless.
1346
+ * - Restoring a folder restores only the nodes that were trashed together
1347
+ * with it; anything the user trashed separately stays in the Trash.
1348
+ * - Permanent deletion removes remote objects first, then metadata.
1349
+ */
1350
+ declare class FolderService {
1351
+ private readonly folders;
1352
+ private readonly files;
1353
+ private readonly engineFor;
1354
+ private readonly activity;
1355
+ constructor(folders: FolderRepository, files: FileRepository, engineFor: EngineResolver, activity?: ActivityRecorder);
1356
+ create(userId: string, input: {
1357
+ name: string;
1358
+ parentId: string | null;
1359
+ }): Promise<FolderRecord>;
1360
+ /** Active folders directly inside a folder (null = root). */
1361
+ listChildren(userId: string, parentId: string | null): Promise<FolderRecord[]>;
1362
+ query(userId: string, input: ListQuery, page: PageRequest): Promise<Paged<FolderRecord>>;
1363
+ /** Every active folder for the user (used to build trees). */
1364
+ tree(userId: string): Promise<FolderRecord[]>;
1365
+ get(userId: string, id: string): Promise<FolderRecord>;
1366
+ rename(userId: string, id: string, name: string): Promise<FolderRecord>;
1367
+ move(userId: string, id: string, newParentId: string | null): Promise<FolderRecord>;
1368
+ setStarred(userId: string, id: string, starred: boolean): Promise<FolderRecord>;
1369
+ /**
1370
+ * Moves a folder and its entire subtree to the Trash. Nothing is deleted
1371
+ * from Telegram, so restore is lossless.
1372
+ */
1373
+ trash(userId: string, id: string): Promise<FolderMutationResult>;
1374
+ /**
1375
+ * Restores a folder subtree. Only nodes trashed at the same instant as the
1376
+ * folder itself are restored, so items the user trashed separately stay put.
1377
+ */
1378
+ restore(userId: string, id: string): Promise<FolderMutationResult>;
1379
+ /**
1380
+ * Permanently deletes a folder subtree: remote objects first, then
1381
+ * metadata. Files whose remote delete fails are left in the Trash so the
1382
+ * user can retry rather than losing data silently.
1383
+ */
1384
+ deletePermanently(userId: string, id: string): Promise<FolderMutationResult>;
1385
+ /**
1386
+ * v0.1-compatible recursive delete. Now routes through the v0.2 permanent
1387
+ * delete so remote cleanup and orphan prevention are consistent.
1388
+ */
1389
+ delete(userId: string, id: string): Promise<FolderMutationResult>;
1390
+ /** The folder plus every descendant (BFS), optionally including trashed. */
1391
+ private collectSubtree;
1392
+ /** Ids of files inside the given folders that are not already trashed. */
1393
+ private collectFileIds;
1394
+ }
1395
+
1396
+ interface SearchResult {
1397
+ files: FileRecord[];
1398
+ folders: FolderRecord[];
1399
+ pagination: PaginationDTO;
1400
+ }
1401
+ /**
1402
+ * PostgreSQL-backed search (no Elasticsearch, per the v0.2 scope).
1403
+ *
1404
+ * The user query is parsed into structured filters (`type:`, `size:`,
1405
+ * `folder:`, …) and evaluated by the repository layer, so results respect
1406
+ * ownership and stay safely parameterized.
1407
+ */
1408
+ declare class SearchService {
1409
+ private readonly files;
1410
+ private readonly folders;
1411
+ constructor(files: FileRepository, folders: FolderRepository);
1412
+ search(userId: string, rawQuery: string, page: PageRequest, overrides?: ListQuery): Promise<SearchResult>;
1413
+ /** Convenience wrapper returning the shared DTO shape. */
1414
+ searchDTO(userId: string, rawQuery: string, page: PageRequest): Promise<Omit<SearchResultDTO, "files" | "folders"> & SearchResult>;
1415
+ }
1416
+
1417
+ /**
1418
+ * Storage dashboard statistics, computed from PostgreSQL metadata only —
1419
+ * no Telegram round-trips, so the page is fast and works offline.
1420
+ */
1421
+ declare class StatsService {
1422
+ private readonly files;
1423
+ private readonly folders;
1424
+ constructor(files: FileRepository, folders: FolderRepository);
1425
+ stats(userId: string, quotaBytes?: number | null): Promise<StorageStats>;
1426
+ }
1427
+
1428
+ interface TrashListing {
1429
+ files: FileRecord[];
1430
+ folders: FolderRecord[];
1431
+ pagination: PaginationDTO;
1432
+ }
1433
+ interface EmptyTrashResult {
1434
+ deletedFiles: number;
1435
+ deletedFolders: number;
1436
+ failedFiles: number;
1437
+ }
1438
+ /**
1439
+ * Trash is a virtual view over soft-deleted rows. Listing shows only the
1440
+ * explicitly trashed items; emptying removes their remote objects before the
1441
+ * metadata, and reports failures instead of hiding them.
1442
+ */
1443
+ declare class TrashService {
1444
+ private readonly files;
1445
+ private readonly folders;
1446
+ private readonly engineFor;
1447
+ constructor(files: FileRepository, folders: FolderRepository, engineFor: EngineResolver);
1448
+ list(userId: string, page: PageRequest, extra?: ListQuery): Promise<TrashListing>;
1449
+ /**
1450
+ * Permanently removes everything in the Trash for a user. Folders are
1451
+ * processed root-first so removing a parent also removes its contents.
1452
+ */
1453
+ empty(userId: string): Promise<EmptyTrashResult>;
1454
+ /** Restores a single trashed item (used by the batch restore endpoint). */
1455
+ verifyTrashedFile(userId: string, id: string): Promise<FileRecord>;
1456
+ }
1457
+
1458
+ interface IntegrityCheckOptions {
1459
+ /** Re-download and re-hash objects (slow but authoritative). */
1460
+ deep?: boolean;
1461
+ /** Maximum files to inspect in one pass. */
1462
+ limit?: number;
1463
+ }
1464
+ /**
1465
+ * Detects drift between PostgreSQL metadata and Telegram storage:
1466
+ *
1467
+ * - `missing` — metadata exists, the Telegram message is gone
1468
+ * - `size_mismatch` — the stored object's size differs from the metadata
1469
+ * - `hash_mismatch` — deep mode only: downloaded bytes don't match sha256
1470
+ * - `unreadable` — the provider errored while inspecting the object
1471
+ *
1472
+ * The checker is strictly read-only: repairs are a separate, explicit step so
1473
+ * nothing is ever deleted silently (see the v0.2 reliability rules).
1474
+ */
1475
+ declare class IntegrityService {
1476
+ private readonly files;
1477
+ private readonly engineFor;
1478
+ constructor(files: FileRepository, engineFor: EngineResolver);
1479
+ check(userId: string, options?: IntegrityCheckOptions): Promise<IntegrityReportDTO>;
1480
+ private inspect;
1481
+ private inspectObject;
1482
+ }
1483
+
1484
+ /**
1485
+ * Recent view: the most recently touched active files, newest activity first.
1486
+ * Trashed and permanently-deleted files are filtered out so the view never
1487
+ * surfaces items the user cannot open.
1488
+ */
1489
+ declare class RecentService {
1490
+ private readonly activity;
1491
+ private readonly files;
1492
+ constructor(activity: ActivityRepository, files: FileRepository);
1493
+ list(userId: string, page: PageRequest): Promise<RecentPageDTO>;
1494
+ }
1495
+
1496
+ /**
1497
+ * Sanitizes a client-supplied filename into a safe, single path component.
1498
+ * Returns an empty string when nothing usable remains.
1499
+ */
1500
+ declare function sanitizeFileName(name: string): string;
1501
+
1502
+ interface TelegramCredentials {
1503
+ apiId: number;
1504
+ apiHash: string;
1505
+ }
1506
+ interface ConnectionStatus {
1507
+ state: ConnectionState;
1508
+ lastError: string | null;
1509
+ lastCheckAt: Date | null;
1510
+ healthy: boolean | null;
1511
+ }
1512
+ /**
1513
+ * Owns one Telegram (MTProto) connection per authenticated user and tracks an
1514
+ * explicit lifecycle state for each:
1515
+ *
1516
+ * DISCONNECTED → CONNECTING → CONNECTED
1517
+ * ↘ RECONNECTING → CONNECTED
1518
+ * ↘ ERROR | AUTH_REQUIRED
1519
+ *
1520
+ * Responsibilities: connect, disconnect, reconnect, validateSession,
1521
+ * healthCheck, refreshConnection. Sessions never leave the server.
1522
+ */
1523
+ declare class TelegramClientManager {
1524
+ private readonly credentials;
1525
+ private readonly sessions;
1526
+ private connections;
1527
+ private lastReconnectAt;
1528
+ constructor(credentials: TelegramCredentials, sessions: SessionRepository);
1529
+ /** Returns a connected client, (re)connecting when needed. */
1530
+ getForUser(userId: string): Promise<TelegramClient>;
1531
+ /** Connection status for health reporting and the storage dashboard. */
1532
+ statusForUser(userId: string): ConnectionStatus;
1533
+ /**
1534
+ * Probes a user's connection. Cheap by default (connection state only);
1535
+ * `deep` performs a real Telegram round-trip to validate the session.
1536
+ */
1537
+ healthCheck(userId: string, deep?: boolean): Promise<StorageHealth>;
1538
+ /** Registers an already-authenticated client (from the login flow). */
1539
+ cacheClient(userId: string, client: TelegramClient): void;
1540
+ /** Drops a user's connection (e.g. after logout) without touching storage. */
1541
+ disconnectUser(userId: string): Promise<void>;
1542
+ disconnectAll(): Promise<void>;
1543
+ private isConnected;
1544
+ private connectUser;
1545
+ private reconnect;
1546
+ private establish;
1547
+ }
1548
+
1549
+ /**
1550
+ * Maps a Telegram (GramJS) failure to a structured domain error with an
1551
+ * appropriate HTTP status and a client-facing message that never leaks
1552
+ * credentials or session material.
1553
+ */
1554
+ declare function mapTelegramError(error: unknown, fallbackMessage: string): DomainError;
1555
+
1556
+ interface TelegramStorageOptions {
1557
+ /** Raw channel id of the user's private storage channel. */
1558
+ chatId: string;
1559
+ /** Channel access hash required to build the input peer. */
1560
+ accessHash: string;
1561
+ /** Human-readable channel title, surfaced in health reporting. */
1562
+ title?: string;
1563
+ }
1564
+ /**
1565
+ * StorageProvider implementation backed by the user's private Telegram
1566
+ * channel. Files are sent as force-downloaded documents (no compression), so
1567
+ * every upload is a document message in the channel.
1568
+ *
1569
+ * v0.2 adds progress reporting, cancellation, streamed downloads and a
1570
+ * health check, all translated into the provider-agnostic core vocabulary.
1571
+ */
1572
+ declare class TelegramStorageProvider implements StorageProvider {
1573
+ private readonly client;
1574
+ readonly name = "telegram";
1575
+ private readonly peer;
1576
+ private readonly title;
1577
+ constructor(client: TelegramClient, options: TelegramStorageOptions);
1578
+ put(input: StorageUploadInput, control?: TransferControl): Promise<StoredObject>;
1579
+ get(ref: StoredRef, control?: TransferControl): Promise<Buffer>;
1580
+ /**
1581
+ * Streams the object in Telegram-sized chunks, so memory use stays bounded
1582
+ * by one chunk regardless of file size. The first request happens here so
1583
+ * that a missing message fails before the caller starts consuming bytes.
1584
+ */
1585
+ getStream(ref: StoredRef, control?: TransferControl): Promise<Readable>;
1586
+ delete(ref: StoredRef): Promise<void>;
1587
+ exists(ref: StoredRef): Promise<boolean>;
1588
+ /**
1589
+ * Read-only listing of stored documents in the channel, newest first, up to
1590
+ * `limit`. Only messages that carry a document are returned; text messages and
1591
+ * service messages are skipped. Nothing is modified.
1592
+ */
1593
+ listObjects(limit: number): Promise<StoredObject[]>;
1594
+ stat(ref: StoredRef): Promise<StoredObject | null>;
1595
+ /**
1596
+ * Validates that the client is connected and the storage channel is
1597
+ * reachable (a real Telegram round-trip on the channel peer).
1598
+ */
1599
+ healthCheck(): Promise<StorageHealth>;
1600
+ private throwIfAborted;
1601
+ private documentSize;
1602
+ private requireMessage;
1603
+ private getMessage;
1604
+ }
1605
+
1606
+ declare const TELEGRAM_PROVIDER = "telegram";
1607
+ type VerifyCodeResult = {
1608
+ status: "ok";
1609
+ user: UserRecord;
1610
+ } | {
1611
+ status: "password_required";
1612
+ };
1613
+ /**
1614
+ * Orchestrates the full Telegram account connection flow:
1615
+ * phone → code (→ 2FA password) → session persisted → user upserted →
1616
+ * private storage channel created.
1617
+ */
1618
+ declare class TelegramConnectionService {
1619
+ private readonly repos;
1620
+ private readonly clientManager;
1621
+ private readonly credentials;
1622
+ private flows;
1623
+ constructor(repos: Repos, clientManager: TelegramClientManager, credentials: TelegramCredentials);
1624
+ /** Sends the Telegram confirmation code for the phone number. */
1625
+ startLogin(phone: string): Promise<void>;
1626
+ /** Verifies the confirmation code. Returns `password_required` for 2FA accounts. */
1627
+ verifyCode(phone: string, code: string): Promise<VerifyCodeResult>;
1628
+ /** Completes sign-in for accounts with two-factor authentication enabled. */
1629
+ verifyPassword(phone: string, password: string): Promise<VerifyCodeResult>;
1630
+ /** Aborts a pending login flow (disconnects the client). */
1631
+ cancelLogin(phone: string): Promise<void>;
1632
+ /**
1633
+ * Ensures the user has a registered private storage channel, creating it
1634
+ * on first use. Idempotent.
1635
+ */
1636
+ ensureStorage(userId: string): Promise<StorageRecord>;
1637
+ ensureStorageForClient(userId: string, client: TelegramClient): Promise<StorageRecord>;
1638
+ private requireFlow;
1639
+ private finalizeLogin;
1640
+ private sweepExpiredFlows;
1641
+ private disconnectQuietly;
1642
+ }
447
1643
 
448
- export { type ActivityEventDTO, type ActivityPageDTO, type ApiErrorBody, type BatchResultDTO, type ConnectionState, type DownloadProgress, type DownloadResult, ERROR_CODES, type EmptyTrashResultDTO, type ErrorCode, type FileDTO, type FileVersionDTO, type FilesPageDTO, type FolderDTO, type FolderMutationResultDTO, type FoldersPageDTO, type HealthDTO, type HealthStatus, type IntegrityIssueDTO, type IntegrityReportDTO, OmniCloudClient, type OmniCloudClientOptions, OmniCloudError, type PaginationDTO, type RecentItemDTO, type RecentPageDTO, type RetryOptions, type SearchResultDTO, type SessionInfo, type StorageDTO, type StorageHealthDTO, type StorageStatsDTO, type StorageTypeBreakdownDTO, type TelegramVerifyResponse, type TrashPageDTO, type UploadInput, type UploadOptions, type UploadProgress, type UserDTO };
1644
+ 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, type StreamedDownload, 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 };