@tomorrowos/sdk 0.9.67 → 0.9.69
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/README.md +41 -56
- package/REPLIT_SETUP.md +184 -184
- package/VERCEL_QUESTIONS.md +1 -1
- package/VERCEL_SETUP.md +15 -20
- package/dist/server-status.d.ts.map +1 -1
- package/dist/server-status.js +118 -34
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +41 -1
- package/dist/vercel-blob-storage.d.ts +28 -0
- package/dist/vercel-blob-storage.d.ts.map +1 -0
- package/dist/vercel-blob-storage.js +70 -0
- package/package.json +2 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter-v0/package.json +2 -2
package/VERCEL_QUESTIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vercel / v0 - Verbatim question bank
|
|
2
2
|
|
|
3
|
-
> **Protocol:** `vercel-questions/1.5` (pairs with `VERCEL_SETUP.md` 1.
|
|
3
|
+
> **Protocol:** `vercel-questions/1.5` (pairs with `VERCEL_SETUP.md` 1.14+ / `@tomorrowos/sdk` 0.9.69+)
|
|
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 "Blob recommended", no "disable 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`).
|
|
7
7
|
>
|
|
8
|
-
> Protocol id: `vercel-setup/1.
|
|
8
|
+
> Protocol id: `vercel-setup/1.14` - pairs with `@tomorrowos/sdk` **0.9.69+** (`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
|
> **Q1/Q2 = choice questions. Q3 = form only:** open **all seven optional inputs at once**, then **HARD STOP and WAIT** for submit or "skip". Never auto-skip because fields are optional. Never ask a branding choice first. Never show only Name + Primary colour. Logo = **Upload** above **Logo URL**.
|
|
@@ -465,7 +465,7 @@ Adapt script names / process runners as needed. **Invariant:**
|
|
|
465
465
|
|
|
466
466
|
Use the latest published `@tomorrowos/sdk` when scaffolding (do not invent versions).
|
|
467
467
|
|
|
468
|
-
Add `@vercel/blob`
|
|
468
|
+
Add nothing extra for Blob when using `@tomorrowos/sdk@0.9.69+` (`@vercel/blob` is bundled). Only ensure `BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob` when the user chooses **Vercel Blob** in Question 2.
|
|
469
469
|
|
|
470
470
|
For Preview shell only, add as needed: `next`, `react`, `react-dom`, and a process runner (`concurrently` or equivalent). These must **not** become the Production CMS.
|
|
471
471
|
|
|
@@ -721,13 +721,13 @@ If you catch yourself about to offer Blob-as-recommended, Supabase Storage, loca
|
|
|
721
721
|
|
|
722
722
|
The SDK auto-detects these env vars.
|
|
723
723
|
|
|
724
|
-
### Step C
|
|
724
|
+
### Step C - If **2 / Vercel Blob** (same Question 2)
|
|
725
725
|
|
|
726
726
|
**Ask exactly:**
|
|
727
727
|
|
|
728
728
|
> I will enable **Vercel Blob** for media uploads.
|
|
729
729
|
>
|
|
730
|
-
> 1. In the Vercel project: **Storage
|
|
730
|
+
> 1. In the Vercel project: **Storage ? Create ? Blob** (or link an existing Blob store to this project).
|
|
731
731
|
> 2. Confirm **`BLOB_READ_WRITE_TOKEN`** is available (Vercel usually injects it when Blob is linked).
|
|
732
732
|
>
|
|
733
733
|
> Paste the token only if it is not already set in your project Env Vars. Do you already have Blob linked on this Vercel project? (yes / no)
|
|
@@ -735,21 +735,13 @@ The SDK auto-detects these env vars.
|
|
|
735
735
|
**You must then:**
|
|
736
736
|
|
|
737
737
|
1. Set Vercel Env Var: `BLOB_READ_WRITE_TOKEN=<token>` (if not auto-injected). Prefer the Env popup when available.
|
|
738
|
-
2.
|
|
739
|
-
3.
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
```ts
|
|
744
|
-
import { put } from "@vercel/blob";
|
|
745
|
-
|
|
746
|
-
// On upload: const blob = await put(filename, body, { access: "public", token: process.env.BLOB_READ_WRITE_TOKEN });
|
|
747
|
-
// Store blob.url in uploaded_assets ??same shape as Cloudinary secure URLs.
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
4. Warn: until the project?s Blob bridge is wired, Control Panel **Media** status may show local/ephemeral ??verify uploads return `https://*.blob.vercel-storage.com/...` before calling setup complete.
|
|
751
|
-
5. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
|
|
738
|
+
2. Set Vercel Env Var yourself (agent-owned): `TOMORROWOS_MEDIA=vercel-blob`. **Do not** ask the user to type this.
|
|
739
|
+
3. Ensure `@tomorrowos/sdk@0.9.69+` is installed (`@vercel/blob` is a transitive dependency ? no project-level Blob bridge / custom `put()` middleware).
|
|
740
|
+
4. The SDK **natively** uploads via Vercel Blob when the token is present (same HTTP routes as local: `/media/upload` and chunked complete). Returned asset URLs are absolute `https://*.public.blob.vercel-storage.com/...`.
|
|
741
|
+
5. Confirm Control Panel **Media Server** shows provider **Blob** (not Local). Do **not** invent status copy about "Blob bridge" or paste the URL pattern as a status detail.
|
|
742
|
+
6. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
|
|
752
743
|
|
|
744
|
+
**Forbidden for Q2=Blob:** scaffolding a custom "Blob bridge", writing status text like "uploads persist to durable https://*.public.blob.vercel-storage.com/ URLs via the Blob bridge", or leaving uploads on local `cms-panel/uploads`.
|
|
753
745
|
---
|
|
754
746
|
|
|
755
747
|
## Question 3 - Brand form (`brand.json` only; **not a choice question**; **must wait**)
|
|
@@ -874,7 +866,7 @@ Then (**Scheme A ??mandatory for Publish**):
|
|
|
874
866
|
4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty, Start = `npm run start`, Fluid on). Patch dashboard via API if it still says Next.js.
|
|
875
867
|
5. Do **not** add root `api/` catch-all for raw `http.Server`.
|
|
876
868
|
6. Do **not** add `.replit*`.
|
|
877
|
-
7. If Q2 = Vercel Blob:
|
|
869
|
+
7. If Q2 = Vercel Blob: set `BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob` (SDK-native; no custom bridge).
|
|
878
870
|
|
|
879
871
|
### B. Environment Variables (Vercel dashboard + local)
|
|
880
872
|
|
|
@@ -886,6 +878,7 @@ Then (**Scheme A ??mandatory for Publish**):
|
|
|
886
878
|
| `DATABASE_SSL` | Yes for Postgres | **Agent auto-sets only** ??never a blank field for the user (`true`) |
|
|
887
879
|
| `CLOUDINARY_*` | If Q2 = Cloudinary | User via **one Env popup** (all fields) |
|
|
888
880
|
| `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | User / Blob link |
|
|
881
|
+
| `TOMORROWOS_MEDIA` | If Q2 = Vercel Blob | **Agent auto-sets** `vercel-blob` |
|
|
889
882
|
| `TOMORROWOS_INTERNAL_PORT` | Preview only | Agent (e.g. `3001`) |
|
|
890
883
|
| `PORT` | Optional | Vercel injects for Production |
|
|
891
884
|
|
|
@@ -1020,7 +1013,9 @@ Protocol complete when:
|
|
|
1020
1013
|
|
|
1021
1014
|
## Protocol version
|
|
1022
1015
|
|
|
1023
|
-
`vercel-setup/1.
|
|
1016
|
+
`vercel-setup/1.14` - pairs with `@tomorrowos/sdk` 0.9.69+ and `VERCEL_QUESTIONS.md` 1.5.
|
|
1017
|
+
|
|
1018
|
+
**Changelog 1.14:** Vercel Blob is **SDK-native** (`BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob`). No project Blob bridge. Status providers use display names (Neon, Supabase, Blob, Cloudinary, Replit Object Storage). Forbidden marketing status copy about blob URL patterns.
|
|
1024
1019
|
|
|
1025
1020
|
**Changelog 1.13:** Q3 must **HARD STOP and WAIT** for user submit or "skip". Optional fields do **not** authorize auto-continuing with defaults. Clarifies "do not re-ask" = after the user already replied, not "skip waiting".
|
|
1026
1021
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-status.d.ts","sourceRoot":"","sources":["../src/server-status.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server-status.d.ts","sourceRoot":"","sources":["../src/server-status.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,IAAI,GAAG,UAAU,GAAG,SAAS,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA4ZD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA6B7B"}
|
package/dist/server-status.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { getReplitObjectStorageClient, isReplitHost, resolveReplitObjectStorageMode } from "./replit-object-storage.js";
|
|
4
4
|
import { pingCloudinary, resolveCloudinaryConfig } from "./cloudinary-storage.js";
|
|
5
|
+
import { isVercelBlobConfigured, pingVercelBlob, resolveVercelBlobMode, resolveVercelBlobToken } from "./vercel-blob-storage.js";
|
|
5
6
|
import { getSdkPackageVersion } from "./sdk-version.js";
|
|
6
7
|
/** Replit Preview/dev often cannot resolve Supabase hosts — not a config error. */
|
|
7
8
|
function isReplitPreviewDnsFailure(message) {
|
|
@@ -13,33 +14,76 @@ function isReplitPreviewDnsFailure(message) {
|
|
|
13
14
|
m.includes("cannot resolve") ||
|
|
14
15
|
m.includes("name or service not known"));
|
|
15
16
|
}
|
|
17
|
+
function isNeonDatabaseUrl(url) {
|
|
18
|
+
const u = String(url || "").toLowerCase();
|
|
19
|
+
return (u.includes("neon.tech") ||
|
|
20
|
+
u.includes("neon.db") ||
|
|
21
|
+
/\.neon\./.test(u));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Display name shown next to "Database" in the Control Panel status card.
|
|
25
|
+
* Prefer specific products (Neon / Supabase / Replit Postgres) over generic PostgreSQL.
|
|
26
|
+
*/
|
|
16
27
|
function resolveDatabaseProvider(env) {
|
|
17
28
|
const driver = String(env.TOMORROWOS_STORE || "").trim().toLowerCase();
|
|
18
|
-
const
|
|
19
|
-
|
|
29
|
+
const supabaseUrl = String(env.SUPABASE_URL || "").trim();
|
|
30
|
+
const databaseUrl = String(env.DATABASE_URL || "").trim();
|
|
31
|
+
const hasUrl = !!(supabaseUrl || databaseUrl);
|
|
32
|
+
const probeUrl = supabaseUrl || databaseUrl;
|
|
33
|
+
if (driver === "supabase" || (!driver && supabaseUrl)) {
|
|
20
34
|
return {
|
|
21
|
-
provider: "
|
|
35
|
+
provider: "Supabase",
|
|
22
36
|
label: "Database",
|
|
23
|
-
configured:
|
|
37
|
+
configured: hasUrl,
|
|
38
|
+
kind: "supabase"
|
|
24
39
|
};
|
|
25
40
|
}
|
|
26
|
-
if (driver === "postgres" || (!driver && hasUrl && !
|
|
41
|
+
if (driver === "postgres" || (!driver && hasUrl && !supabaseUrl)) {
|
|
42
|
+
if (isNeonDatabaseUrl(probeUrl)) {
|
|
43
|
+
return {
|
|
44
|
+
provider: "Neon",
|
|
45
|
+
label: "Database",
|
|
46
|
+
configured: hasUrl,
|
|
47
|
+
kind: "postgres"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (isReplitHost(env)) {
|
|
51
|
+
return {
|
|
52
|
+
provider: "Replit Postgres",
|
|
53
|
+
label: "Database",
|
|
54
|
+
configured: hasUrl,
|
|
55
|
+
kind: "postgres"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
27
58
|
return {
|
|
28
|
-
provider: "
|
|
59
|
+
provider: "PostgreSQL",
|
|
29
60
|
label: "Database",
|
|
30
|
-
configured: hasUrl
|
|
61
|
+
configured: hasUrl,
|
|
62
|
+
kind: "postgres"
|
|
31
63
|
};
|
|
32
64
|
}
|
|
33
65
|
if (driver === "memory") {
|
|
34
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
provider: "Memory",
|
|
68
|
+
label: "Database",
|
|
69
|
+
configured: true,
|
|
70
|
+
kind: "memory"
|
|
71
|
+
};
|
|
35
72
|
}
|
|
36
73
|
if (driver === "sqlite" || !driver) {
|
|
37
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
provider: "SQLite",
|
|
76
|
+
label: "Database",
|
|
77
|
+
configured: true,
|
|
78
|
+
kind: "sqlite"
|
|
79
|
+
};
|
|
38
80
|
}
|
|
81
|
+
const pretty = driver.charAt(0).toUpperCase() + driver.slice(1).toLowerCase();
|
|
39
82
|
return {
|
|
40
|
-
provider:
|
|
83
|
+
provider: pretty || "Unknown",
|
|
41
84
|
label: "Database",
|
|
42
|
-
configured: hasUrl || driver === "sqlite" || driver === "memory"
|
|
85
|
+
configured: hasUrl || driver === "sqlite" || driver === "memory",
|
|
86
|
+
kind: "unknown"
|
|
43
87
|
};
|
|
44
88
|
}
|
|
45
89
|
async function withTimeout(promise, timeoutMs, label) {
|
|
@@ -59,7 +103,7 @@ async function withTimeout(promise, timeoutMs, label) {
|
|
|
59
103
|
}
|
|
60
104
|
async function probeDatabase(store, env, timeoutMs) {
|
|
61
105
|
const meta = resolveDatabaseProvider(env);
|
|
62
|
-
const wantsRemote = meta.
|
|
106
|
+
const wantsRemote = meta.kind === "supabase" || meta.kind === "postgres";
|
|
63
107
|
if (wantsRemote && !meta.configured) {
|
|
64
108
|
return {
|
|
65
109
|
id: "database",
|
|
@@ -71,7 +115,7 @@ async function probeDatabase(store, env, timeoutMs) {
|
|
|
71
115
|
}
|
|
72
116
|
try {
|
|
73
117
|
await withTimeout(store.listPairedDevices(), timeoutMs, meta.label);
|
|
74
|
-
if (meta.
|
|
118
|
+
if (meta.kind === "memory") {
|
|
75
119
|
return {
|
|
76
120
|
id: "database",
|
|
77
121
|
label: meta.label,
|
|
@@ -80,7 +124,7 @@ async function probeDatabase(store, env, timeoutMs) {
|
|
|
80
124
|
detail: "In-memory only — data is lost on restart."
|
|
81
125
|
};
|
|
82
126
|
}
|
|
83
|
-
if (meta.
|
|
127
|
+
if (meta.kind === "sqlite" && isReplitHost(env)) {
|
|
84
128
|
return {
|
|
85
129
|
id: "database",
|
|
86
130
|
label: meta.label,
|
|
@@ -94,7 +138,7 @@ async function probeDatabase(store, env, timeoutMs) {
|
|
|
94
138
|
label: meta.label,
|
|
95
139
|
provider: meta.provider,
|
|
96
140
|
state: "ok",
|
|
97
|
-
detail: `${meta.
|
|
141
|
+
detail: `${meta.provider} reachable`
|
|
98
142
|
};
|
|
99
143
|
}
|
|
100
144
|
catch (err) {
|
|
@@ -127,7 +171,7 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
127
171
|
return {
|
|
128
172
|
id: "media",
|
|
129
173
|
label: "Media Server",
|
|
130
|
-
provider: "
|
|
174
|
+
provider: "Cloudinary",
|
|
131
175
|
state: "ok",
|
|
132
176
|
detail: `Cloudinary (${cloudinary.cloudName}) reachable`
|
|
133
177
|
};
|
|
@@ -137,7 +181,7 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
137
181
|
return {
|
|
138
182
|
id: "media",
|
|
139
183
|
label: "Media Server",
|
|
140
|
-
provider: "
|
|
184
|
+
provider: "Cloudinary",
|
|
141
185
|
state: "error",
|
|
142
186
|
detail: message
|
|
143
187
|
};
|
|
@@ -150,11 +194,47 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
150
194
|
return {
|
|
151
195
|
id: "media",
|
|
152
196
|
label: "Media Server",
|
|
153
|
-
provider: "
|
|
197
|
+
provider: "Cloudinary",
|
|
154
198
|
state: "error",
|
|
155
199
|
detail: "Cloudinary config incomplete — set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET."
|
|
156
200
|
};
|
|
157
201
|
}
|
|
202
|
+
const blobMode = resolveVercelBlobMode(env);
|
|
203
|
+
if (blobMode !== "off") {
|
|
204
|
+
const token = resolveVercelBlobToken(env);
|
|
205
|
+
if (!token) {
|
|
206
|
+
return {
|
|
207
|
+
id: "media",
|
|
208
|
+
label: "Media Server",
|
|
209
|
+
provider: "Blob",
|
|
210
|
+
state: "error",
|
|
211
|
+
detail: "Vercel Blob required but BLOB_READ_WRITE_TOKEN is not set. Link a Blob store to this Vercel project."
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await withTimeout(pingVercelBlob(token), timeoutMs, "Blob");
|
|
216
|
+
return {
|
|
217
|
+
id: "media",
|
|
218
|
+
label: "Media Server",
|
|
219
|
+
provider: "Blob",
|
|
220
|
+
state: "ok",
|
|
221
|
+
detail: "Blob reachable"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
226
|
+
if (blobMode === "required" || isVercelBlobConfigured(env)) {
|
|
227
|
+
return {
|
|
228
|
+
id: "media",
|
|
229
|
+
label: "Media Server",
|
|
230
|
+
provider: "Blob",
|
|
231
|
+
state: "error",
|
|
232
|
+
detail: message
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// preferred but ping failed: fall through
|
|
236
|
+
}
|
|
237
|
+
}
|
|
158
238
|
const rosMode = resolveReplitObjectStorageMode(env);
|
|
159
239
|
if (rosMode !== "off") {
|
|
160
240
|
try {
|
|
@@ -164,16 +244,16 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
164
244
|
return {
|
|
165
245
|
id: "media",
|
|
166
246
|
label: "Media Server",
|
|
167
|
-
provider: "
|
|
247
|
+
provider: "Replit Object Storage",
|
|
168
248
|
state: "ok",
|
|
169
|
-
detail: "Replit Object Storage
|
|
249
|
+
detail: "Replit Object Storage reachable"
|
|
170
250
|
};
|
|
171
251
|
}
|
|
172
252
|
if (rosMode === "required") {
|
|
173
253
|
return {
|
|
174
254
|
id: "media",
|
|
175
255
|
label: "Media Server",
|
|
176
|
-
provider: "
|
|
256
|
+
provider: "Replit Object Storage",
|
|
177
257
|
state: "error",
|
|
178
258
|
detail: "TOMORROWOS_MEDIA requires Replit Object Storage but no client is available. Link an App Storage bucket and install @replit/object-storage."
|
|
179
259
|
};
|
|
@@ -185,7 +265,7 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
185
265
|
return {
|
|
186
266
|
id: "media",
|
|
187
267
|
label: "Media Server",
|
|
188
|
-
provider: "
|
|
268
|
+
provider: "Replit Object Storage",
|
|
189
269
|
state: "error",
|
|
190
270
|
detail: message
|
|
191
271
|
};
|
|
@@ -193,9 +273,10 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
193
273
|
// preferred: fall through to local disk probe
|
|
194
274
|
}
|
|
195
275
|
}
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
276
|
+
const onReplit = isReplitHost(env);
|
|
277
|
+
const provider = onReplit ? "Replit Object Storage" : "Local";
|
|
278
|
+
const providerLabel = onReplit
|
|
279
|
+
? "Local uploads (Object Storage not active)"
|
|
199
280
|
: "Local uploads";
|
|
200
281
|
if (!staticRoot) {
|
|
201
282
|
return {
|
|
@@ -203,7 +284,7 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
203
284
|
label: "Media Server",
|
|
204
285
|
provider,
|
|
205
286
|
state: "missing",
|
|
206
|
-
detail: "No
|
|
287
|
+
detail: "No durable media backend and staticRoot is unset — uploads unavailable."
|
|
207
288
|
};
|
|
208
289
|
}
|
|
209
290
|
const uploadsDir = path.join(path.resolve(staticRoot), "uploads");
|
|
@@ -215,7 +296,7 @@ async function probeMedia(staticRoot, env, timeoutMs) {
|
|
|
215
296
|
label: "Media Server",
|
|
216
297
|
provider,
|
|
217
298
|
state: "warn",
|
|
218
|
-
detail: `${providerLabel}
|
|
299
|
+
detail: `${providerLabel} at ${uploadsDir}. Prefer Cloudinary, Blob, or Replit Object Storage for production.`
|
|
219
300
|
};
|
|
220
301
|
}
|
|
221
302
|
catch (err) {
|
|
@@ -239,19 +320,22 @@ function buildBlockers(connectors) {
|
|
|
239
320
|
message: c.detail,
|
|
240
321
|
fixHint: c.state === "missing"
|
|
241
322
|
? "Paste your Supabase Postgres connection string as Secret SUPABASE_URL, set TOMORROWOS_STORE=supabase, then restart the CMS."
|
|
242
|
-
: "Check SUPABASE_URL / network / SSL (DATABASE_SSL=true). After Publish, confirm the live URL can reach
|
|
323
|
+
: "Check SUPABASE_URL / DATABASE_URL / network / SSL (DATABASE_SSL=true). After Publish, confirm the live URL can reach the database."
|
|
243
324
|
});
|
|
244
325
|
}
|
|
245
326
|
if (c.id === "media" && c.state === "error") {
|
|
327
|
+
const provider = String(c.provider || "");
|
|
246
328
|
blockers.push({
|
|
247
329
|
connectorId: c.id,
|
|
248
330
|
title: "Media storage not ready",
|
|
249
331
|
message: c.detail,
|
|
250
|
-
fixHint:
|
|
332
|
+
fixHint: provider === "Cloudinary"
|
|
251
333
|
? "Fix Cloudinary Secrets (CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET) and restart."
|
|
252
|
-
:
|
|
253
|
-
? "Link a
|
|
254
|
-
: "
|
|
334
|
+
: provider === "Blob"
|
|
335
|
+
? "Link a Vercel Blob store, set BLOB_READ_WRITE_TOKEN (and TOMORROWOS_MEDIA=vercel-blob), then redeploy."
|
|
336
|
+
: provider === "Replit Object Storage"
|
|
337
|
+
? "Link a Replit App Storage bucket, set TOMORROWOS_MEDIA=replit-object-storage, ensure @replit/object-storage is installed, then restart."
|
|
338
|
+
: "Configure Cloudinary, Vercel Blob, or Replit Object Storage for durable media URLs."
|
|
255
339
|
});
|
|
256
340
|
}
|
|
257
341
|
if (c.id === "media" && c.state === "missing") {
|
|
@@ -259,7 +343,7 @@ function buildBlockers(connectors) {
|
|
|
259
343
|
connectorId: c.id,
|
|
260
344
|
title: "Media storage not configured",
|
|
261
345
|
message: c.detail,
|
|
262
|
-
fixHint: "Set Cloudinary
|
|
346
|
+
fixHint: "Set Cloudinary, Vercel Blob (BLOB_READ_WRITE_TOKEN), or ensure the CMS listens with staticRoot pointing at public/ / cms-panel/."
|
|
263
347
|
});
|
|
264
348
|
}
|
|
265
349
|
}
|
|
@@ -280,7 +364,7 @@ export async function buildServerStatus(options) {
|
|
|
280
364
|
const server = {
|
|
281
365
|
id: "server",
|
|
282
366
|
label: "Server",
|
|
283
|
-
provider: "
|
|
367
|
+
provider: "TomorrowOS",
|
|
284
368
|
state: "ok",
|
|
285
369
|
detail: "CMS process is running"
|
|
286
370
|
};
|
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;AAaxB,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
|
@@ -10,6 +10,7 @@ import { PlaylistCatalog } from "./playlist-catalog.js";
|
|
|
10
10
|
import { MemoryStore } from "./store/memory-store.js";
|
|
11
11
|
import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-assets.js";
|
|
12
12
|
import { createSignedUploadParams, deleteCloudinaryAsset, fetchCloudinaryResource, resolveCloudinaryConfig, uploadBufferToCloudinary } from "./cloudinary-storage.js";
|
|
13
|
+
import { isVercelBlobConfigured, resolveVercelBlobMode, resolveVercelBlobToken, uploadBufferToVercelBlob } from "./vercel-blob-storage.js";
|
|
13
14
|
import { probeVideoDurationMs } from "./media-probe.js";
|
|
14
15
|
import { assembleChunkedUpload, cleanupChunkedUpload, cleanupStaleChunkedUploads, initChunkedUpload, MEDIA_UPLOAD_CHUNK_BYTES, storeChunkedUploadPart } from "./chunked-upload.js";
|
|
15
16
|
import { buildBrightSignZipWithCmsEndpoint, resolveRequestCmsOrigin } from "./player-zip.js";
|
|
@@ -1342,6 +1343,44 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1342
1343
|
await this.store.setUploadedAsset(asset);
|
|
1343
1344
|
return { asset, deduplicated: false };
|
|
1344
1345
|
}
|
|
1346
|
+
const blobMode = resolveVercelBlobMode();
|
|
1347
|
+
const blobToken = resolveVercelBlobToken();
|
|
1348
|
+
if (blobMode !== "off") {
|
|
1349
|
+
if (!blobToken) {
|
|
1350
|
+
if (blobMode === "required") {
|
|
1351
|
+
throw new Error("TOMORROWOS_MEDIA=vercel-blob requires BLOB_READ_WRITE_TOKEN. Link a Blob store to this Vercel project.");
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
else {
|
|
1355
|
+
try {
|
|
1356
|
+
const storedName = buildContentAddressedNameFromHash(sha256, safeName);
|
|
1357
|
+
const uploaded = await uploadBufferToVercelBlob(body, {
|
|
1358
|
+
pathname: `uploads/${storedName}`,
|
|
1359
|
+
token: blobToken,
|
|
1360
|
+
contentType: mimeType
|
|
1361
|
+
});
|
|
1362
|
+
const asset = {
|
|
1363
|
+
id: existing?.id ?? randomUUID(),
|
|
1364
|
+
sha256,
|
|
1365
|
+
storageKey: uploaded.pathname || storedName,
|
|
1366
|
+
url: uploaded.url,
|
|
1367
|
+
originalFilename: safeName,
|
|
1368
|
+
mimeType,
|
|
1369
|
+
resourceType,
|
|
1370
|
+
bytes: body.length,
|
|
1371
|
+
createdAt: existing?.createdAt ?? now,
|
|
1372
|
+
updatedAt: now
|
|
1373
|
+
};
|
|
1374
|
+
await this.store.setUploadedAsset(asset);
|
|
1375
|
+
return { asset, deduplicated: false };
|
|
1376
|
+
}
|
|
1377
|
+
catch (err) {
|
|
1378
|
+
if (blobMode === "required")
|
|
1379
|
+
throw err;
|
|
1380
|
+
console.warn("[TomorrowOS] Vercel Blob upload failed; falling back:", err);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1345
1384
|
return this.persistRelativeUpload(body, sha256, safeName, mimeType, existing, resourceType);
|
|
1346
1385
|
}
|
|
1347
1386
|
getAssetIdsFromItems(items) {
|
|
@@ -1665,7 +1704,8 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1665
1704
|
status: "success",
|
|
1666
1705
|
chunked: Boolean(this.staticRoot),
|
|
1667
1706
|
chunkSize: MEDIA_UPLOAD_CHUNK_BYTES,
|
|
1668
|
-
cloudinary: Boolean(resolveCloudinaryConfig())
|
|
1707
|
+
cloudinary: Boolean(resolveCloudinaryConfig()),
|
|
1708
|
+
vercelBlob: isVercelBlobConfigured()
|
|
1669
1709
|
});
|
|
1670
1710
|
return;
|
|
1671
1711
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Blob media storage via `@vercel/blob`.
|
|
3
|
+
* Auto-detects `BLOB_READ_WRITE_TOKEN`; `TOMORROWOS_MEDIA=vercel-blob|blob` makes it required.
|
|
4
|
+
*/
|
|
5
|
+
export type VercelBlobMode = "off" | "preferred" | "required";
|
|
6
|
+
export interface VercelBlobUploadResult {
|
|
7
|
+
url: string;
|
|
8
|
+
pathname: string;
|
|
9
|
+
contentType?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function isVercelHost(env?: NodeJS.ProcessEnv): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* - `TOMORROWOS_MEDIA=vercel-blob|blob` → required
|
|
14
|
+
* - `BLOB_READ_WRITE_TOKEN` present (and media not forced elsewhere) → preferred
|
|
15
|
+
* - else → off
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveVercelBlobMode(env?: NodeJS.ProcessEnv): VercelBlobMode;
|
|
18
|
+
export declare function resolveVercelBlobToken(env?: NodeJS.ProcessEnv): string | null;
|
|
19
|
+
export declare function isVercelBlobConfigured(env?: NodeJS.ProcessEnv): boolean;
|
|
20
|
+
export declare function uploadBufferToVercelBlob(body: Buffer, options: {
|
|
21
|
+
pathname: string;
|
|
22
|
+
token: string;
|
|
23
|
+
contentType?: string;
|
|
24
|
+
addRandomSuffix?: boolean;
|
|
25
|
+
}): Promise<VercelBlobUploadResult>;
|
|
26
|
+
/** Lightweight token / reachability check for CMS status UI. */
|
|
27
|
+
export declare function pingVercelBlob(token: string): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=vercel-blob-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel-blob-storage.d.ts","sourceRoot":"","sources":["../src/vercel-blob-storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,cAAc,CAqBhB;AAED,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,IAAI,CAEf;AAED,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAIT;AAUD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GACA,OAAO,CAAC,sBAAsB,CAAC,CAejC;AAED,gEAAgE;AAChE,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Blob media storage via `@vercel/blob`.
|
|
3
|
+
* Auto-detects `BLOB_READ_WRITE_TOKEN`; `TOMORROWOS_MEDIA=vercel-blob|blob` makes it required.
|
|
4
|
+
*/
|
|
5
|
+
function cleanOptional(value) {
|
|
6
|
+
const trimmed = String(value || "").trim();
|
|
7
|
+
return trimmed || undefined;
|
|
8
|
+
}
|
|
9
|
+
export function isVercelHost(env = process.env) {
|
|
10
|
+
return !!(env.VERCEL || env.VERCEL_ENV || env.VERCEL_URL);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* - `TOMORROWOS_MEDIA=vercel-blob|blob` → required
|
|
14
|
+
* - `BLOB_READ_WRITE_TOKEN` present (and media not forced elsewhere) → preferred
|
|
15
|
+
* - else → off
|
|
16
|
+
*/
|
|
17
|
+
export function resolveVercelBlobMode(env = process.env) {
|
|
18
|
+
const media = cleanOptional(env.TOMORROWOS_MEDIA)?.toLowerCase();
|
|
19
|
+
if (media === "local" ||
|
|
20
|
+
media === "disk" ||
|
|
21
|
+
media === "filesystem" ||
|
|
22
|
+
media === "cloudinary" ||
|
|
23
|
+
media === "replit" ||
|
|
24
|
+
media === "replit-object-storage" ||
|
|
25
|
+
media === "object-storage" ||
|
|
26
|
+
media === "app-storage") {
|
|
27
|
+
return "off";
|
|
28
|
+
}
|
|
29
|
+
if (media === "vercel-blob" || media === "blob") {
|
|
30
|
+
return "required";
|
|
31
|
+
}
|
|
32
|
+
if (cleanOptional(env.BLOB_READ_WRITE_TOKEN)) {
|
|
33
|
+
return "preferred";
|
|
34
|
+
}
|
|
35
|
+
return "off";
|
|
36
|
+
}
|
|
37
|
+
export function resolveVercelBlobToken(env = process.env) {
|
|
38
|
+
return cleanOptional(env.BLOB_READ_WRITE_TOKEN) || null;
|
|
39
|
+
}
|
|
40
|
+
export function isVercelBlobConfigured(env = process.env) {
|
|
41
|
+
const mode = resolveVercelBlobMode(env);
|
|
42
|
+
if (mode === "off")
|
|
43
|
+
return false;
|
|
44
|
+
return !!resolveVercelBlobToken(env);
|
|
45
|
+
}
|
|
46
|
+
async function loadBlobApi() {
|
|
47
|
+
const mod = await import("@vercel/blob");
|
|
48
|
+
return { put: mod.put, list: mod.list };
|
|
49
|
+
}
|
|
50
|
+
export async function uploadBufferToVercelBlob(body, options) {
|
|
51
|
+
const { put } = await loadBlobApi();
|
|
52
|
+
const pathname = String(options.pathname || "upload").replace(/^\/+/, "");
|
|
53
|
+
const result = await put(pathname, body, {
|
|
54
|
+
access: "public",
|
|
55
|
+
token: options.token,
|
|
56
|
+
contentType: options.contentType,
|
|
57
|
+
addRandomSuffix: options.addRandomSuffix ?? false,
|
|
58
|
+
multipart: body.length > 4 * 1024 * 1024
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
url: result.url,
|
|
62
|
+
pathname: result.pathname,
|
|
63
|
+
contentType: result.contentType
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Lightweight token / reachability check for CMS status UI. */
|
|
67
|
+
export async function pingVercelBlob(token) {
|
|
68
|
+
const { list } = await loadBlobApi();
|
|
69
|
+
await list({ limit: 1, token });
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.69",
|
|
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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@replit/object-storage": "^1.0.0",
|
|
39
|
+
"@vercel/blob": "^2.6.1",
|
|
39
40
|
"better-sqlite3": "^12.11.1",
|
|
40
41
|
"cloudinary": "^2.10.0",
|
|
41
42
|
"fflate": "^0.8.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.69",
|
|
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.69",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|