@lacrous/omnicloud 0.2.32 → 0.2.34
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.cjs +101 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +94 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +133 -5
- package/server/prisma/migrations/6_api_tokens/migration.sql +19 -0
- package/server/prisma/schema.prisma +16 -0
package/dist/index.d.cts
CHANGED
|
@@ -429,6 +429,12 @@ declare class FilesApi {
|
|
|
429
429
|
* fetch elsewhere.
|
|
430
430
|
*/
|
|
431
431
|
upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO$1>;
|
|
432
|
+
private canStreamFromDisk;
|
|
433
|
+
/**
|
|
434
|
+
* Node: streams a file from disk as a multipart body. The file is re-opened on
|
|
435
|
+
* every attempt, so a retry sends the same bytes without holding them in memory.
|
|
436
|
+
*/
|
|
437
|
+
private uploadPathOnce;
|
|
432
438
|
private uploadOnce;
|
|
433
439
|
private uploadViaXhr;
|
|
434
440
|
/** Uploads a new version of an existing file. */
|
|
@@ -858,6 +864,28 @@ interface BrowserSessionRecord {
|
|
|
858
864
|
userAgent: string | null;
|
|
859
865
|
ip: string | null;
|
|
860
866
|
}
|
|
867
|
+
interface ApiTokenRecord {
|
|
868
|
+
id: string;
|
|
869
|
+
userId: string;
|
|
870
|
+
name: string;
|
|
871
|
+
tokenHash: string;
|
|
872
|
+
createdAt: Date;
|
|
873
|
+
revokedAt: Date | null;
|
|
874
|
+
lastUsedAt: Date | null;
|
|
875
|
+
}
|
|
876
|
+
/** Personal API tokens. Holds only token hashes, never raw tokens. */
|
|
877
|
+
interface ApiTokenRepository {
|
|
878
|
+
create(input: {
|
|
879
|
+
userId: string;
|
|
880
|
+
name: string;
|
|
881
|
+
tokenHash: string;
|
|
882
|
+
}): Promise<ApiTokenRecord>;
|
|
883
|
+
findByTokenHash(tokenHash: string): Promise<ApiTokenRecord | null>;
|
|
884
|
+
findByIdForUser(userId: string, id: string): Promise<ApiTokenRecord | null>;
|
|
885
|
+
listForUser(userId: string): Promise<ApiTokenRecord[]>;
|
|
886
|
+
touch(id: string, at: Date): Promise<void>;
|
|
887
|
+
revoke(id: string, at: Date): Promise<void>;
|
|
888
|
+
}
|
|
861
889
|
/** Server-side browser sessions. Holds only token hashes, never raw tokens. */
|
|
862
890
|
interface BrowserSessionRepository {
|
|
863
891
|
create(input: {
|
|
@@ -1061,6 +1089,7 @@ interface Repos {
|
|
|
1061
1089
|
users: UserRepository;
|
|
1062
1090
|
sessions: SessionRepository;
|
|
1063
1091
|
browserSessions: BrowserSessionRepository;
|
|
1092
|
+
apiTokens: ApiTokenRepository;
|
|
1064
1093
|
uploadOperations: UploadOperationRepository;
|
|
1065
1094
|
storages: StorageRepository;
|
|
1066
1095
|
folders: FolderRepository;
|
package/dist/index.d.ts
CHANGED
|
@@ -429,6 +429,12 @@ declare class FilesApi {
|
|
|
429
429
|
* fetch elsewhere.
|
|
430
430
|
*/
|
|
431
431
|
upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO$1>;
|
|
432
|
+
private canStreamFromDisk;
|
|
433
|
+
/**
|
|
434
|
+
* Node: streams a file from disk as a multipart body. The file is re-opened on
|
|
435
|
+
* every attempt, so a retry sends the same bytes without holding them in memory.
|
|
436
|
+
*/
|
|
437
|
+
private uploadPathOnce;
|
|
432
438
|
private uploadOnce;
|
|
433
439
|
private uploadViaXhr;
|
|
434
440
|
/** Uploads a new version of an existing file. */
|
|
@@ -858,6 +864,28 @@ interface BrowserSessionRecord {
|
|
|
858
864
|
userAgent: string | null;
|
|
859
865
|
ip: string | null;
|
|
860
866
|
}
|
|
867
|
+
interface ApiTokenRecord {
|
|
868
|
+
id: string;
|
|
869
|
+
userId: string;
|
|
870
|
+
name: string;
|
|
871
|
+
tokenHash: string;
|
|
872
|
+
createdAt: Date;
|
|
873
|
+
revokedAt: Date | null;
|
|
874
|
+
lastUsedAt: Date | null;
|
|
875
|
+
}
|
|
876
|
+
/** Personal API tokens. Holds only token hashes, never raw tokens. */
|
|
877
|
+
interface ApiTokenRepository {
|
|
878
|
+
create(input: {
|
|
879
|
+
userId: string;
|
|
880
|
+
name: string;
|
|
881
|
+
tokenHash: string;
|
|
882
|
+
}): Promise<ApiTokenRecord>;
|
|
883
|
+
findByTokenHash(tokenHash: string): Promise<ApiTokenRecord | null>;
|
|
884
|
+
findByIdForUser(userId: string, id: string): Promise<ApiTokenRecord | null>;
|
|
885
|
+
listForUser(userId: string): Promise<ApiTokenRecord[]>;
|
|
886
|
+
touch(id: string, at: Date): Promise<void>;
|
|
887
|
+
revoke(id: string, at: Date): Promise<void>;
|
|
888
|
+
}
|
|
861
889
|
/** Server-side browser sessions. Holds only token hashes, never raw tokens. */
|
|
862
890
|
interface BrowserSessionRepository {
|
|
863
891
|
create(input: {
|
|
@@ -1061,6 +1089,7 @@ interface Repos {
|
|
|
1061
1089
|
users: UserRepository;
|
|
1062
1090
|
sessions: SessionRepository;
|
|
1063
1091
|
browserSessions: BrowserSessionRepository;
|
|
1092
|
+
apiTokens: ApiTokenRepository;
|
|
1064
1093
|
uploadOperations: UploadOperationRepository;
|
|
1065
1094
|
storages: StorageRepository;
|
|
1066
1095
|
folders: FolderRepository;
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,51 @@ var OmniCloudError = class extends Error {
|
|
|
14
14
|
details;
|
|
15
15
|
};
|
|
16
16
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 300 };
|
|
17
|
+
function randomId() {
|
|
18
|
+
return globalThis.crypto.randomUUID().replace(/-/g, "");
|
|
19
|
+
}
|
|
20
|
+
function multipartHead(boundary, fields, fileName) {
|
|
21
|
+
const parts = fields.map(
|
|
22
|
+
([name, value]) => `--${boundary}\r
|
|
23
|
+
Content-Disposition: form-data; name="${name}"\r
|
|
24
|
+
\r
|
|
25
|
+
${value}\r
|
|
26
|
+
`
|
|
27
|
+
);
|
|
28
|
+
const safeName = fileName.replace(/["\\\r\n]/g, "_");
|
|
29
|
+
parts.push(
|
|
30
|
+
`--${boundary}\r
|
|
31
|
+
Content-Disposition: form-data; name="file"; filename="${safeName}"\r
|
|
32
|
+
Content-Type: application/octet-stream\r
|
|
33
|
+
\r
|
|
34
|
+
`
|
|
35
|
+
);
|
|
36
|
+
return parts.join("");
|
|
37
|
+
}
|
|
38
|
+
function nodeMultipartStream(head, file, tail) {
|
|
39
|
+
const encoder = new TextEncoder();
|
|
40
|
+
const iterator = file[Symbol.asyncIterator]();
|
|
41
|
+
let sentHead = false;
|
|
42
|
+
return new ReadableStream({
|
|
43
|
+
async pull(controller) {
|
|
44
|
+
if (!sentHead) {
|
|
45
|
+
sentHead = true;
|
|
46
|
+
controller.enqueue(encoder.encode(head));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { done, value } = await iterator.next();
|
|
50
|
+
if (done) {
|
|
51
|
+
controller.enqueue(encoder.encode(tail));
|
|
52
|
+
controller.close();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
controller.enqueue(typeof value === "string" ? encoder.encode(value) : new Uint8Array(value));
|
|
56
|
+
},
|
|
57
|
+
async cancel(reason) {
|
|
58
|
+
await iterator.return?.(reason);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
17
62
|
function sleep(ms, signal) {
|
|
18
63
|
return new Promise((resolve, reject) => {
|
|
19
64
|
const timer = setTimeout(resolve, ms);
|
|
@@ -305,10 +350,13 @@ var FilesApi = class {
|
|
|
305
350
|
async upload(input, options = {}) {
|
|
306
351
|
const attempts = Math.max(1, (options.retry ?? 2) + 1);
|
|
307
352
|
let lastError;
|
|
308
|
-
const
|
|
353
|
+
const source = input.path !== void 0 && this.canStreamFromDisk() ? { kind: "path", path: input.path } : { kind: "blob", blob: await toBlob(input) };
|
|
309
354
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
310
355
|
try {
|
|
311
|
-
|
|
356
|
+
if (source.kind === "path") {
|
|
357
|
+
return await this.uploadPathOnce(source.path, input.name, options);
|
|
358
|
+
}
|
|
359
|
+
return await this.uploadOnce(source.blob, input.name, options);
|
|
312
360
|
} catch (error) {
|
|
313
361
|
lastError = error;
|
|
314
362
|
if (options.signal?.aborted) throw error;
|
|
@@ -318,6 +366,46 @@ var FilesApi = class {
|
|
|
318
366
|
}
|
|
319
367
|
throw lastError;
|
|
320
368
|
}
|
|
369
|
+
canStreamFromDisk() {
|
|
370
|
+
return typeof XMLHttpRequest !== "function" && typeof ReadableStream === "function";
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Node: streams a file from disk as a multipart body. The file is re-opened on
|
|
374
|
+
* every attempt, so a retry sends the same bytes without holding them in memory.
|
|
375
|
+
*/
|
|
376
|
+
async uploadPathOnce(path, name, options) {
|
|
377
|
+
const { createReadStream, statSync } = await import("fs");
|
|
378
|
+
const size = statSync(path).size;
|
|
379
|
+
const boundary = `omnicloud-${randomId()}`;
|
|
380
|
+
const fields = [];
|
|
381
|
+
if (options.folderId) fields.push(["folderId", options.folderId]);
|
|
382
|
+
const head = multipartHead(boundary, fields, name);
|
|
383
|
+
const tail = `\r
|
|
384
|
+
--${boundary}--\r
|
|
385
|
+
`;
|
|
386
|
+
const body = nodeMultipartStream(head, createReadStream(path), tail);
|
|
387
|
+
const response = await this.client.fetchImpl(`${this.client.baseUrl}/api/files`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: {
|
|
390
|
+
...this.client.defaultHeaders,
|
|
391
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
392
|
+
"Content-Length": String(Buffer.byteLength(head) + size + Buffer.byteLength(tail))
|
|
393
|
+
},
|
|
394
|
+
body,
|
|
395
|
+
credentials: "include",
|
|
396
|
+
duplex: "half"
|
|
397
|
+
});
|
|
398
|
+
const text = await response.text();
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
const error = safeJsonParse(text)?.error;
|
|
401
|
+
throw new OmniCloudError(
|
|
402
|
+
response.status,
|
|
403
|
+
error?.code ?? "UPLOAD_FAILED",
|
|
404
|
+
error?.message ?? `Upload failed (${response.status})`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return JSON.parse(text).file;
|
|
408
|
+
}
|
|
321
409
|
async uploadOnce(blob, name, options) {
|
|
322
410
|
const form = new FormData();
|
|
323
411
|
form.append("file", blob, name);
|
|
@@ -1318,6 +1406,9 @@ async function resolveItemQuery(folders, userId, input) {
|
|
|
1318
1406
|
// ../core/src/services/auth-session-service.ts
|
|
1319
1407
|
import { createHash as createHash3, randomBytes } from "crypto";
|
|
1320
1408
|
|
|
1409
|
+
// ../core/src/services/api-token-service.ts
|
|
1410
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1411
|
+
|
|
1321
1412
|
// ../core/src/services/version-retention.ts
|
|
1322
1413
|
function versionsToPrune(versions, currentVersionId, policy, now) {
|
|
1323
1414
|
const historical = versions.filter((version) => version.id !== currentVersionId).sort((a, b) => b.versionNumber - a.versionNumber);
|
|
@@ -2693,7 +2784,7 @@ import { Transform as Transform2 } from "stream";
|
|
|
2693
2784
|
import { pipeline } from "stream/promises";
|
|
2694
2785
|
|
|
2695
2786
|
// ../core/src/utils/secret-box.ts
|
|
2696
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
2787
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, scryptSync } from "crypto";
|
|
2697
2788
|
|
|
2698
2789
|
// ../telegram/src/client.ts
|
|
2699
2790
|
import { Api, TelegramClient } from "telegram";
|