@lacrous/omnicloud 0.0.0-stage → 0.1.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.
@@ -0,0 +1,528 @@
1
+ import { TelegramClient } from 'telegram';
2
+
3
+ /** Derives a MIME type from the filename extension. Never trusts client input. */
4
+ declare function mimeFromFilename(filename: string): string;
5
+ type FileCategory = "image" | "video" | "audio" | "pdf" | "archive" | "document" | "text" | "other";
6
+ /** Coarse category used for icon selection in the UI. */
7
+ declare function fileCategory(mimeType: string): FileCategory;
8
+
9
+ /**
10
+ * Data transfer objects and constants shared between the API, the web app,
11
+ * and the @lacrous/omnicloud SDK.
12
+ *
13
+ * Telegram identifiers that may exceed JavaScript's safe integer range are
14
+ * transported as strings across the API boundary.
15
+ */
16
+ interface UserDTO {
17
+ id: string;
18
+ username: string | null;
19
+ firstName: string | null;
20
+ lastName: string | null;
21
+ }
22
+ interface StorageDTO {
23
+ id: string;
24
+ provider: string;
25
+ title: string;
26
+ createdAt: string;
27
+ }
28
+ /** Returned by GET /api/auth/me. `user` is null when not signed in. */
29
+ interface SessionInfo {
30
+ user: UserDTO | null;
31
+ storage: StorageDTO | null;
32
+ }
33
+ interface FileDTO {
34
+ id: string;
35
+ name: string;
36
+ size: number;
37
+ mimeType: string;
38
+ sha256: string;
39
+ folderId: string | null;
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ }
43
+ interface FolderDTO {
44
+ id: string;
45
+ name: string;
46
+ parentId: string | null;
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ }
50
+ interface SearchResultDTO {
51
+ files: FileDTO[];
52
+ folders: FolderDTO[];
53
+ }
54
+ interface DeleteFolderResultDTO {
55
+ deletedFolders: number;
56
+ deletedFiles: number;
57
+ }
58
+ /** POST /api/auth/telegram/verify response. */
59
+ type TelegramVerifyResponse = {
60
+ status: "ok";
61
+ user: UserDTO;
62
+ } | {
63
+ status: "password_required";
64
+ };
65
+ /** Stable machine-readable error codes returned in API error bodies. */
66
+ declare const ERROR_CODES$1: {
67
+ readonly VALIDATION: "VALIDATION_ERROR";
68
+ readonly UNAUTHENTICATED: "UNAUTHENTICATED";
69
+ readonly FORBIDDEN: "FORBIDDEN";
70
+ readonly NOT_FOUND: "NOT_FOUND";
71
+ readonly CONFLICT: "CONFLICT";
72
+ readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
73
+ readonly RATE_LIMITED: "RATE_LIMITED";
74
+ readonly STORAGE_NOT_INITIALIZED: "STORAGE_NOT_INITIALIZED";
75
+ readonly TELEGRAM_ERROR: "TELEGRAM_ERROR";
76
+ readonly INTERNAL: "INTERNAL_ERROR";
77
+ };
78
+ type ErrorCode$1 = (typeof ERROR_CODES$1)[keyof typeof ERROR_CODES$1];
79
+ interface ApiErrorBody {
80
+ error: {
81
+ code: ErrorCode$1;
82
+ message: string;
83
+ details?: unknown;
84
+ };
85
+ }
86
+
87
+ declare class OmniCloudError extends Error {
88
+ readonly status: number;
89
+ readonly code: string;
90
+ constructor(status: number, code: string, message: string);
91
+ }
92
+ interface OmniCloudClientOptions {
93
+ /** Base URL of an OmniCloud server. Defaults to same-origin ("" is kept as-is). */
94
+ baseUrl?: string;
95
+ /** Custom fetch implementation (e.g. for tests). */
96
+ fetchImpl?: typeof fetch;
97
+ }
98
+ interface UploadProgress {
99
+ loaded: number;
100
+ total: number;
101
+ percent: number;
102
+ }
103
+ interface UploadOptions {
104
+ folderId?: string | null;
105
+ onProgress?: (progress: UploadProgress) => void;
106
+ }
107
+ interface UploadInput {
108
+ /** File content: a File/Blob in the browser, or a Buffer/Uint8Array in Node. */
109
+ data: Blob | Uint8Array;
110
+ name: string;
111
+ }
112
+ /**
113
+ * HTTP client for an OmniCloud server.
114
+ *
115
+ * Works in browsers (cookie sessions, upload progress via XHR) and Node.js
116
+ * (fetch fallback, cookie forwarding is the caller's responsibility).
117
+ */
118
+ declare class OmniCloudClient {
119
+ private readonly baseUrl;
120
+ private readonly fetchImpl;
121
+ constructor(options?: OmniCloudClientOptions);
122
+ me(): Promise<SessionInfo>;
123
+ startTelegramLogin(phone: string): Promise<void>;
124
+ verifyTelegramCode(phone: string, code: string): Promise<TelegramVerifyResponse>;
125
+ submitTelegramPassword(phone: string, password: string): Promise<{
126
+ status: "ok";
127
+ user: UserDTO;
128
+ }>;
129
+ ensureStorage(): Promise<StorageDTO>;
130
+ 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
+ }
153
+
154
+ /** Stable machine-readable error codes returned in API error bodies. */
155
+ declare const ERROR_CODES: {
156
+ readonly VALIDATION: "VALIDATION_ERROR";
157
+ readonly UNAUTHENTICATED: "UNAUTHENTICATED";
158
+ readonly FORBIDDEN: "FORBIDDEN";
159
+ readonly NOT_FOUND: "NOT_FOUND";
160
+ readonly CONFLICT: "CONFLICT";
161
+ readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
162
+ readonly RATE_LIMITED: "RATE_LIMITED";
163
+ readonly STORAGE_NOT_INITIALIZED: "STORAGE_NOT_INITIALIZED";
164
+ readonly TELEGRAM_ERROR: "TELEGRAM_ERROR";
165
+ readonly INTERNAL: "INTERNAL_ERROR";
166
+ };
167
+ type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
168
+
169
+ /**
170
+ * Base class for all domain errors. The API maps these to structured HTTP
171
+ * responses; everything else becomes a 500.
172
+ */
173
+ declare class DomainError extends Error {
174
+ readonly code: ErrorCode;
175
+ readonly status: number;
176
+ readonly details?: unknown;
177
+ constructor(code: ErrorCode, status: number, message: string, details?: unknown);
178
+ }
179
+ /** The underlying storage backend (e.g. Telegram) failed. */
180
+ declare class StorageProviderError extends DomainError {
181
+ constructor(message: string, details?: unknown);
182
+ }
183
+
184
+ /**
185
+ * Domain records. Repository implementations (e.g. the Prisma layer) map
186
+ * their persistence models onto these records; the rest of the application
187
+ * only ever works with them.
188
+ *
189
+ * Telegram identifiers are represented as strings/numbers that fit in
190
+ * JavaScript's safe integer range; the persistence layer converts as needed.
191
+ */
192
+ interface UserRecord {
193
+ id: string;
194
+ telegramUserId: string;
195
+ username: string | null;
196
+ firstName: string | null;
197
+ lastName: string | null;
198
+ phone: string | null;
199
+ createdAt: Date;
200
+ }
201
+ interface TelegramSessionRecord {
202
+ userId: string;
203
+ stringSession: string;
204
+ }
205
+ interface StorageRecord {
206
+ id: string;
207
+ userId: string;
208
+ provider: string;
209
+ title: string;
210
+ telegramChatId: string;
211
+ telegramAccessHash: string;
212
+ createdAt: Date;
213
+ }
214
+ interface FolderRecord {
215
+ id: string;
216
+ userId: string;
217
+ parentId: string | null;
218
+ name: string;
219
+ createdAt: Date;
220
+ updatedAt: Date;
221
+ }
222
+ interface FileRecord {
223
+ id: string;
224
+ userId: string;
225
+ folderId: string | null;
226
+ name: string;
227
+ size: number;
228
+ mimeType: string;
229
+ sha256: string;
230
+ telegramMessageId: number;
231
+ createdAt: Date;
232
+ updatedAt: Date;
233
+ }
234
+
235
+ /**
236
+ * Repository interfaces. The application depends on these, not on a specific
237
+ * database — the Prisma implementation lives in @omnicloud/database, and
238
+ * tests use in-memory implementations.
239
+ */
240
+ interface UserRepository {
241
+ findById(id: string): Promise<UserRecord | null>;
242
+ findByTelegramId(telegramUserId: string): Promise<UserRecord | null>;
243
+ upsertFromTelegram(input: {
244
+ telegramUserId: string;
245
+ username: string | null;
246
+ firstName: string | null;
247
+ lastName: string | null;
248
+ phone: string | null;
249
+ }): Promise<UserRecord>;
250
+ }
251
+ interface SessionRepository {
252
+ get(userId: string): Promise<TelegramSessionRecord | null>;
253
+ save(userId: string, stringSession: string): Promise<void>;
254
+ delete(userId: string): Promise<void>;
255
+ }
256
+ interface StorageRepository {
257
+ findByUserAndProvider(userId: string, provider: string): Promise<StorageRecord | null>;
258
+ create(input: {
259
+ userId: string;
260
+ provider: string;
261
+ title: string;
262
+ telegramChatId: string;
263
+ telegramAccessHash: string;
264
+ }): Promise<StorageRecord>;
265
+ }
266
+ interface FolderRepository {
267
+ create(input: {
268
+ userId: string;
269
+ parentId: string | null;
270
+ name: string;
271
+ }): Promise<FolderRecord>;
272
+ findById(id: string): Promise<FolderRecord | null>;
273
+ /** Every folder belonging to the user (used for trees and cascade deletes). */
274
+ listByUser(userId: string): Promise<FolderRecord[]>;
275
+ listChildren(userId: string, parentId: string | null): Promise<FolderRecord[]>;
276
+ update(id: string, patch: {
277
+ name?: string;
278
+ parentId?: string | null;
279
+ }): Promise<FolderRecord>;
280
+ deleteMany(ids: string[]): Promise<void>;
281
+ searchByName(userId: string, query: string, limit: number): Promise<FolderRecord[]>;
282
+ }
283
+ 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>;
293
+ findById(id: string): Promise<FileRecord | null>;
294
+ listByFolder(userId: string, folderId: string | null): Promise<FileRecord[]>;
295
+ listByUser(userId: string): Promise<FileRecord[]>;
296
+ update(id: string, patch: {
297
+ name?: string;
298
+ folderId?: string | null;
299
+ }): Promise<FileRecord>;
300
+ deleteMany(ids: string[]): Promise<void>;
301
+ searchByName(userId: string, query: string, limit: number): Promise<FileRecord[]>;
302
+ }
303
+ interface Repos {
304
+ users: UserRepository;
305
+ sessions: SessionRepository;
306
+ storages: StorageRepository;
307
+ folders: FolderRepository;
308
+ files: FileRepository;
309
+ }
310
+
311
+ /**
312
+ * Generic storage abstraction. The application must never depend on
313
+ * Telegram-specific details — it talks to a StorageProvider.
314
+ */
315
+ /** Reference to an object inside the backing store (opaque to callers). */
316
+ interface StoredRef {
317
+ messageId: string;
318
+ }
319
+ interface StoredObject extends StoredRef {
320
+ name: string;
321
+ size: number;
322
+ mimeType: string;
323
+ }
324
+ interface StorageUploadInput {
325
+ name: string;
326
+ mimeType: string;
327
+ data: Buffer;
328
+ }
329
+ interface StorageProvider {
330
+ 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>;
335
+ /** Deletes the object. Deleting an already-missing object must not throw. */
336
+ delete(ref: StoredRef): Promise<void>;
337
+ exists(ref: StoredRef): Promise<boolean>;
338
+ stat(ref: StoredRef): Promise<StoredObject | null>;
339
+ }
340
+
341
+ declare function sha256Hex(data: Uint8Array): string;
342
+
343
+ interface EngineUploadResult {
344
+ stored: StoredObject;
345
+ sha256: string;
346
+ size: number;
347
+ }
348
+ /**
349
+ * Sits between the application and the raw StorageProvider and adds the
350
+ * cross-cutting guarantees: SHA-256 integrity and consistent error mapping.
351
+ *
352
+ * The active provider is chosen per user by the host application (each user
353
+ * has their own Telegram storage channel).
354
+ */
355
+ declare class StorageEngine {
356
+ private readonly provider;
357
+ constructor(provider: StorageProvider);
358
+ 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>;
362
+ remove(ref: StoredRef): Promise<void>;
363
+ verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
364
+ }
365
+ declare function mapProviderError(message: string, error: unknown): StorageProviderError;
366
+
367
+ interface FileDownload {
368
+ record: FileRecord;
369
+ data: Buffer;
370
+ integrityVerified: boolean;
371
+ }
372
+ type EngineResolver = (userId: string) => Promise<StorageEngine>;
373
+ /**
374
+ * All file metadata operations. Every method verifies ownership: records
375
+ * belonging to other users are reported as missing.
376
+ */
377
+ declare class FileService {
378
+ private readonly files;
379
+ private readonly folders;
380
+ 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>;
387
+ list(userId: string, folderId: string | null): Promise<FileRecord[]>;
388
+ get(userId: string, id: string): Promise<FileRecord>;
389
+ rename(userId: string, id: string, name: string): Promise<FileRecord>;
390
+ move(userId: string, id: string, folderId: string | null): Promise<FileRecord>;
391
+ delete(userId: string, id: string): Promise<void>;
392
+ download(userId: string, id: string): Promise<FileDownload>;
393
+ }
394
+
395
+ interface FolderDeletionResult {
396
+ deletedFolders: number;
397
+ deletedFiles: number;
398
+ }
399
+ /**
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.
404
+ */
405
+ declare class FolderService {
406
+ private readonly folders;
407
+ private readonly files;
408
+ private readonly engineFor;
409
+ constructor(folders: FolderRepository, files: FileRepository, engineFor: EngineResolver);
410
+ create(userId: string, input: {
411
+ name: string;
412
+ parentId: string | null;
413
+ }): Promise<FolderRecord>;
414
+ listChildren(userId: string, parentId: string | null): Promise<FolderRecord[]>;
415
+ tree(userId: string): Promise<FolderRecord[]>;
416
+ get(userId: string, id: string): Promise<FolderRecord>;
417
+ rename(userId: string, id: string, name: string): Promise<FolderRecord>;
418
+ move(userId: string, id: string, newParentId: string | null): Promise<FolderRecord>;
419
+ delete(userId: string, id: string): Promise<FolderDeletionResult>;
420
+ }
421
+
422
+ /** Metadata-based search over file and folder names (PostgreSQL-backed). */
423
+ declare class SearchService {
424
+ private readonly files;
425
+ private readonly folders;
426
+ constructor(files: FileRepository, folders: FolderRepository);
427
+ search(userId: string, query: string, limit?: number): Promise<{
428
+ files: FileRecord[];
429
+ folders: FolderRecord[];
430
+ }>;
431
+ }
432
+
433
+ /**
434
+ * Sanitizes a client-supplied filename into a safe, single path component.
435
+ * Returns an empty string when nothing usable remains.
436
+ */
437
+ declare function sanitizeFileName(name: string): string;
438
+
439
+ interface TelegramCredentials {
440
+ apiId: number;
441
+ apiHash: string;
442
+ }
443
+ /**
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.
447
+ */
448
+ declare class TelegramClientManager {
449
+ private readonly credentials;
450
+ private readonly sessions;
451
+ private clients;
452
+ constructor(credentials: TelegramCredentials, sessions: SessionRepository);
453
+ getForUser(userId: string): Promise<TelegramClient>;
454
+ /** Registers an already-authenticated client (e.g. from the login flow). */
455
+ cacheClient(userId: string, client: TelegramClient): void;
456
+ disconnectAll(): Promise<void>;
457
+ private connect;
458
+ }
459
+
460
+ /**
461
+ * Maps a Telegram (GramJS) failure to a structured domain error with an
462
+ * appropriate HTTP status and client-facing message.
463
+ */
464
+ declare function mapTelegramError(error: unknown, fallbackMessage: string): DomainError;
465
+
466
+ interface TelegramStorageOptions {
467
+ /** Raw channel id of the user's private storage channel. */
468
+ chatId: string;
469
+ /** Channel access hash required to build the input peer. */
470
+ accessHash: string;
471
+ }
472
+ /**
473
+ * 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.
476
+ */
477
+ declare class TelegramStorageProvider implements StorageProvider {
478
+ private readonly client;
479
+ readonly name = "telegram";
480
+ private readonly peer;
481
+ constructor(client: TelegramClient, options: TelegramStorageOptions);
482
+ put(input: StorageUploadInput): Promise<StoredObject>;
483
+ get(ref: StoredRef): Promise<Buffer>;
484
+ delete(ref: StoredRef): Promise<void>;
485
+ exists(ref: StoredRef): Promise<boolean>;
486
+ stat(ref: StoredRef): Promise<StoredObject | null>;
487
+ private getMessage;
488
+ }
489
+
490
+ declare const TELEGRAM_PROVIDER = "telegram";
491
+ type VerifyCodeResult = {
492
+ status: "ok";
493
+ user: UserRecord;
494
+ } | {
495
+ status: "password_required";
496
+ };
497
+ /**
498
+ * Orchestrates the full Telegram account connection flow:
499
+ * phone → code (→ 2FA password) → session persisted → user upserted →
500
+ * private storage channel created.
501
+ */
502
+ declare class TelegramConnectionService {
503
+ private readonly repos;
504
+ private readonly clientManager;
505
+ private readonly credentials;
506
+ private flows;
507
+ constructor(repos: Repos, clientManager: TelegramClientManager, credentials: TelegramCredentials);
508
+ /** Sends the Telegram confirmation code for the phone number. */
509
+ startLogin(phone: string): Promise<void>;
510
+ /** Verifies the confirmation code. Returns `password_required` for 2FA accounts. */
511
+ verifyCode(phone: string, code: string): Promise<VerifyCodeResult>;
512
+ /** Completes sign-in for accounts with two-factor authentication enabled. */
513
+ verifyPassword(phone: string, password: string): Promise<VerifyCodeResult>;
514
+ /** Aborts a pending login flow (disconnects the client). */
515
+ cancelLogin(phone: string): Promise<void>;
516
+ /**
517
+ * Ensures the user has a registered private storage channel, creating it
518
+ * on first use. Idempotent.
519
+ */
520
+ ensureStorage(userId: string): Promise<StorageRecord>;
521
+ ensureStorageForClient(userId: string, client: TelegramClient): Promise<StorageRecord>;
522
+ private requireFlow;
523
+ private finalizeLogin;
524
+ private sweepExpiredFlows;
525
+ private disconnectQuietly;
526
+ }
527
+
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 };