@lacrous/omnicloud 0.2.16 → 0.2.17
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 +25 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +33 -7
- package/server/prisma/migrations/5_upload_request_fingerprint/migration.sql +3 -0
- package/server/prisma/schema.prisma +43 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacrous/omnicloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "SDK and core building blocks for OmniCloud — self-hosted cloud storage powered by your Telegram account.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"prisma": "^6.2.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@omnicloud/core": "0.2.
|
|
52
|
-
"@omnicloud/shared": "0.2.
|
|
53
|
-
"@omnicloud/telegram": "0.2.
|
|
51
|
+
"@omnicloud/core": "0.2.17",
|
|
52
|
+
"@omnicloud/shared": "0.2.17",
|
|
53
|
+
"@omnicloud/telegram": "0.2.17",
|
|
54
54
|
"@types/node": "^22.10.0",
|
|
55
55
|
"esbuild": "0.25.12",
|
|
56
56
|
"tsup": "^8.3.0",
|
package/server/index.js
CHANGED
|
@@ -1078,6 +1078,20 @@ function sanitizeFileName(name) {
|
|
|
1078
1078
|
return cleaned.slice(0, 255);
|
|
1079
1079
|
}
|
|
1080
1080
|
|
|
1081
|
+
// ../core/src/utils/request-fingerprint.ts
|
|
1082
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1083
|
+
function uploadRequestFingerprint(input) {
|
|
1084
|
+
const contentHash = input.spooled?.sha256 ?? sha256Hex(input.data ?? Buffer.alloc(0));
|
|
1085
|
+
const canonical = JSON.stringify({
|
|
1086
|
+
name: sanitizeFileName(input.name),
|
|
1087
|
+
folderId: input.folderId ?? null,
|
|
1088
|
+
replaceFileId: input.replaceFileId ?? null,
|
|
1089
|
+
contentHash,
|
|
1090
|
+
size: input.spooled?.size ?? input.data?.byteLength ?? 0
|
|
1091
|
+
});
|
|
1092
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1081
1095
|
// ../core/src/services/file-service.ts
|
|
1082
1096
|
var FileService = class {
|
|
1083
1097
|
constructor(files, folders, engineFor, activity = noopActivityRecorder, operations = null, log = null) {
|
|
@@ -1110,7 +1124,13 @@ var FileService = class {
|
|
|
1110
1124
|
if (!isValidOperationId(operationId)) {
|
|
1111
1125
|
throw new ValidationError("Invalid upload operation id");
|
|
1112
1126
|
}
|
|
1127
|
+
const fingerprint = uploadRequestFingerprint(input);
|
|
1113
1128
|
let op = await operations.findByOperationId(userId, operationId);
|
|
1129
|
+
if (op?.requestFingerprint && op.requestFingerprint !== fingerprint) {
|
|
1130
|
+
throw new ConflictError(
|
|
1131
|
+
"This upload key was already used for a different file. Use a new key for a new upload."
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1114
1134
|
if (op?.status === "COMPLETED" && op.fileId) {
|
|
1115
1135
|
return this.get(userId, op.fileId);
|
|
1116
1136
|
}
|
|
@@ -1119,7 +1139,7 @@ var FileService = class {
|
|
|
1119
1139
|
}
|
|
1120
1140
|
if (!op) {
|
|
1121
1141
|
try {
|
|
1122
|
-
op = await operations.create({ userId, operationId });
|
|
1142
|
+
op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
|
|
1123
1143
|
} catch (error) {
|
|
1124
1144
|
if (!(error instanceof OperationAlreadyExistsError)) throw error;
|
|
1125
1145
|
const outcome = await this.followWinner(userId, input, operationId, operations);
|
|
@@ -2214,7 +2234,7 @@ var RecentService = class {
|
|
|
2214
2234
|
};
|
|
2215
2235
|
|
|
2216
2236
|
// ../core/src/utils/spool.ts
|
|
2217
|
-
import { createHash as
|
|
2237
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2218
2238
|
import { createWriteStream } from "node:fs";
|
|
2219
2239
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
2220
2240
|
import { tmpdir } from "node:os";
|
|
@@ -2226,7 +2246,7 @@ async function spoolToFile(source, maxBytes) {
|
|
|
2226
2246
|
const dir = await mkdtemp(join(tmpdir(), `${SPOOL_PREFIX}${process.pid}-`));
|
|
2227
2247
|
const path = join(dir, "payload");
|
|
2228
2248
|
const discard = () => rm(dir, { recursive: true, force: true });
|
|
2229
|
-
const hash =
|
|
2249
|
+
const hash = createHash5("sha256");
|
|
2230
2250
|
let size = 0;
|
|
2231
2251
|
const meter = new Transform2({
|
|
2232
2252
|
transform(chunk, _encoding, callback) {
|
|
@@ -3554,6 +3574,7 @@ function mapUploadOperation(row) {
|
|
|
3554
3574
|
telegramMessageId: row.telegramMessageId === null ? null : Number(row.telegramMessageId),
|
|
3555
3575
|
sha256: row.sha256,
|
|
3556
3576
|
size: row.size === null ? null : Number(row.size),
|
|
3577
|
+
requestFingerprint: row.requestFingerprint,
|
|
3557
3578
|
fileId: row.fileId,
|
|
3558
3579
|
error: row.error,
|
|
3559
3580
|
createdAt: row.createdAt,
|
|
@@ -3575,7 +3596,12 @@ function createUploadOperationRepo(prisma) {
|
|
|
3575
3596
|
async create(input) {
|
|
3576
3597
|
try {
|
|
3577
3598
|
const row = await prisma.uploadOperation.create({
|
|
3578
|
-
data: {
|
|
3599
|
+
data: {
|
|
3600
|
+
userId: input.userId,
|
|
3601
|
+
operationId: input.operationId,
|
|
3602
|
+
status: "PENDING",
|
|
3603
|
+
requestFingerprint: input.requestFingerprint ?? null
|
|
3604
|
+
}
|
|
3579
3605
|
});
|
|
3580
3606
|
return mapUploadOperation(row);
|
|
3581
3607
|
} catch (error) {
|
|
@@ -4128,9 +4154,9 @@ import { readFileSync } from "node:fs";
|
|
|
4128
4154
|
import { join as join2 } from "node:path";
|
|
4129
4155
|
|
|
4130
4156
|
// ../../apps/api/src/csp.ts
|
|
4131
|
-
import { createHash as
|
|
4157
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
4132
4158
|
function sha256Source(content) {
|
|
4133
|
-
return `'sha256-${
|
|
4159
|
+
return `'sha256-${createHash6("sha256").update(content, "utf8").digest("base64")}'`;
|
|
4134
4160
|
}
|
|
4135
4161
|
function buildContentSecurityPolicy(inlineScripts, inlineStyles) {
|
|
4136
4162
|
const scriptSources = ["'self'", ...inlineScripts.map(sha256Source)];
|
|
@@ -5004,7 +5030,7 @@ function encryptionCheck(configured) {
|
|
|
5004
5030
|
}
|
|
5005
5031
|
|
|
5006
5032
|
// ../../apps/api/src/routes/storage.ts
|
|
5007
|
-
var VERSION = "0.2.
|
|
5033
|
+
var VERSION = "0.2.17";
|
|
5008
5034
|
function registerStorageRoutes(app2, container2) {
|
|
5009
5035
|
app2.get("/api/health", async () => {
|
|
5010
5036
|
const database = await probeDatabase(container2);
|
|
@@ -24,29 +24,29 @@ model User {
|
|
|
24
24
|
createdAt DateTime @default(now())
|
|
25
25
|
updatedAt DateTime @updatedAt
|
|
26
26
|
|
|
27
|
-
telegramSession
|
|
28
|
-
storages
|
|
29
|
-
folders
|
|
30
|
-
files
|
|
31
|
-
fileVersions
|
|
32
|
-
activityEvents
|
|
33
|
-
browserSessions
|
|
27
|
+
telegramSession TelegramSession?
|
|
28
|
+
storages Storage[]
|
|
29
|
+
folders Folder[]
|
|
30
|
+
files File[]
|
|
31
|
+
fileVersions FileVersion[]
|
|
32
|
+
activityEvents ActivityEvent[]
|
|
33
|
+
browserSessions BrowserSession[]
|
|
34
34
|
uploadOperations UploadOperation[]
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/// A signed-in browser session. Only the SHA-256 of the cookie token is stored,
|
|
38
38
|
/// so revoking the row ends the session on the next request.
|
|
39
39
|
model BrowserSession {
|
|
40
|
-
id
|
|
41
|
-
userId
|
|
42
|
-
user
|
|
43
|
-
tokenHash
|
|
44
|
-
createdAt
|
|
45
|
-
expiresAt
|
|
46
|
-
revokedAt
|
|
47
|
-
lastUsedAt
|
|
48
|
-
userAgent
|
|
49
|
-
ip
|
|
40
|
+
id String @id @default(cuid())
|
|
41
|
+
userId String
|
|
42
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
43
|
+
tokenHash String @unique
|
|
44
|
+
createdAt DateTime @default(now())
|
|
45
|
+
expiresAt DateTime
|
|
46
|
+
revokedAt DateTime?
|
|
47
|
+
lastUsedAt DateTime?
|
|
48
|
+
userAgent String?
|
|
49
|
+
ip String?
|
|
50
50
|
|
|
51
51
|
@@index([userId, revokedAt])
|
|
52
52
|
@@index([expiresAt])
|
|
@@ -56,18 +56,19 @@ model BrowserSession {
|
|
|
56
56
|
/// second Telegram object. `telegramMessageId` is recorded as soon as Telegram
|
|
57
57
|
/// accepts the object, so recovery can commit the file without uploading again.
|
|
58
58
|
model UploadOperation {
|
|
59
|
-
id
|
|
60
|
-
userId
|
|
61
|
-
user
|
|
62
|
-
operationId
|
|
63
|
-
status
|
|
64
|
-
telegramMessageId
|
|
65
|
-
sha256
|
|
66
|
-
size
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
id String @id @default(cuid())
|
|
60
|
+
userId String
|
|
61
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
62
|
+
operationId String
|
|
63
|
+
status String
|
|
64
|
+
telegramMessageId BigInt?
|
|
65
|
+
sha256 String?
|
|
66
|
+
size BigInt?
|
|
67
|
+
requestFingerprint String?
|
|
68
|
+
fileId String?
|
|
69
|
+
error String?
|
|
70
|
+
createdAt DateTime @default(now())
|
|
71
|
+
updatedAt DateTime @updatedAt
|
|
71
72
|
|
|
72
73
|
@@unique([userId, operationId])
|
|
73
74
|
@@index([status, updatedAt])
|
|
@@ -102,20 +103,20 @@ model Storage {
|
|
|
102
103
|
/// Folders are virtual OmniCloud objects — they have no physical
|
|
103
104
|
/// representation on Telegram. `deletedAt` is the Trash marker.
|
|
104
105
|
model Folder {
|
|
105
|
-
id
|
|
106
|
-
userId
|
|
107
|
-
user
|
|
108
|
-
parentId
|
|
109
|
-
parent
|
|
110
|
-
children
|
|
111
|
-
name
|
|
112
|
-
starred
|
|
113
|
-
deletedAt
|
|
106
|
+
id String @id @default(cuid())
|
|
107
|
+
userId String
|
|
108
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
109
|
+
parentId String?
|
|
110
|
+
parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Restrict)
|
|
111
|
+
children Folder[] @relation("FolderTree")
|
|
112
|
+
name String
|
|
113
|
+
starred Boolean @default(false)
|
|
114
|
+
deletedAt DateTime?
|
|
114
115
|
/// Groups everything one trash operation moved, for exact restore.
|
|
115
116
|
trashBatchId String?
|
|
116
|
-
createdAt
|
|
117
|
-
updatedAt
|
|
118
|
-
files
|
|
117
|
+
createdAt DateTime @default(now())
|
|
118
|
+
updatedAt DateTime @updatedAt
|
|
119
|
+
files File[]
|
|
119
120
|
|
|
120
121
|
@@index([userId, parentId, deletedAt])
|
|
121
122
|
@@index([userId, deletedAt])
|
|
@@ -191,4 +192,4 @@ model ActivityEvent {
|
|
|
191
192
|
@@index([userId, createdAt])
|
|
192
193
|
@@index([userId, resourceId, createdAt])
|
|
193
194
|
@@index([createdAt])
|
|
194
|
-
}
|
|
195
|
+
}
|