@tomorrowos/sdk 0.9.52 → 0.9.53
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/LOVABLE_SETUP.md +1 -1
- package/VERCEL_QUESTIONS.md +1 -1
- package/VERCEL_SETUP.md +1 -1
- package/dist/cloudinary-storage.d.ts +17 -0
- package/dist/cloudinary-storage.d.ts.map +1 -1
- package/dist/cloudinary-storage.js +65 -3
- package/dist/tomorrowos.d.ts +2 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +104 -1
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/methods.js +164 -0
- package/templates/cms-starter-v0/cms-panel/methods.js +161 -0
- package/templates/cms-starter-v0/package.json +2 -2
package/LOVABLE_SETUP.md
CHANGED
package/VERCEL_QUESTIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vercel / v0 閳?Verbatim question bank
|
|
2
2
|
|
|
3
|
-
> **Protocol:** `vercel-questions/1.0` (pairs with `VERCEL_SETUP.md` 1.9+ / `@tomorrowos/sdk` 0.9.
|
|
3
|
+
> **Protocol:** `vercel-questions/1.0` (pairs with `VERCEL_SETUP.md` 1.9+ / `@tomorrowos/sdk` 0.9.53+)
|
|
4
4
|
> **For Agents:** When asking the user a setup question, **copy-paste the block below for that step EXACTLY**.
|
|
5
5
|
> Do **not** paraphrase, reorder, rename options, or invent alternatives (no S3, no Supabase Storage, no 閳ユ窂lob recommended閳? no 閳ユ竸isable media閳?.
|
|
6
6
|
> After each answer, configure per `VERCEL_SETUP.md`, then paste the **next** block.
|
package/VERCEL_SETUP.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
> **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
|
|
7
7
|
>
|
|
8
|
-
> Protocol id: `vercel-setup/1.9` — pairs with `@tomorrowos/sdk` **0.9.
|
|
8
|
+
> Protocol id: `vercel-setup/1.9` — pairs with `@tomorrowos/sdk` **0.9.53+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
|
|
9
9
|
>
|
|
10
10
|
> **Questions source of truth:** `VERCEL_QUESTIONS.md` ? Agents must **open that file and paste each Q block verbatim**. Do not paraphrase.
|
|
11
11
|
> **Copy questions verbatim from `VERCEL_QUESTIONS.md`.** Do not invent alternate Q2 options (no ?Supabase Storage?? no S3, no ?disable media?? no making Blob the recommended option).
|
|
@@ -11,6 +11,23 @@ export interface CloudinaryUploadResult {
|
|
|
11
11
|
bytes?: number;
|
|
12
12
|
}
|
|
13
13
|
export declare function resolveCloudinaryConfig(env?: NodeJS.ProcessEnv): CloudinaryStorageConfig | null;
|
|
14
|
+
/** Browser direct-upload params (signed). Secret never leaves the server. */
|
|
15
|
+
export declare function createSignedUploadParams(config: CloudinaryStorageConfig, options: {
|
|
16
|
+
publicId: string;
|
|
17
|
+
filename: string;
|
|
18
|
+
}): {
|
|
19
|
+
cloudName: string;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
signature: string;
|
|
23
|
+
publicId: string;
|
|
24
|
+
filenameOverride: string;
|
|
25
|
+
uploadUrl: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function fetchCloudinaryResource(config: CloudinaryStorageConfig, publicId: string, resourceType?: string): Promise<CloudinaryUploadResult & {
|
|
28
|
+
etag?: string;
|
|
29
|
+
durationSec?: number;
|
|
30
|
+
}>;
|
|
14
31
|
export declare function uploadBufferToCloudinary(body: Buffer, options: {
|
|
15
32
|
config: CloudinaryStorageConfig;
|
|
16
33
|
publicId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;
|
|
1
|
+
{"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;AAsBD,6EAA6E;AAC7E,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9C;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB,CAyBA;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,sBAAsB,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+B3E;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,uBAAuB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,sBAAsB,CAAC,CAoCjC;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,qEAAqE;AACrE,wBAAsB,cAAc,CAClC,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -29,11 +29,73 @@ function configureCloudinary(config) {
|
|
|
29
29
|
secure: true
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
function resolvePublicId(config, publicId) {
|
|
33
|
+
if (!config.folder)
|
|
34
|
+
return publicId;
|
|
35
|
+
const folder = config.folder.replace(/^\/+|\/+$/g, "");
|
|
36
|
+
if (!folder)
|
|
37
|
+
return publicId;
|
|
38
|
+
if (publicId === folder || publicId.startsWith(`${folder}/`))
|
|
39
|
+
return publicId;
|
|
40
|
+
return `${folder}/${publicId}`;
|
|
41
|
+
}
|
|
42
|
+
/** Browser direct-upload params (signed). Secret never leaves the server. */
|
|
43
|
+
export function createSignedUploadParams(config, options) {
|
|
44
|
+
configureCloudinary(config);
|
|
45
|
+
const publicId = resolvePublicId(config, options.publicId);
|
|
46
|
+
const timestamp = Math.round(Date.now() / 1000);
|
|
47
|
+
const paramsToSign = {
|
|
48
|
+
timestamp,
|
|
49
|
+
public_id: publicId,
|
|
50
|
+
overwrite: true,
|
|
51
|
+
unique_filename: false,
|
|
52
|
+
use_filename: false,
|
|
53
|
+
filename_override: options.filename
|
|
54
|
+
};
|
|
55
|
+
const signature = cloudinary.utils.api_sign_request(paramsToSign, config.apiSecret);
|
|
56
|
+
return {
|
|
57
|
+
cloudName: config.cloudName,
|
|
58
|
+
apiKey: config.apiKey,
|
|
59
|
+
timestamp,
|
|
60
|
+
signature,
|
|
61
|
+
publicId,
|
|
62
|
+
filenameOverride: options.filename,
|
|
63
|
+
uploadUrl: `https://api.cloudinary.com/v1_1/${config.cloudName}/auto/upload`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function fetchCloudinaryResource(config, publicId, resourceType = "image") {
|
|
67
|
+
configureCloudinary(config);
|
|
68
|
+
const types = resourceType === "auto"
|
|
69
|
+
? ["video", "image", "raw"]
|
|
70
|
+
: [resourceType, "video", "image", "raw"];
|
|
71
|
+
let lastErr;
|
|
72
|
+
for (const type of types) {
|
|
73
|
+
try {
|
|
74
|
+
const result = (await cloudinary.api.resource(publicId, {
|
|
75
|
+
resource_type: type
|
|
76
|
+
}));
|
|
77
|
+
return {
|
|
78
|
+
publicId: result.public_id,
|
|
79
|
+
secureUrl: result.secure_url,
|
|
80
|
+
resourceType: result.resource_type || type,
|
|
81
|
+
bytes: result.bytes,
|
|
82
|
+
etag: result.etag,
|
|
83
|
+
durationSec: typeof result.duration === "number" && Number.isFinite(result.duration)
|
|
84
|
+
? result.duration
|
|
85
|
+
: undefined
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
lastErr = err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw lastErr instanceof Error
|
|
93
|
+
? lastErr
|
|
94
|
+
: new Error("Cloudinary asset not found");
|
|
95
|
+
}
|
|
32
96
|
export async function uploadBufferToCloudinary(body, options) {
|
|
33
97
|
configureCloudinary(options.config);
|
|
34
|
-
const publicId = options.config.
|
|
35
|
-
? `${options.config.folder.replace(/^\/+|\/+$/g, "")}/${options.publicId}`
|
|
36
|
-
: options.publicId;
|
|
98
|
+
const publicId = resolvePublicId(options.config, options.publicId);
|
|
37
99
|
const result = await new Promise((resolve, reject) => {
|
|
38
100
|
const stream = cloudinary.uploader.upload_stream({
|
|
39
101
|
resource_type: "auto",
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -207,6 +207,8 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
207
207
|
private getSuggestedCmsUrl;
|
|
208
208
|
listen(options: ListenOptions): http.Server;
|
|
209
209
|
private handleMediaUpload;
|
|
210
|
+
private handleMediaUploadSign;
|
|
211
|
+
private handleMediaRegister;
|
|
210
212
|
private storeUploadedMediaAsset;
|
|
211
213
|
private getAssetIdsFromItems;
|
|
212
214
|
private isUploadedAssetReferenced;
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAYxB,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EACV,gBAAgB,EAKhB,eAAe,EAEhB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -8,7 +8,7 @@ import { generateDeterministicPairingCode, generateRandomPairingCode, isValidPai
|
|
|
8
8
|
import { PlaylistCatalog } from "./playlist-catalog.js";
|
|
9
9
|
import { MemoryStore } from "./store/memory-store.js";
|
|
10
10
|
import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-assets.js";
|
|
11
|
-
import { deleteCloudinaryAsset, resolveCloudinaryConfig, uploadBufferToCloudinary } from "./cloudinary-storage.js";
|
|
11
|
+
import { createSignedUploadParams, deleteCloudinaryAsset, fetchCloudinaryResource, resolveCloudinaryConfig, uploadBufferToCloudinary } from "./cloudinary-storage.js";
|
|
12
12
|
import { probeVideoDurationMs } from "./media-probe.js";
|
|
13
13
|
import { contentHashHex, storeUploadIfNeeded } from "./upload-storage.js";
|
|
14
14
|
import { detectPrimaryLanIpv4 } from "./lan-address.js";
|
|
@@ -969,6 +969,101 @@ export class TomorrowOS extends EventEmitter {
|
|
|
969
969
|
sendJson(res, 400, { status: "failed", error: msg });
|
|
970
970
|
}
|
|
971
971
|
}
|
|
972
|
+
async handleMediaUploadSign(req, res, url) {
|
|
973
|
+
const cloudinaryConfig = resolveCloudinaryConfig();
|
|
974
|
+
if (!cloudinaryConfig) {
|
|
975
|
+
sendJson(res, 404, {
|
|
976
|
+
status: "failed",
|
|
977
|
+
error: "Cloudinary is not configured"
|
|
978
|
+
});
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const rawName = url.searchParams.get("filename") || "upload";
|
|
982
|
+
const safeName = sanitizeUploadFilename(rawName);
|
|
983
|
+
const publicId = randomUUID().replace(/-/g, "");
|
|
984
|
+
const signed = createSignedUploadParams(cloudinaryConfig, {
|
|
985
|
+
publicId,
|
|
986
|
+
filename: safeName
|
|
987
|
+
});
|
|
988
|
+
sendJson(res, 200, {
|
|
989
|
+
status: "success",
|
|
990
|
+
mode: "cloudinary-direct",
|
|
991
|
+
...signed
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
async handleMediaRegister(req, res) {
|
|
995
|
+
const cloudinaryConfig = resolveCloudinaryConfig();
|
|
996
|
+
if (!cloudinaryConfig) {
|
|
997
|
+
sendJson(res, 400, {
|
|
998
|
+
status: "failed",
|
|
999
|
+
error: "Cloudinary is not configured"
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const body = (await readJsonBody(req));
|
|
1005
|
+
const publicId = String(body.publicId || "").trim();
|
|
1006
|
+
if (!publicId) {
|
|
1007
|
+
sendJson(res, 400, { status: "failed", error: "publicId is required" });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const safeName = sanitizeUploadFilename(body.originalFilename || path.basename(publicId) || "upload");
|
|
1011
|
+
const hintedType = String(body.resourceType || "auto").trim() || "auto";
|
|
1012
|
+
const remote = await fetchCloudinaryResource(cloudinaryConfig, publicId, hintedType);
|
|
1013
|
+
const sha256 = remote.etag
|
|
1014
|
+
? `cloudinary-etag:${remote.etag}`
|
|
1015
|
+
: `cloudinary:${remote.publicId}`;
|
|
1016
|
+
const existing = await this.store.getUploadedAssetBySha256(sha256);
|
|
1017
|
+
if (existing && isRemoteUploadedAssetUrl(existing.url)) {
|
|
1018
|
+
const payload = {
|
|
1019
|
+
status: "success",
|
|
1020
|
+
url: existing.url,
|
|
1021
|
+
assetId: existing.id,
|
|
1022
|
+
filename: safeName,
|
|
1023
|
+
size: existing.bytes,
|
|
1024
|
+
contentHash: existing.sha256,
|
|
1025
|
+
deduplicated: true
|
|
1026
|
+
};
|
|
1027
|
+
if (remote.durationSec != null) {
|
|
1028
|
+
payload.durationMs = Math.round(remote.durationSec * 1000);
|
|
1029
|
+
}
|
|
1030
|
+
sendJson(res, 200, payload);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const now = new Date().toISOString();
|
|
1034
|
+
const mimeType = typeof body.mimeType === "string" ? body.mimeType : undefined;
|
|
1035
|
+
const asset = {
|
|
1036
|
+
id: randomUUID(),
|
|
1037
|
+
sha256,
|
|
1038
|
+
storageKey: remote.publicId,
|
|
1039
|
+
url: remote.secureUrl,
|
|
1040
|
+
originalFilename: safeName,
|
|
1041
|
+
mimeType,
|
|
1042
|
+
resourceType: remote.resourceType || inferResourceType(mimeType, safeName),
|
|
1043
|
+
bytes: remote.bytes,
|
|
1044
|
+
createdAt: now,
|
|
1045
|
+
updatedAt: now
|
|
1046
|
+
};
|
|
1047
|
+
await this.store.setUploadedAsset(asset);
|
|
1048
|
+
const payload = {
|
|
1049
|
+
status: "success",
|
|
1050
|
+
url: asset.url,
|
|
1051
|
+
assetId: asset.id,
|
|
1052
|
+
filename: safeName,
|
|
1053
|
+
size: asset.bytes,
|
|
1054
|
+
contentHash: asset.sha256,
|
|
1055
|
+
deduplicated: false
|
|
1056
|
+
};
|
|
1057
|
+
if (remote.durationSec != null) {
|
|
1058
|
+
payload.durationMs = Math.round(remote.durationSec * 1000);
|
|
1059
|
+
}
|
|
1060
|
+
sendJson(res, 200, payload);
|
|
1061
|
+
}
|
|
1062
|
+
catch (e) {
|
|
1063
|
+
const msg = e instanceof Error ? e.message : "Register failed";
|
|
1064
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
972
1067
|
async storeUploadedMediaAsset(body, safeName, mimeType) {
|
|
973
1068
|
const sha256 = contentHashHex(body);
|
|
974
1069
|
const cloudinaryConfig = resolveCloudinaryConfig();
|
|
@@ -1269,6 +1364,14 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1269
1364
|
await this.handleMediaUpload(req, res, url);
|
|
1270
1365
|
return;
|
|
1271
1366
|
}
|
|
1367
|
+
if (req.method === "GET" && pathname === "/media/upload-sign") {
|
|
1368
|
+
await this.handleMediaUploadSign(req, res, url);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (req.method === "POST" && pathname === "/media/register") {
|
|
1372
|
+
await this.handleMediaRegister(req, res);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1272
1375
|
if (req.method === "GET" && this.staticRoot) {
|
|
1273
1376
|
const served = await this.tryServeStatic(pathname, res);
|
|
1274
1377
|
if (served)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.53",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.53",
|
|
4
4
|
"description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build-player": "tomorrowos build --platform tizen"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@tomorrowos/sdk": "^0.9.
|
|
16
|
+
"@tomorrowos/sdk": "^0.9.53",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -43,6 +43,10 @@ let uploadInProgress = false;
|
|
|
43
43
|
const UPLOAD_MAX_RETRIES = 3;
|
|
44
44
|
const DEVICE_RECONNECT_GRACE_MS = 60000;
|
|
45
45
|
const UPLOAD_TIMEOUT_MS = 120000;
|
|
46
|
+
/** Direct-to-Cloudinary: no XHR timeout (large videos can exceed 2 minutes). */
|
|
47
|
+
const CLOUDINARY_DIRECT_TIMEOUT_MS = 0;
|
|
48
|
+
/** Cloudinary chunked upload minimum is 5MB; use 8MB chunks. */
|
|
49
|
+
const CLOUDINARY_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Apply brand.json to the Control Panel (name + colours). No logo on the panel.
|
|
@@ -1661,7 +1665,164 @@ function uploadFileWithProgress(file, { onProgress, attempt }) {
|
|
|
1661
1665
|
});
|
|
1662
1666
|
}
|
|
1663
1667
|
|
|
1668
|
+
async function fetchCloudinaryUploadSign(filename) {
|
|
1669
|
+
const q = new URLSearchParams({ filename: filename || "upload" });
|
|
1670
|
+
const res = await fetch(`/media/upload-sign?${q.toString()}`, {
|
|
1671
|
+
cache: "no-store"
|
|
1672
|
+
});
|
|
1673
|
+
if (res.status === 404) return null;
|
|
1674
|
+
let payload = {};
|
|
1675
|
+
try {
|
|
1676
|
+
payload = await res.json();
|
|
1677
|
+
} catch {
|
|
1678
|
+
payload = {};
|
|
1679
|
+
}
|
|
1680
|
+
if (!res.ok || payload.status === "failed" || !payload.uploadUrl) {
|
|
1681
|
+
if (res.status === 404) return null;
|
|
1682
|
+
throw new Error(payload.error || `Upload sign failed (${res.status})`);
|
|
1683
|
+
}
|
|
1684
|
+
return payload;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function buildCloudinaryFormData(fileOrBlob, sign) {
|
|
1688
|
+
const form = new FormData();
|
|
1689
|
+
const name = String(sign.filenameOverride || fileOrBlob.name || "upload");
|
|
1690
|
+
form.append("file", fileOrBlob, name);
|
|
1691
|
+
form.append("api_key", String(sign.apiKey));
|
|
1692
|
+
form.append("timestamp", String(sign.timestamp));
|
|
1693
|
+
form.append("signature", String(sign.signature));
|
|
1694
|
+
form.append("public_id", String(sign.publicId));
|
|
1695
|
+
form.append("overwrite", "true");
|
|
1696
|
+
form.append("unique_filename", "false");
|
|
1697
|
+
form.append("use_filename", "false");
|
|
1698
|
+
form.append("filename_override", name);
|
|
1699
|
+
return form;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function xhrSendForm(url, form, { timeoutMs, onProgress, headers }) {
|
|
1703
|
+
return new Promise((resolve, reject) => {
|
|
1704
|
+
const xhr = new XMLHttpRequest();
|
|
1705
|
+
xhr.open("POST", url, true);
|
|
1706
|
+
xhr.timeout = timeoutMs;
|
|
1707
|
+
if (headers) {
|
|
1708
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1709
|
+
xhr.setRequestHeader(key, value);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
xhr.upload.onprogress = (event) => {
|
|
1713
|
+
if (!event.lengthComputable) return;
|
|
1714
|
+
if (typeof onProgress === "function") onProgress(event.loaded, event.total);
|
|
1715
|
+
};
|
|
1716
|
+
xhr.onerror = () => reject(new Error("Network error uploading to Cloudinary"));
|
|
1717
|
+
xhr.ontimeout = () => reject(new Error("Cloudinary upload timed out"));
|
|
1718
|
+
xhr.onload = () => {
|
|
1719
|
+
let payload = {};
|
|
1720
|
+
try {
|
|
1721
|
+
payload = xhr.responseText ? JSON.parse(xhr.responseText) : {};
|
|
1722
|
+
} catch {
|
|
1723
|
+
payload = {};
|
|
1724
|
+
}
|
|
1725
|
+
if (xhr.status >= 200 && xhr.status < 300 && !payload.error) {
|
|
1726
|
+
resolve(payload);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const msg =
|
|
1730
|
+
payload.error?.message ||
|
|
1731
|
+
payload.error ||
|
|
1732
|
+
`Cloudinary upload failed (${xhr.status})`;
|
|
1733
|
+
reject(new Error(typeof msg === "string" ? msg : JSON.stringify(msg)));
|
|
1734
|
+
};
|
|
1735
|
+
xhr.send(form);
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
async function uploadToCloudinaryDirect(file, sign, onProgress) {
|
|
1740
|
+
const uploadUrl = String(sign.uploadUrl);
|
|
1741
|
+
const uniqueUploadId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1742
|
+
|
|
1743
|
+
if (file.size <= CLOUDINARY_CHUNK_BYTES) {
|
|
1744
|
+
const form = buildCloudinaryFormData(file, sign);
|
|
1745
|
+
const result = await xhrSendForm(uploadUrl, form, {
|
|
1746
|
+
timeoutMs: CLOUDINARY_DIRECT_TIMEOUT_MS,
|
|
1747
|
+
onProgress: (loaded, total) => {
|
|
1748
|
+
if (typeof onProgress !== "function") return;
|
|
1749
|
+
onProgress(Math.round((loaded / total) * 100));
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
return result;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
let offset = 0;
|
|
1756
|
+
let finalResult = null;
|
|
1757
|
+
while (offset < file.size) {
|
|
1758
|
+
const end = Math.min(offset + CLOUDINARY_CHUNK_BYTES, file.size);
|
|
1759
|
+
const chunk = file.slice(offset, end);
|
|
1760
|
+
const form = buildCloudinaryFormData(chunk, sign);
|
|
1761
|
+
const chunkBase = offset;
|
|
1762
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1763
|
+
finalResult = await xhrSendForm(uploadUrl, form, {
|
|
1764
|
+
timeoutMs: CLOUDINARY_DIRECT_TIMEOUT_MS,
|
|
1765
|
+
headers: {
|
|
1766
|
+
"X-Unique-Upload-Id": uniqueUploadId,
|
|
1767
|
+
"Content-Range": `bytes ${offset}-${end - 1}/${file.size}`
|
|
1768
|
+
},
|
|
1769
|
+
onProgress: (loaded) => {
|
|
1770
|
+
if (typeof onProgress !== "function") return;
|
|
1771
|
+
const overall = Math.min(file.size, chunkBase + loaded);
|
|
1772
|
+
onProgress(Math.round((overall / file.size) * 100));
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
offset = end;
|
|
1776
|
+
}
|
|
1777
|
+
return finalResult;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async function registerCloudinaryUpload(file, cloudinaryResult) {
|
|
1781
|
+
const res = await fetch("/media/register", {
|
|
1782
|
+
method: "POST",
|
|
1783
|
+
headers: { "Content-Type": "application/json" },
|
|
1784
|
+
body: JSON.stringify({
|
|
1785
|
+
publicId: cloudinaryResult.public_id,
|
|
1786
|
+
resourceType: cloudinaryResult.resource_type || "auto",
|
|
1787
|
+
originalFilename: file.name,
|
|
1788
|
+
mimeType: file.type || undefined
|
|
1789
|
+
})
|
|
1790
|
+
});
|
|
1791
|
+
let payload = {};
|
|
1792
|
+
try {
|
|
1793
|
+
payload = await res.json();
|
|
1794
|
+
} catch {
|
|
1795
|
+
payload = {};
|
|
1796
|
+
}
|
|
1797
|
+
if (!res.ok || payload.status === "failed") {
|
|
1798
|
+
throw new Error(payload.error || `Register failed (${res.status})`);
|
|
1799
|
+
}
|
|
1800
|
+
return payload;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
async function uploadViaCloudinaryDirect(file, onProgress) {
|
|
1804
|
+
const sign = await fetchCloudinaryUploadSign(file.name);
|
|
1805
|
+
if (!sign) return null;
|
|
1806
|
+
const cloudinaryResult = await uploadToCloudinaryDirect(file, sign, onProgress);
|
|
1807
|
+
if (!cloudinaryResult?.public_id) {
|
|
1808
|
+
throw new Error("Cloudinary upload returned no public_id");
|
|
1809
|
+
}
|
|
1810
|
+
if (typeof onProgress === "function") onProgress(100);
|
|
1811
|
+
return registerCloudinaryUpload(file, cloudinaryResult);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1664
1814
|
async function uploadFile(file, onProgress) {
|
|
1815
|
+
try {
|
|
1816
|
+
const direct = await uploadViaCloudinaryDirect(file, onProgress);
|
|
1817
|
+
if (direct) return direct;
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
const msg = err?.message || String(err);
|
|
1820
|
+
// Fall back to proxy upload only when Cloudinary is unavailable.
|
|
1821
|
+
if (!/not configured|Upload sign failed \(404\)/i.test(msg)) {
|
|
1822
|
+
throw err;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1665
1826
|
let lastErr = null;
|
|
1666
1827
|
for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
1667
1828
|
try {
|
|
@@ -1669,6 +1830,9 @@ async function uploadFile(file, onProgress) {
|
|
|
1669
1830
|
return payload;
|
|
1670
1831
|
} catch (err) {
|
|
1671
1832
|
lastErr = err;
|
|
1833
|
+
const msg = err?.message || String(err);
|
|
1834
|
+
// 413 from host proxy will not succeed on retry — fail fast.
|
|
1835
|
+
if (/Upload failed \(413\)/i.test(msg)) break;
|
|
1672
1836
|
if (attempt >= UPLOAD_MAX_RETRIES) break;
|
|
1673
1837
|
await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
|
|
1674
1838
|
}
|
|
@@ -43,6 +43,10 @@ let uploadInProgress = false;
|
|
|
43
43
|
const UPLOAD_MAX_RETRIES = 3;
|
|
44
44
|
const DEVICE_RECONNECT_GRACE_MS = 60000;
|
|
45
45
|
const UPLOAD_TIMEOUT_MS = 120000;
|
|
46
|
+
/** Direct-to-Cloudinary: no XHR timeout (large videos can exceed 2 minutes). */
|
|
47
|
+
const CLOUDINARY_DIRECT_TIMEOUT_MS = 0;
|
|
48
|
+
/** Cloudinary chunked upload minimum is 5MB; use 8MB chunks. */
|
|
49
|
+
const CLOUDINARY_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Apply brand.json to the Control Panel (name + colours). No logo on the panel.
|
|
@@ -1662,7 +1666,162 @@ function uploadFileWithProgress(file, { onProgress, attempt }) {
|
|
|
1662
1666
|
});
|
|
1663
1667
|
}
|
|
1664
1668
|
|
|
1669
|
+
async function fetchCloudinaryUploadSign(filename) {
|
|
1670
|
+
const q = new URLSearchParams({ filename: filename || "upload" });
|
|
1671
|
+
const res = await fetch(`/media/upload-sign?${q.toString()}`, {
|
|
1672
|
+
cache: "no-store"
|
|
1673
|
+
});
|
|
1674
|
+
if (res.status === 404) return null;
|
|
1675
|
+
let payload = {};
|
|
1676
|
+
try {
|
|
1677
|
+
payload = await res.json();
|
|
1678
|
+
} catch {
|
|
1679
|
+
payload = {};
|
|
1680
|
+
}
|
|
1681
|
+
if (!res.ok || payload.status === "failed" || !payload.uploadUrl) {
|
|
1682
|
+
if (res.status === 404) return null;
|
|
1683
|
+
throw new Error(payload.error || `Upload sign failed (${res.status})`);
|
|
1684
|
+
}
|
|
1685
|
+
return payload;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function buildCloudinaryFormData(fileOrBlob, sign) {
|
|
1689
|
+
const form = new FormData();
|
|
1690
|
+
const name = String(sign.filenameOverride || fileOrBlob.name || "upload");
|
|
1691
|
+
form.append("file", fileOrBlob, name);
|
|
1692
|
+
form.append("api_key", String(sign.apiKey));
|
|
1693
|
+
form.append("timestamp", String(sign.timestamp));
|
|
1694
|
+
form.append("signature", String(sign.signature));
|
|
1695
|
+
form.append("public_id", String(sign.publicId));
|
|
1696
|
+
form.append("overwrite", "true");
|
|
1697
|
+
form.append("unique_filename", "false");
|
|
1698
|
+
form.append("use_filename", "false");
|
|
1699
|
+
form.append("filename_override", name);
|
|
1700
|
+
return form;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function xhrSendForm(url, form, { timeoutMs, onProgress, headers }) {
|
|
1704
|
+
return new Promise((resolve, reject) => {
|
|
1705
|
+
const xhr = new XMLHttpRequest();
|
|
1706
|
+
xhr.open("POST", url, true);
|
|
1707
|
+
xhr.timeout = timeoutMs;
|
|
1708
|
+
if (headers) {
|
|
1709
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1710
|
+
xhr.setRequestHeader(key, value);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
xhr.upload.onprogress = (event) => {
|
|
1714
|
+
if (!event.lengthComputable) return;
|
|
1715
|
+
if (typeof onProgress === "function") onProgress(event.loaded, event.total);
|
|
1716
|
+
};
|
|
1717
|
+
xhr.onerror = () => reject(new Error("Network error uploading to Cloudinary"));
|
|
1718
|
+
xhr.ontimeout = () => reject(new Error("Cloudinary upload timed out"));
|
|
1719
|
+
xhr.onload = () => {
|
|
1720
|
+
let payload = {};
|
|
1721
|
+
try {
|
|
1722
|
+
payload = xhr.responseText ? JSON.parse(xhr.responseText) : {};
|
|
1723
|
+
} catch {
|
|
1724
|
+
payload = {};
|
|
1725
|
+
}
|
|
1726
|
+
if (xhr.status >= 200 && xhr.status < 300 && !payload.error) {
|
|
1727
|
+
resolve(payload);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const msg =
|
|
1731
|
+
payload.error?.message ||
|
|
1732
|
+
payload.error ||
|
|
1733
|
+
`Cloudinary upload failed (${xhr.status})`;
|
|
1734
|
+
reject(new Error(typeof msg === "string" ? msg : JSON.stringify(msg)));
|
|
1735
|
+
};
|
|
1736
|
+
xhr.send(form);
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
async function uploadToCloudinaryDirect(file, sign, onProgress) {
|
|
1741
|
+
const uploadUrl = String(sign.uploadUrl);
|
|
1742
|
+
const uniqueUploadId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1743
|
+
|
|
1744
|
+
if (file.size <= CLOUDINARY_CHUNK_BYTES) {
|
|
1745
|
+
const form = buildCloudinaryFormData(file, sign);
|
|
1746
|
+
const result = await xhrSendForm(uploadUrl, form, {
|
|
1747
|
+
timeoutMs: CLOUDINARY_DIRECT_TIMEOUT_MS,
|
|
1748
|
+
onProgress: (loaded, total) => {
|
|
1749
|
+
if (typeof onProgress !== "function") return;
|
|
1750
|
+
onProgress(Math.round((loaded / total) * 100));
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
return result;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
let offset = 0;
|
|
1757
|
+
let finalResult = null;
|
|
1758
|
+
while (offset < file.size) {
|
|
1759
|
+
const end = Math.min(offset + CLOUDINARY_CHUNK_BYTES, file.size);
|
|
1760
|
+
const chunk = file.slice(offset, end);
|
|
1761
|
+
const form = buildCloudinaryFormData(chunk, sign);
|
|
1762
|
+
const chunkBase = offset;
|
|
1763
|
+
finalResult = await xhrSendForm(uploadUrl, form, {
|
|
1764
|
+
timeoutMs: CLOUDINARY_DIRECT_TIMEOUT_MS,
|
|
1765
|
+
headers: {
|
|
1766
|
+
"X-Unique-Upload-Id": uniqueUploadId,
|
|
1767
|
+
"Content-Range": `bytes ${offset}-${end - 1}/${file.size}`
|
|
1768
|
+
},
|
|
1769
|
+
onProgress: (loaded) => {
|
|
1770
|
+
if (typeof onProgress !== "function") return;
|
|
1771
|
+
const overall = Math.min(file.size, chunkBase + loaded);
|
|
1772
|
+
onProgress(Math.round((overall / file.size) * 100));
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
offset = end;
|
|
1776
|
+
}
|
|
1777
|
+
return finalResult;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async function registerCloudinaryUpload(file, cloudinaryResult) {
|
|
1781
|
+
const res = await fetch("/media/register", {
|
|
1782
|
+
method: "POST",
|
|
1783
|
+
headers: { "Content-Type": "application/json" },
|
|
1784
|
+
body: JSON.stringify({
|
|
1785
|
+
publicId: cloudinaryResult.public_id,
|
|
1786
|
+
resourceType: cloudinaryResult.resource_type || "auto",
|
|
1787
|
+
originalFilename: file.name,
|
|
1788
|
+
mimeType: file.type || undefined
|
|
1789
|
+
})
|
|
1790
|
+
});
|
|
1791
|
+
let payload = {};
|
|
1792
|
+
try {
|
|
1793
|
+
payload = await res.json();
|
|
1794
|
+
} catch {
|
|
1795
|
+
payload = {};
|
|
1796
|
+
}
|
|
1797
|
+
if (!res.ok || payload.status === "failed") {
|
|
1798
|
+
throw new Error(payload.error || `Register failed (${res.status})`);
|
|
1799
|
+
}
|
|
1800
|
+
return payload;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
async function uploadViaCloudinaryDirect(file, onProgress) {
|
|
1804
|
+
const sign = await fetchCloudinaryUploadSign(file.name);
|
|
1805
|
+
if (!sign) return null;
|
|
1806
|
+
const cloudinaryResult = await uploadToCloudinaryDirect(file, sign, onProgress);
|
|
1807
|
+
if (!cloudinaryResult?.public_id) {
|
|
1808
|
+
throw new Error("Cloudinary upload returned no public_id");
|
|
1809
|
+
}
|
|
1810
|
+
if (typeof onProgress === "function") onProgress(100);
|
|
1811
|
+
return registerCloudinaryUpload(file, cloudinaryResult);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1665
1814
|
async function uploadFile(file, onProgress) {
|
|
1815
|
+
try {
|
|
1816
|
+
const direct = await uploadViaCloudinaryDirect(file, onProgress);
|
|
1817
|
+
if (direct) return direct;
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
const msg = err?.message || String(err);
|
|
1820
|
+
if (!/not configured|Upload sign failed \(404\)/i.test(msg)) {
|
|
1821
|
+
throw err;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1666
1825
|
let lastErr = null;
|
|
1667
1826
|
for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
1668
1827
|
try {
|
|
@@ -1670,6 +1829,8 @@ async function uploadFile(file, onProgress) {
|
|
|
1670
1829
|
return payload;
|
|
1671
1830
|
} catch (err) {
|
|
1672
1831
|
lastErr = err;
|
|
1832
|
+
const msg = err?.message || String(err);
|
|
1833
|
+
if (/Upload failed \(413\)/i.test(msg)) break;
|
|
1673
1834
|
if (attempt >= UPLOAD_MAX_RETRIES) break;
|
|
1674
1835
|
await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
|
|
1675
1836
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.53",
|
|
4
4
|
"description": "TomorrowOS CMS for Vercel / v0 Publish (Fluid Function + WebSockets + optional Next Preview).",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"build-player": "tomorrowos build --platform tizen"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@tomorrowos/sdk": "^0.9.
|
|
20
|
+
"@tomorrowos/sdk": "^0.9.53",
|
|
21
21
|
"dotenv": "^17.2.3",
|
|
22
22
|
"tsx": "^4.19.0"
|
|
23
23
|
},
|