@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.
package/dist/index.js ADDED
@@ -0,0 +1,1008 @@
1
+ // src/client.ts
2
+ var OmniCloudError = class extends Error {
3
+ constructor(status, code, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.name = "OmniCloudError";
8
+ }
9
+ status;
10
+ code;
11
+ };
12
+ var OmniCloudClient = class {
13
+ baseUrl;
14
+ fetchImpl;
15
+ constructor(options = {}) {
16
+ this.baseUrl = (options.baseUrl ?? "").replace(/\/$/, "");
17
+ this.fetchImpl = options.fetchImpl ?? fetch.bind(globalThis);
18
+ }
19
+ // ── Authentication ───────────────────────────────────────────────────────
20
+ async me() {
21
+ return this.request("GET", "/api/auth/me");
22
+ }
23
+ async startTelegramLogin(phone) {
24
+ await this.request("POST", "/api/auth/telegram/start", { phone });
25
+ }
26
+ async verifyTelegramCode(phone, code) {
27
+ return this.request("POST", "/api/auth/telegram/verify", { phone, code });
28
+ }
29
+ async submitTelegramPassword(phone, password) {
30
+ return this.request("POST", "/api/auth/telegram/password", { phone, password });
31
+ }
32
+ async ensureStorage() {
33
+ const { storage } = await this.request("POST", "/api/storage/ensure");
34
+ return storage;
35
+ }
36
+ async logout() {
37
+ await this.request("POST", "/api/auth/logout");
38
+ }
39
+ // ── Folders ──────────────────────────────────────────────────────────────
40
+ async listFolders(parentId) {
41
+ const { folders } = await this.request(
42
+ "GET",
43
+ `/api/folders${parentId ? `?parentId=${encodeURIComponent(parentId)}` : ""}`
44
+ );
45
+ return folders;
46
+ }
47
+ async folderTree() {
48
+ const { folders } = await this.request("GET", "/api/folders/tree");
49
+ return folders;
50
+ }
51
+ async createFolder(name, parentId = null) {
52
+ const { folder } = await this.request("POST", "/api/folders", {
53
+ name,
54
+ parentId
55
+ });
56
+ return folder;
57
+ }
58
+ async renameFolder(id, name) {
59
+ const { folder } = await this.request("PATCH", `/api/folders/${id}`, {
60
+ name
61
+ });
62
+ return folder;
63
+ }
64
+ async moveFolder(id, parentId) {
65
+ const { folder } = await this.request(
66
+ "POST",
67
+ `/api/folders/${id}/move`,
68
+ { parentId }
69
+ );
70
+ return folder;
71
+ }
72
+ async deleteFolder(id) {
73
+ return this.request("DELETE", `/api/folders/${id}`);
74
+ }
75
+ // ── Files ────────────────────────────────────────────────────────────────
76
+ async listFiles(folderId) {
77
+ const { files } = await this.request(
78
+ "GET",
79
+ `/api/files${folderId ? `?folderId=${encodeURIComponent(folderId)}` : ""}`
80
+ );
81
+ return files;
82
+ }
83
+ async getFile(id) {
84
+ const { file } = await this.request("GET", `/api/files/${id}`);
85
+ return file;
86
+ }
87
+ async renameFile(id, name) {
88
+ const { file } = await this.request("PATCH", `/api/files/${id}`, { name });
89
+ return file;
90
+ }
91
+ async moveFile(id, folderId) {
92
+ const { file } = await this.request("POST", `/api/files/${id}/move`, {
93
+ folderId
94
+ });
95
+ return file;
96
+ }
97
+ async deleteFile(id) {
98
+ await this.request("DELETE", `/api/files/${id}`);
99
+ }
100
+ /** URL of the download endpoint (usable as href for direct downloads). */
101
+ fileDownloadUrl(id) {
102
+ return `${this.baseUrl}/api/files/${id}/download`;
103
+ }
104
+ /**
105
+ * Uploads a file. Uses XMLHttpRequest when available so `onProgress`
106
+ * works (browsers); falls back to fetch without progress in Node.
107
+ */
108
+ async uploadFile(input, options = {}) {
109
+ const form = new FormData();
110
+ const blob = input.data instanceof Blob ? input.data : new Blob([new Uint8Array(input.data)]);
111
+ form.append("file", blob, input.name);
112
+ if (options.folderId) form.append("folderId", options.folderId);
113
+ if (typeof XMLHttpRequest === "function") {
114
+ return this.uploadViaXhr(form, options.onProgress);
115
+ }
116
+ return this.request("POST", "/api/files", form);
117
+ }
118
+ async search(query) {
119
+ return this.request("GET", `/api/search?q=${encodeURIComponent(query)}`);
120
+ }
121
+ // ── internals ────────────────────────────────────────────────────────────
122
+ uploadViaXhr(form, onProgress) {
123
+ return new Promise((resolve, reject) => {
124
+ const xhr = new XMLHttpRequest();
125
+ xhr.open("POST", `${this.baseUrl}/api/files`);
126
+ xhr.responseType = "json";
127
+ xhr.withCredentials = true;
128
+ xhr.upload.onprogress = (event) => {
129
+ if (!onProgress) return;
130
+ onProgress({
131
+ loaded: event.loaded,
132
+ total: event.total,
133
+ percent: event.total > 0 ? Math.round(event.loaded / event.total * 100) : 0
134
+ });
135
+ };
136
+ xhr.onload = () => {
137
+ const body = xhr.response;
138
+ if (xhr.status >= 200 && xhr.status < 300 && body?.file) {
139
+ resolve(body.file);
140
+ return;
141
+ }
142
+ reject(
143
+ new OmniCloudError(
144
+ xhr.status,
145
+ body?.error?.code ?? "INTERNAL_ERROR",
146
+ body?.error?.message ?? `Upload failed (${xhr.status})`
147
+ )
148
+ );
149
+ };
150
+ xhr.onerror = () => reject(new OmniCloudError(0, "NETWORK_ERROR", "Network error during upload"));
151
+ xhr.send(form);
152
+ });
153
+ }
154
+ async request(method, path, body) {
155
+ const isJsonBody = body !== void 0 && !(body instanceof FormData);
156
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
157
+ method,
158
+ headers: isJsonBody ? { "Content-Type": "application/json" } : void 0,
159
+ body: body === void 0 ? void 0 : isJsonBody ? JSON.stringify(body) : body,
160
+ credentials: "include"
161
+ });
162
+ if (response.status === 204) return void 0;
163
+ const data = await response.json().catch(() => null);
164
+ if (!response.ok) {
165
+ const error = data?.error;
166
+ throw new OmniCloudError(
167
+ response.status,
168
+ error?.code ?? "INTERNAL_ERROR",
169
+ error?.message ?? `Request failed (${response.status})`
170
+ );
171
+ }
172
+ return data;
173
+ }
174
+ };
175
+
176
+ // ../shared/src/mime.ts
177
+ var MIME_BY_EXTENSION = {
178
+ // Documents
179
+ pdf: "application/pdf",
180
+ doc: "application/msword",
181
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
182
+ xls: "application/vnd.ms-excel",
183
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
184
+ ppt: "application/vnd.ms-powerpoint",
185
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
186
+ odt: "application/vnd.oasis.opendocument.text",
187
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
188
+ txt: "text/plain",
189
+ md: "text/markdown",
190
+ csv: "text/csv",
191
+ rtf: "application/rtf",
192
+ html: "text/html",
193
+ htm: "text/html",
194
+ css: "text/css",
195
+ xml: "application/xml",
196
+ yaml: "application/yaml",
197
+ yml: "application/yaml",
198
+ json: "application/json",
199
+ epub: "application/epub+zip",
200
+ // Images
201
+ png: "image/png",
202
+ jpg: "image/jpeg",
203
+ jpeg: "image/jpeg",
204
+ gif: "image/gif",
205
+ webp: "image/webp",
206
+ svg: "image/svg+xml",
207
+ bmp: "image/bmp",
208
+ ico: "image/x-icon",
209
+ tiff: "image/tiff",
210
+ tif: "image/tiff",
211
+ avif: "image/avif",
212
+ heic: "image/heic",
213
+ // Audio
214
+ mp3: "audio/mpeg",
215
+ wav: "audio/wav",
216
+ ogg: "audio/ogg",
217
+ flac: "audio/flac",
218
+ m4a: "audio/mp4",
219
+ aac: "audio/aac",
220
+ opus: "audio/opus",
221
+ // Video
222
+ mp4: "video/mp4",
223
+ mkv: "video/x-matroska",
224
+ webm: "video/webm",
225
+ avi: "video/x-msvideo",
226
+ mov: "video/quicktime",
227
+ wmv: "video/x-ms-wmv",
228
+ flv: "video/x-flv",
229
+ // Archives
230
+ zip: "application/zip",
231
+ rar: "application/vnd.rar",
232
+ "7z": "application/x-7z-compressed",
233
+ tar: "application/x-tar",
234
+ gz: "application/gzip",
235
+ bz2: "application/x-bzip2",
236
+ xz: "application/x-xz",
237
+ // Other
238
+ apk: "application/vnd.android.package-archive",
239
+ exe: "application/x-msdownload",
240
+ dmg: "application/x-apple-diskimage",
241
+ iso: "application/x-iso9660-image",
242
+ bin: "application/octet-stream"
243
+ };
244
+ var DEFAULT_MIME_TYPE = "application/octet-stream";
245
+ function fileExtension(filename) {
246
+ const base = filename.split(/[/\\]/).pop() ?? "";
247
+ const dot = base.lastIndexOf(".");
248
+ if (dot <= 0 || dot === base.length - 1) return "";
249
+ return base.slice(dot + 1).toLowerCase();
250
+ }
251
+ function mimeFromFilename(filename) {
252
+ return MIME_BY_EXTENSION[fileExtension(filename)] ?? DEFAULT_MIME_TYPE;
253
+ }
254
+ var PREFIXED_CATEGORIES = [
255
+ ["image/", "image"],
256
+ ["video/", "video"],
257
+ ["audio/", "audio"],
258
+ ["application/pdf", "pdf"],
259
+ ["application/zip", "archive"],
260
+ ["application/x-tar", "archive"],
261
+ ["application/gzip", "archive"],
262
+ ["application/x-7z-compressed", "archive"],
263
+ ["application/vnd.rar", "archive"],
264
+ ["application/x-bzip2", "archive"],
265
+ ["application/x-xz", "archive"],
266
+ ["text/", "text"],
267
+ ["application/json", "text"],
268
+ ["application/xml", "text"],
269
+ ["application/yaml", "text"],
270
+ ["application/javascript", "text"],
271
+ ["text/markdown", "text"]
272
+ ];
273
+ var OFFICE_MIMES = /* @__PURE__ */ new Set([
274
+ "application/msword",
275
+ "application/vnd.ms-excel",
276
+ "application/vnd.ms-powerpoint",
277
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
278
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
279
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
280
+ "application/vnd.oasis.opendocument.text",
281
+ "application/vnd.oasis.opendocument.spreadsheet",
282
+ "application/rtf",
283
+ "application/epub+zip"
284
+ ]);
285
+ function fileCategory(mimeType) {
286
+ for (const [prefix, category] of PREFIXED_CATEGORIES) {
287
+ if (mimeType.startsWith(prefix)) return category;
288
+ }
289
+ if (OFFICE_MIMES.has(mimeType)) return "document";
290
+ return "other";
291
+ }
292
+
293
+ // ../shared/src/index.ts
294
+ var ERROR_CODES = {
295
+ VALIDATION: "VALIDATION_ERROR",
296
+ UNAUTHENTICATED: "UNAUTHENTICATED",
297
+ FORBIDDEN: "FORBIDDEN",
298
+ NOT_FOUND: "NOT_FOUND",
299
+ CONFLICT: "CONFLICT",
300
+ PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
301
+ RATE_LIMITED: "RATE_LIMITED",
302
+ STORAGE_NOT_INITIALIZED: "STORAGE_NOT_INITIALIZED",
303
+ TELEGRAM_ERROR: "TELEGRAM_ERROR",
304
+ INTERNAL: "INTERNAL_ERROR"
305
+ };
306
+
307
+ // ../core/src/errors.ts
308
+ var DomainError = class extends Error {
309
+ code;
310
+ status;
311
+ details;
312
+ constructor(code, status, message, details) {
313
+ super(message);
314
+ this.name = new.target.name;
315
+ this.code = code;
316
+ this.status = status;
317
+ this.details = details;
318
+ }
319
+ };
320
+ var ValidationError = class extends DomainError {
321
+ constructor(message, details) {
322
+ super(ERROR_CODES.VALIDATION, 400, message, details);
323
+ }
324
+ };
325
+ var UnauthorizedError = class extends DomainError {
326
+ constructor(message = "Authentication required") {
327
+ super(ERROR_CODES.UNAUTHENTICATED, 401, message);
328
+ }
329
+ };
330
+ var ForbiddenError = class extends DomainError {
331
+ constructor(message = "You do not have access to this resource") {
332
+ super(ERROR_CODES.FORBIDDEN, 403, message);
333
+ }
334
+ };
335
+ var NotFoundError = class extends DomainError {
336
+ constructor(message = "Resource not found") {
337
+ super(ERROR_CODES.NOT_FOUND, 404, message);
338
+ }
339
+ };
340
+ var RateLimitedError = class extends DomainError {
341
+ constructor(message = "Too many requests, please retry later") {
342
+ super(ERROR_CODES.RATE_LIMITED, 429, message);
343
+ }
344
+ };
345
+ var StorageNotInitializedError = class extends DomainError {
346
+ constructor() {
347
+ super(ERROR_CODES.STORAGE_NOT_INITIALIZED, 409, "Storage is not initialized for this account");
348
+ }
349
+ };
350
+ var StorageProviderError = class extends DomainError {
351
+ constructor(message, details) {
352
+ super(ERROR_CODES.TELEGRAM_ERROR, 502, message, details);
353
+ }
354
+ };
355
+
356
+ // ../core/src/utils/hash.ts
357
+ import { createHash } from "crypto";
358
+ function sha256Hex(data) {
359
+ return createHash("sha256").update(data).digest("hex");
360
+ }
361
+
362
+ // ../core/src/storage/engine.ts
363
+ var StorageEngine = class {
364
+ constructor(provider) {
365
+ this.provider = provider;
366
+ }
367
+ provider;
368
+ get providerName() {
369
+ return this.provider.name;
370
+ }
371
+ /** Uploads an object and computes its checksum in one step. */
372
+ async upload(input) {
373
+ const sha256 = sha256Hex(input.data);
374
+ let stored;
375
+ try {
376
+ stored = await this.provider.put(input);
377
+ } catch (error) {
378
+ throw mapProviderError("Upload failed", error);
379
+ }
380
+ return { stored, sha256, size: input.data.byteLength };
381
+ }
382
+ async download(ref) {
383
+ try {
384
+ return await this.provider.get(ref);
385
+ } catch (error) {
386
+ throw mapProviderError("Download failed", error);
387
+ }
388
+ }
389
+ async remove(ref) {
390
+ try {
391
+ await this.provider.delete(ref);
392
+ } catch (error) {
393
+ throw mapProviderError("Delete failed", error);
394
+ }
395
+ }
396
+ async verifyIntegrity(data, expectedSha256) {
397
+ return sha256Hex(data) === expectedSha256;
398
+ }
399
+ };
400
+ function mapProviderError(message, error) {
401
+ if (error instanceof StorageProviderError) return error;
402
+ const detail = error instanceof Error ? error.message : String(error);
403
+ return new StorageProviderError(detail ? `${message}: ${detail}` : message, error);
404
+ }
405
+
406
+ // ../core/src/utils/filename.ts
407
+ function sanitizeFileName(name) {
408
+ const base = name.split(/[/\\]/).pop() ?? name;
409
+ const cleaned = base.replace(/[\u0000-\u001f\u007f]/g, "").replace(/[<>:"|?*]/g, "_").trim();
410
+ if (!cleaned || cleaned === "." || cleaned === "..") return "";
411
+ return cleaned.slice(0, 255);
412
+ }
413
+
414
+ // ../core/src/services/file-service.ts
415
+ var FileService = class {
416
+ constructor(files, folders, engineFor) {
417
+ this.files = files;
418
+ this.folders = folders;
419
+ this.engineFor = engineFor;
420
+ }
421
+ files;
422
+ folders;
423
+ engineFor;
424
+ async upload(userId, input) {
425
+ const name = sanitizeFileName(input.name);
426
+ if (!name) throw new ValidationError("Invalid file name");
427
+ if (input.folderId !== null) {
428
+ const folder = await this.folders.findById(input.folderId);
429
+ if (!folder || folder.userId !== userId) throw new NotFoundError("Folder not found");
430
+ }
431
+ const mimeType = mimeFromFilename(name);
432
+ const engine = await this.engineFor(userId);
433
+ const { stored, sha256, size } = await engine.upload({ name, mimeType, data: input.data });
434
+ return this.files.create({
435
+ userId,
436
+ folderId: input.folderId,
437
+ name,
438
+ size,
439
+ mimeType,
440
+ sha256,
441
+ telegramMessageId: Number(stored.messageId)
442
+ });
443
+ }
444
+ async list(userId, folderId) {
445
+ if (folderId !== null) {
446
+ const folder = await this.folders.findById(folderId);
447
+ if (!folder || folder.userId !== userId) throw new NotFoundError("Folder not found");
448
+ }
449
+ return this.files.listByFolder(userId, folderId);
450
+ }
451
+ async get(userId, id) {
452
+ const record = await this.files.findById(id);
453
+ if (!record || record.userId !== userId) throw new NotFoundError("File not found");
454
+ return record;
455
+ }
456
+ async rename(userId, id, name) {
457
+ await this.get(userId, id);
458
+ const clean = sanitizeFileName(name);
459
+ if (!clean) throw new ValidationError("Invalid file name");
460
+ return this.files.update(id, { name: clean });
461
+ }
462
+ async move(userId, id, folderId) {
463
+ await this.get(userId, id);
464
+ if (folderId !== null) {
465
+ const folder = await this.folders.findById(folderId);
466
+ if (!folder || folder.userId !== userId) throw new NotFoundError("Folder not found");
467
+ }
468
+ return this.files.update(id, { folderId });
469
+ }
470
+ async delete(userId, id) {
471
+ const record = await this.get(userId, id);
472
+ const engine = await this.engineFor(userId);
473
+ try {
474
+ await engine.remove({ messageId: String(record.telegramMessageId) });
475
+ } catch {
476
+ }
477
+ await this.files.deleteMany([record.id]);
478
+ }
479
+ async download(userId, id) {
480
+ const record = await this.get(userId, id);
481
+ const engine = await this.engineFor(userId);
482
+ const data = await engine.download({ messageId: String(record.telegramMessageId) });
483
+ const integrityVerified = await engine.verifyIntegrity(data, record.sha256);
484
+ return { record, data, integrityVerified };
485
+ }
486
+ };
487
+
488
+ // ../core/src/services/folder-service.ts
489
+ var FolderService = class {
490
+ constructor(folders, files, engineFor) {
491
+ this.folders = folders;
492
+ this.files = files;
493
+ this.engineFor = engineFor;
494
+ }
495
+ folders;
496
+ files;
497
+ engineFor;
498
+ async create(userId, input) {
499
+ const name = sanitizeFileName(input.name);
500
+ if (!name) throw new ValidationError("Invalid folder name");
501
+ if (input.parentId !== null) {
502
+ const parent = await this.folders.findById(input.parentId);
503
+ if (!parent || parent.userId !== userId) throw new NotFoundError("Folder not found");
504
+ }
505
+ return this.folders.create({ userId, parentId: input.parentId, name });
506
+ }
507
+ async listChildren(userId, parentId) {
508
+ if (parentId !== null) {
509
+ const parent = await this.folders.findById(parentId);
510
+ if (!parent || parent.userId !== userId) throw new NotFoundError("Folder not found");
511
+ }
512
+ return this.folders.listChildren(userId, parentId);
513
+ }
514
+ async tree(userId) {
515
+ return this.folders.listByUser(userId);
516
+ }
517
+ async get(userId, id) {
518
+ const record = await this.folders.findById(id);
519
+ if (!record || record.userId !== userId) throw new NotFoundError("Folder not found");
520
+ return record;
521
+ }
522
+ async rename(userId, id, name) {
523
+ await this.get(userId, id);
524
+ const clean = sanitizeFileName(name);
525
+ if (!clean) throw new ValidationError("Invalid folder name");
526
+ return this.folders.update(id, { name: clean });
527
+ }
528
+ async move(userId, id, newParentId) {
529
+ await this.get(userId, id);
530
+ if (newParentId === id) throw new ValidationError("Cannot move a folder into itself");
531
+ if (newParentId !== null) {
532
+ let cursor = newParentId;
533
+ while (cursor !== null) {
534
+ if (cursor === id) {
535
+ throw new ValidationError("Cannot move a folder into one of its subfolders");
536
+ }
537
+ const parent = await this.folders.findById(cursor);
538
+ if (!parent || parent.userId !== userId) throw new NotFoundError("Folder not found");
539
+ cursor = parent.parentId;
540
+ }
541
+ }
542
+ return this.folders.update(id, { parentId: newParentId });
543
+ }
544
+ async delete(userId, id) {
545
+ await this.get(userId, id);
546
+ const all = await this.folders.listByUser(userId);
547
+ const childrenOf = /* @__PURE__ */ new Map();
548
+ for (const folder of all) {
549
+ const list = childrenOf.get(folder.parentId) ?? [];
550
+ list.push(folder.id);
551
+ childrenOf.set(folder.parentId, list);
552
+ }
553
+ const doomed = [id];
554
+ const queue = [id];
555
+ while (queue.length > 0) {
556
+ const current = queue.shift();
557
+ for (const child of childrenOf.get(current) ?? []) {
558
+ doomed.push(child);
559
+ queue.push(child);
560
+ }
561
+ }
562
+ const doomedSet = new Set(doomed);
563
+ const userFiles = await this.files.listByUser(userId);
564
+ const affectedFiles = userFiles.filter((f) => f.folderId !== null && doomedSet.has(f.folderId));
565
+ const engine = await this.engineFor(userId);
566
+ for (const file of affectedFiles) {
567
+ try {
568
+ await engine.remove({ messageId: String(file.telegramMessageId) });
569
+ } catch {
570
+ }
571
+ }
572
+ await this.files.deleteMany(affectedFiles.map((f) => f.id));
573
+ await this.folders.deleteMany(doomed);
574
+ return { deletedFolders: doomed.length, deletedFiles: affectedFiles.length };
575
+ }
576
+ };
577
+
578
+ // ../core/src/services/search-service.ts
579
+ var MAX_QUERY_LENGTH = 100;
580
+ var SearchService = class {
581
+ constructor(files, folders) {
582
+ this.files = files;
583
+ this.folders = folders;
584
+ }
585
+ files;
586
+ folders;
587
+ async search(userId, query, limit = 25) {
588
+ const q = query.trim();
589
+ if (!q) throw new ValidationError("Search query must not be empty");
590
+ if (q.length > MAX_QUERY_LENGTH) {
591
+ throw new ValidationError(`Search query must be at most ${MAX_QUERY_LENGTH} characters`);
592
+ }
593
+ const [fileResults, folderResults] = await Promise.all([
594
+ this.files.searchByName(userId, q, limit),
595
+ this.folders.searchByName(userId, q, limit)
596
+ ]);
597
+ return { files: fileResults, folders: folderResults };
598
+ }
599
+ };
600
+
601
+ // ../telegram/src/client.ts
602
+ import { Api, TelegramClient } from "telegram";
603
+ import { StringSession } from "telegram/sessions/index.js";
604
+
605
+ // ../telegram/src/errors.ts
606
+ import { RPCError } from "telegram/errors/index.js";
607
+ function isTelegramRpcError(error) {
608
+ return error instanceof RPCError;
609
+ }
610
+ function isPasswordRequiredError(error) {
611
+ return isTelegramRpcError(error) && error.errorMessage === "SESSION_PASSWORD_NEEDED";
612
+ }
613
+ function mapTelegramError(error, fallbackMessage) {
614
+ if (error instanceof DomainError) return error;
615
+ if (isTelegramRpcError(error)) {
616
+ const msg = error.errorMessage ?? "";
617
+ if (msg.startsWith("FLOOD_WAIT_")) {
618
+ const seconds = Number(msg.slice("FLOOD_WAIT_".length)) || 60;
619
+ return new RateLimitedError(`Telegram rate limit reached \u2014 retry in ${seconds} seconds`);
620
+ }
621
+ switch (msg) {
622
+ case "PHONE_NUMBER_INVALID":
623
+ return new ValidationError("This phone number is not valid");
624
+ case "PHONE_NUMBER_UNOCCUPIED":
625
+ return new ValidationError("No Telegram account exists for this phone number");
626
+ case "PHONE_CODE_INVALID":
627
+ return new ValidationError("Invalid confirmation code");
628
+ case "PHONE_CODE_EXPIRED":
629
+ return new ValidationError("The confirmation code has expired \u2014 request a new one");
630
+ case "PHONE_CODE_EMPTY":
631
+ return new ValidationError("Please enter the confirmation code");
632
+ case "SESSION_PASSWORD_NEEDED":
633
+ return new UnauthorizedError("Two-factor authentication is enabled on this account");
634
+ case "PASSWORD_HASH_INVALID":
635
+ return new ValidationError("Incorrect two-factor password");
636
+ case "PHONE_NUMBER_BANNED":
637
+ return new ForbiddenError("This phone number is banned from Telegram");
638
+ case "AUTH_KEY_UNREGISTERED":
639
+ case "SESSION_REVOKED":
640
+ case "USER_DEACTIVATED":
641
+ return new UnauthorizedError("The Telegram session is no longer valid \u2014 please reconnect");
642
+ case "CHANNEL_PRIVATE":
643
+ case "CHAT_ID_INVALID":
644
+ return new StorageNotInitializedError();
645
+ default:
646
+ return new StorageProviderError(fallbackMessage, {
647
+ code: error.code,
648
+ telegramError: msg
649
+ });
650
+ }
651
+ }
652
+ return new StorageProviderError(
653
+ error instanceof Error ? `${fallbackMessage}: ${error.message}` : fallbackMessage,
654
+ error
655
+ );
656
+ }
657
+
658
+ // ../telegram/src/client.ts
659
+ var TelegramClientManager = class {
660
+ constructor(credentials, sessions) {
661
+ this.credentials = credentials;
662
+ this.sessions = sessions;
663
+ }
664
+ credentials;
665
+ sessions;
666
+ clients = /* @__PURE__ */ new Map();
667
+ async getForUser(userId) {
668
+ const cached = this.clients.get(userId);
669
+ if (cached) {
670
+ try {
671
+ return await cached;
672
+ } catch {
673
+ this.clients.delete(userId);
674
+ }
675
+ }
676
+ const session = await this.sessions.get(userId);
677
+ if (!session) {
678
+ throw new UnauthorizedError("Telegram account is not connected");
679
+ }
680
+ const promise = this.connect(session.stringSession);
681
+ this.clients.set(userId, promise);
682
+ try {
683
+ return await promise;
684
+ } catch (error) {
685
+ this.clients.delete(userId);
686
+ throw mapTelegramError(error, "Could not connect to Telegram");
687
+ }
688
+ }
689
+ /** Registers an already-authenticated client (e.g. from the login flow). */
690
+ cacheClient(userId, client) {
691
+ this.clients.set(userId, Promise.resolve(client));
692
+ }
693
+ async disconnectAll() {
694
+ for (const [userId, promise] of this.clients) {
695
+ this.clients.delete(userId);
696
+ try {
697
+ const client = await promise;
698
+ await client.disconnect();
699
+ } catch {
700
+ }
701
+ }
702
+ }
703
+ async connect(sessionString) {
704
+ const client = new TelegramClient(
705
+ new StringSession(sessionString),
706
+ this.credentials.apiId,
707
+ this.credentials.apiHash,
708
+ { connectionRetries: 3 }
709
+ );
710
+ await client.connect();
711
+ return client;
712
+ }
713
+ };
714
+ function newTelegramClient(credentials) {
715
+ return new TelegramClient(new StringSession(""), credentials.apiId, credentials.apiHash, {
716
+ connectionRetries: 3
717
+ });
718
+ }
719
+ function extractCreatedChannel(result) {
720
+ const chats = result instanceof Api.Updates || result instanceof Api.UpdatesCombined ? result.chats : [];
721
+ for (const chat of chats) {
722
+ if (chat instanceof Api.Channel) return chat;
723
+ }
724
+ throw new StorageProviderError("Telegram did not return the created storage channel");
725
+ }
726
+
727
+ // ../telegram/src/provider.ts
728
+ import { Api as Api2 } from "telegram";
729
+ import bigInt from "big-integer";
730
+ import { CustomFile } from "telegram/client/uploads.js";
731
+ var TelegramStorageProvider = class {
732
+ constructor(client, options) {
733
+ this.client = client;
734
+ this.peer = new Api2.InputPeerChannel({
735
+ channelId: bigInt(options.chatId),
736
+ accessHash: bigInt(options.accessHash)
737
+ });
738
+ }
739
+ client;
740
+ name = "telegram";
741
+ peer;
742
+ async put(input) {
743
+ try {
744
+ const file = new CustomFile(input.name, input.data.byteLength, "", input.data);
745
+ const message = await this.client.sendFile(this.peer, {
746
+ file,
747
+ forceDocument: true,
748
+ workers: 1
749
+ });
750
+ if (!(message instanceof Api2.Message)) {
751
+ throw new StorageProviderError("Telegram returned an unexpected upload response");
752
+ }
753
+ return {
754
+ messageId: message.id.toString(),
755
+ name: input.name,
756
+ size: input.data.byteLength,
757
+ mimeType: input.mimeType
758
+ };
759
+ } catch (error) {
760
+ throw mapTelegramError(error, "Telegram upload failed");
761
+ }
762
+ }
763
+ async get(ref) {
764
+ let data;
765
+ try {
766
+ const message = await this.getMessage(ref.messageId);
767
+ if (!message?.media) {
768
+ throw new StorageProviderError("The stored file is missing from Telegram");
769
+ }
770
+ data = await this.client.downloadMedia(message, {});
771
+ } catch (error) {
772
+ if (error instanceof StorageProviderError) throw error;
773
+ throw mapTelegramError(error, "Telegram download failed");
774
+ }
775
+ if (!data || typeof data === "string") {
776
+ throw new StorageProviderError("Telegram did not return file content");
777
+ }
778
+ return Buffer.from(data);
779
+ }
780
+ async delete(ref) {
781
+ try {
782
+ await this.client.deleteMessages(this.peer, [Number(ref.messageId)], { revoke: true });
783
+ } catch (error) {
784
+ throw mapTelegramError(error, "Telegram delete failed");
785
+ }
786
+ }
787
+ async exists(ref) {
788
+ try {
789
+ return await this.stat(ref) !== null;
790
+ } catch (error) {
791
+ throw mapTelegramError(error, "Telegram lookup failed");
792
+ }
793
+ }
794
+ async stat(ref) {
795
+ let message;
796
+ try {
797
+ message = await this.getMessage(ref.messageId);
798
+ } catch (error) {
799
+ throw mapTelegramError(error, "Telegram lookup failed");
800
+ }
801
+ if (!message?.media) return null;
802
+ const document = message.document;
803
+ if (!(document instanceof Api2.Document)) return null;
804
+ let fileName;
805
+ for (const attr of document.attributes) {
806
+ if (attr instanceof Api2.DocumentAttributeFilename) {
807
+ fileName = attr.fileName;
808
+ break;
809
+ }
810
+ }
811
+ return {
812
+ messageId: ref.messageId,
813
+ name: fileName ?? "file",
814
+ size: Number(document.size.toString()),
815
+ mimeType: document.mimeType
816
+ };
817
+ }
818
+ async getMessage(id) {
819
+ const messages = await this.client.getMessages(this.peer, { ids: [Number(id)] });
820
+ const first = messages[0];
821
+ return first instanceof Api2.Message ? first : void 0;
822
+ }
823
+ };
824
+
825
+ // ../telegram/src/connection.ts
826
+ import { Api as Api3 } from "telegram";
827
+ import { computeCheck } from "telegram/Password.js";
828
+ var TELEGRAM_PROVIDER = "telegram";
829
+ var LOGIN_FLOW_TTL_MS = 5 * 60 * 1e3;
830
+ var TelegramConnectionService = class {
831
+ constructor(repos, clientManager, credentials) {
832
+ this.repos = repos;
833
+ this.clientManager = clientManager;
834
+ this.credentials = credentials;
835
+ }
836
+ repos;
837
+ clientManager;
838
+ credentials;
839
+ flows = /* @__PURE__ */ new Map();
840
+ /** Sends the Telegram confirmation code for the phone number. */
841
+ async startLogin(phone) {
842
+ this.sweepExpiredFlows();
843
+ await this.cancelLogin(phone);
844
+ const client = newTelegramClient(this.credentials);
845
+ try {
846
+ await client.connect();
847
+ const { phoneCodeHash } = await client.sendCode(
848
+ { apiId: this.credentials.apiId, apiHash: this.credentials.apiHash },
849
+ phone
850
+ );
851
+ this.flows.set(phone, { phone, client, phoneCodeHash, createdAt: Date.now() });
852
+ } catch (error) {
853
+ await this.disconnectQuietly(client);
854
+ throw mapTelegramError(error, "Could not send the Telegram confirmation code");
855
+ }
856
+ }
857
+ /** Verifies the confirmation code. Returns `password_required` for 2FA accounts. */
858
+ async verifyCode(phone, code) {
859
+ const flow = this.requireFlow(phone);
860
+ try {
861
+ await flow.client.invoke(
862
+ new Api3.auth.SignIn({
863
+ phoneNumber: flow.phone,
864
+ phoneCodeHash: flow.phoneCodeHash,
865
+ phoneCode: code
866
+ })
867
+ );
868
+ } catch (error) {
869
+ if (isPasswordRequiredError(error)) {
870
+ return { status: "password_required" };
871
+ }
872
+ throw mapTelegramError(error, "Telegram sign-in failed");
873
+ }
874
+ return this.finalizeLogin(flow);
875
+ }
876
+ /** Completes sign-in for accounts with two-factor authentication enabled. */
877
+ async verifyPassword(phone, password) {
878
+ const flow = this.requireFlow(phone);
879
+ try {
880
+ const passwordInfo = await flow.client.invoke(new Api3.account.GetPassword());
881
+ const check = await computeCheck(passwordInfo, password);
882
+ await flow.client.invoke(new Api3.auth.CheckPassword({ password: check }));
883
+ } catch (error) {
884
+ throw mapTelegramError(error, "Telegram sign-in failed");
885
+ }
886
+ return this.finalizeLogin(flow);
887
+ }
888
+ /** Aborts a pending login flow (disconnects the client). */
889
+ async cancelLogin(phone) {
890
+ const flow = this.flows.get(phone);
891
+ if (flow) {
892
+ this.flows.delete(phone);
893
+ await this.disconnectQuietly(flow.client);
894
+ }
895
+ }
896
+ /**
897
+ * Ensures the user has a registered private storage channel, creating it
898
+ * on first use. Idempotent.
899
+ */
900
+ async ensureStorage(userId) {
901
+ const existing = await this.repos.storages.findByUserAndProvider(userId, TELEGRAM_PROVIDER);
902
+ if (existing) return existing;
903
+ const client = await this.clientManager.getForUser(userId);
904
+ return this.ensureStorageForClient(userId, client);
905
+ }
906
+ async ensureStorageForClient(userId, client) {
907
+ const existing = await this.repos.storages.findByUserAndProvider(userId, TELEGRAM_PROVIDER);
908
+ if (existing) return existing;
909
+ let channel;
910
+ try {
911
+ const result = await client.invoke(
912
+ new Api3.channels.CreateChannel({
913
+ title: "OmniCloud Storage",
914
+ about: "Private storage channel created by OmniCloud (https://github.com/Lacrous/OmniCloud)",
915
+ megagroup: false
916
+ })
917
+ );
918
+ channel = extractCreatedChannel(result);
919
+ } catch (error) {
920
+ throw mapTelegramError(error, "Could not create the private storage channel");
921
+ }
922
+ const accessHash = channel.accessHash?.toString();
923
+ if (!accessHash) {
924
+ throw new StorageProviderError("Telegram did not provide the channel access hash");
925
+ }
926
+ return this.repos.storages.create({
927
+ userId,
928
+ provider: TELEGRAM_PROVIDER,
929
+ title: channel.title ?? "OmniCloud Storage",
930
+ telegramChatId: channel.id.toString(),
931
+ telegramAccessHash: accessHash
932
+ });
933
+ }
934
+ // ── internals ────────────────────────────────────────────────────────────
935
+ requireFlow(phone) {
936
+ const flow = this.flows.get(phone);
937
+ if (!flow) {
938
+ throw new NotFoundError("No pending Telegram login for this phone number");
939
+ }
940
+ if (Date.now() - flow.createdAt > LOGIN_FLOW_TTL_MS) {
941
+ this.flows.delete(phone);
942
+ void this.disconnectQuietly(flow.client);
943
+ throw new ValidationError("The login session expired \u2014 please start again");
944
+ }
945
+ return flow;
946
+ }
947
+ async finalizeLogin(flow) {
948
+ this.flows.delete(flow.phone);
949
+ let stringSession;
950
+ let user;
951
+ try {
952
+ stringSession = flow.client.session.save();
953
+ const me = await flow.client.getMe();
954
+ if (!(me instanceof Api3.User)) {
955
+ throw new Error("Telegram did not return the authenticated user");
956
+ }
957
+ user = await this.repos.users.upsertFromTelegram({
958
+ telegramUserId: me.id.toString(),
959
+ username: me.username ?? null,
960
+ firstName: me.firstName ?? null,
961
+ lastName: me.lastName ?? null,
962
+ phone: me.phone ?? flow.phone
963
+ });
964
+ } catch (error) {
965
+ await this.disconnectQuietly(flow.client);
966
+ throw mapTelegramError(error, "Could not complete the Telegram connection");
967
+ }
968
+ await this.repos.sessions.save(user.id, stringSession);
969
+ await this.ensureStorageForClient(user.id, flow.client);
970
+ this.clientManager.cacheClient(user.id, flow.client);
971
+ return { status: "ok", user };
972
+ }
973
+ sweepExpiredFlows() {
974
+ const now = Date.now();
975
+ for (const [phone, flow] of this.flows) {
976
+ if (now - flow.createdAt > LOGIN_FLOW_TTL_MS) {
977
+ this.flows.delete(phone);
978
+ void this.disconnectQuietly(flow.client);
979
+ }
980
+ }
981
+ }
982
+ async disconnectQuietly(client) {
983
+ try {
984
+ await client.disconnect();
985
+ } catch {
986
+ }
987
+ }
988
+ };
989
+ export {
990
+ ERROR_CODES,
991
+ FileService,
992
+ FolderService,
993
+ OmniCloudClient,
994
+ OmniCloudError,
995
+ SearchService,
996
+ StorageEngine,
997
+ TELEGRAM_PROVIDER,
998
+ TelegramClientManager,
999
+ TelegramConnectionService,
1000
+ TelegramStorageProvider,
1001
+ fileCategory,
1002
+ mapProviderError,
1003
+ mapTelegramError,
1004
+ mimeFromFilename,
1005
+ sanitizeFileName,
1006
+ sha256Hex
1007
+ };
1008
+ //# sourceMappingURL=index.js.map