@tomorrowos/sdk 0.9.51 → 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 +110 -4
- 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";
|
|
@@ -168,9 +168,12 @@ function isAllowedWebSocketPath(pathname, allowed) {
|
|
|
168
168
|
const trimmed = p.replace(/\/+$/, "") || "/";
|
|
169
169
|
return allowed.has(trimmed) || allowed.has(`${trimmed}/`);
|
|
170
170
|
}
|
|
171
|
-
|
|
171
|
+
/** JSON API bodies stay capped; media uploads have no SDK size cap. */
|
|
172
|
+
const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024;
|
|
173
|
+
/** `null` = no limit (host/Cloudinary may still enforce their own caps). */
|
|
174
|
+
const MAX_MEDIA_UPLOAD_BYTES = null;
|
|
172
175
|
async function readJsonBody(req) {
|
|
173
|
-
const buf = await readRawBody(req,
|
|
176
|
+
const buf = await readRawBody(req, MAX_JSON_BODY_BYTES);
|
|
174
177
|
const raw = buf.toString("utf8");
|
|
175
178
|
if (!raw.trim())
|
|
176
179
|
return {};
|
|
@@ -187,7 +190,7 @@ async function readRawBody(req, maxBytes) {
|
|
|
187
190
|
for await (const chunk of req) {
|
|
188
191
|
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
189
192
|
total += buf.length;
|
|
190
|
-
if (total > maxBytes) {
|
|
193
|
+
if (maxBytes != null && maxBytes > 0 && total > maxBytes) {
|
|
191
194
|
throw new Error(`Body too large (max ${maxBytes} bytes)`);
|
|
192
195
|
}
|
|
193
196
|
chunks.push(buf);
|
|
@@ -966,6 +969,101 @@ export class TomorrowOS extends EventEmitter {
|
|
|
966
969
|
sendJson(res, 400, { status: "failed", error: msg });
|
|
967
970
|
}
|
|
968
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
|
+
}
|
|
969
1067
|
async storeUploadedMediaAsset(body, safeName, mimeType) {
|
|
970
1068
|
const sha256 = contentHashHex(body);
|
|
971
1069
|
const cloudinaryConfig = resolveCloudinaryConfig();
|
|
@@ -1266,6 +1364,14 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1266
1364
|
await this.handleMediaUpload(req, res, url);
|
|
1267
1365
|
return;
|
|
1268
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
|
+
}
|
|
1269
1375
|
if (req.method === "GET" && this.staticRoot) {
|
|
1270
1376
|
const served = await this.tryServeStatic(pathname, res);
|
|
1271
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
|
},
|