@tomorrowos/sdk 0.9.66 → 0.9.68
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 +4 -3
- package/VERCEL_QUESTIONS.md +15 -7
- package/VERCEL_SETUP.md +38 -34
- 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/README.md
CHANGED
|
@@ -79,7 +79,7 @@ Follow @tomorrowos/sdk (an NPM package) VERCEL_SETUP.md and set up my TomorrowOS
|
|
|
79
79
|
|
|
80
80
|
Open VERCEL_QUESTIONS.md first.
|
|
81
81
|
For Q1, Q2, Q3: paste each question block VERBATIM. Do not rephrase options.
|
|
82
|
-
For Q3: DO NOT ask a branding
|
|
82
|
+
For Q3: DO NOT ask a branding choice. Open one form with ALL SEVEN optional inputs at once, then HARD STOP and WAIT for my submit or "skip". Never auto-continue with defaults without my reply. Never only Name + Primary colour. Logo = Upload above Logo URL.
|
|
83
83
|
After Q1 database choice, YOU set TOMORROWOS_STORE and DATABASE_SSL. Do not ask me to fill those.
|
|
84
84
|
After Q2 media storage choice, YOU set TOMORROWOS_MEDIA. Do not ask me to fill those.
|
|
85
85
|
Run: npx @tomorrowos/sdk@latest init . --hosting v0 --force
|
|
@@ -94,7 +94,7 @@ after deploy verify: GET /status → JSON, GET / → Control Panel, WebSocket on
|
|
|
94
94
|
pair devices against the Production URL only
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Agents must paste questions from [`VERCEL_QUESTIONS.md`](./VERCEL_QUESTIONS.md) verbatim. Q1/Q2 are choice questions; **Q3 is a form only** — open **all seven** optional inputs at once (never a branding choice first; never only Name + Primary colour). Logo = Upload above Logo URL
|
|
97
|
+
Agents must paste questions from [`VERCEL_QUESTIONS.md`](./VERCEL_QUESTIONS.md) verbatim. Q1/Q2 are choice questions; **Q3 is a form only** — open **all seven** optional inputs at once, then **wait** for submit or `"skip"` (never auto-skip because fields are optional; never a branding choice first; never only Name + Primary colour). Logo = Upload above Logo URL. WebSockets require Fluid compute. The agent sets `cms.hostingTarget` to `"vercel"`.
|
|
98
98
|
|
|
99
99
|
Upgrading an existing Vercel / v0 CMS instead:
|
|
100
100
|
|
|
@@ -334,6 +334,7 @@ Works in either direction across `sqlite`, `postgres`, and `supabase`. It moves
|
|
|
334
334
|
- **Local disk:** small files may use `POST /media/upload`. Larger files use **chunked upload** (`/media/upload-init` → `/media/upload-chunk` → `/media/upload-complete`).
|
|
335
335
|
- **Replit Object Storage:** set `TOMORROWOS_MEDIA=replit-object-storage` (auto-preferred on Replit when Cloudinary is unset). Uses `@replit/object-storage`; URLs stay `/uploads/...` and are served from the bucket after Republish. Same upload HTTP routes as local.
|
|
336
336
|
- **Cloudinary:** set `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET` (optional `CLOUDINARY_FOLDER`). The Control Panel uses **browser → Cloudinary** direct upload (`/media/upload-sign` + `/media/register`) when Cloudinary is configured.
|
|
337
|
+
- **Vercel Blob:** set `BLOB_READ_WRITE_TOKEN` and `TOMORROWOS_MEDIA=vercel-blob` (token alone also auto-enables Blob when Cloudinary is unset). SDK uploads via `@vercel/blob` `put()`; asset URLs are absolute `https://*.public.blob.vercel-storage.com/...`.
|
|
337
338
|
|
|
338
339
|
Cloudinary account plans still enforce their own max file sizes (for example Free plan video caps).
|
|
339
340
|
|
|
@@ -392,7 +393,7 @@ Fetch `GET /brand.json` from the panel’s network. A live WebSocket with a blac
|
|
|
392
393
|
Policy URLs must be absolute and reachable from the screen. Relative paths or hosts only visible on your laptop will fail on the panel.
|
|
393
394
|
|
|
394
395
|
**Upload fails with HTTP 413 on Replit (or similar hosts).**
|
|
395
|
-
Use `@tomorrowos/sdk` Control Panel / templates: non-Cloudinary uploads over ~2MB go through **chunked** `/media/upload-chunk`. On Replit, set `TOMORROWOS_MEDIA=replit-object-storage` (or rely on auto-detect) so files land in App Storage. With Cloudinary configured, the panel uses browser direct upload instead.
|
|
396
|
+
Use `@tomorrowos/sdk` Control Panel / templates: non-Cloudinary uploads over ~2MB go through **chunked** `/media/upload-chunk`. On Replit, set `TOMORROWOS_MEDIA=replit-object-storage` (or rely on auto-detect) so files land in App Storage. On Vercel Blob, set `TOMORROWOS_MEDIA=vercel-blob` + `BLOB_READ_WRITE_TOKEN` so proxy/chunked complete stores absolute Blob URLs. With Cloudinary configured, the panel uses browser direct upload instead.
|
|
396
397
|
|
|
397
398
|
**BrightSign zip has empty `cmsEndpoint`.**
|
|
398
399
|
You downloaded the mother zip from a static CDN instead of **this** CMS’s `/players/brightsign.zip`. Use Control Panel → Download Players → BrightSign on the deployed CMS.
|
package/VERCEL_QUESTIONS.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Vercel / v0 - Verbatim question bank
|
|
2
2
|
|
|
3
|
-
> **Protocol:** `vercel-questions/1.
|
|
3
|
+
> **Protocol:** `vercel-questions/1.5` (pairs with `VERCEL_SETUP.md` 1.14+ / `@tomorrowos/sdk` 0.9.68+)
|
|
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.
|
|
7
|
-
> **Q1 and Q2 are choice questions. Q3 is a form only - never a multiple-choice question.**
|
|
7
|
+
> **Q1 and Q2 are choice questions. Q3 is a form only - never a multiple-choice question.**
|
|
8
|
+
> **HARD STOP on Q3:** show the form, then **wait** for the user's reply. Optional fields do **not** mean you may auto-skip.
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
@@ -45,17 +46,21 @@ Reply with **1** or **2** (or "Cloudinary" / "Vercel Blob").
|
|
|
45
46
|
|
|
46
47
|
---
|
|
47
48
|
|
|
48
|
-
## Q3 - Brand form (NOT a question - open this form
|
|
49
|
+
## Q3 - Brand form (NOT a question - open this form, then WAIT)
|
|
49
50
|
|
|
50
51
|
**STOP - read before doing anything for branding:**
|
|
51
52
|
|
|
52
|
-
1. Q3 is **not** a chat
|
|
53
|
+
1. Q3 is **not** a chat multiple-choice step.
|
|
53
54
|
2. **Do not** ask: "Want to customize branding?", "Option A / Option B", "skip or customize?", or any other choice first.
|
|
54
55
|
3. **Do not** open a form with only Product Name + Primary colour (or any subset).
|
|
55
|
-
4. **Immediately** open **one** multi-input form with **all seven fields visible at the same time**.
|
|
56
|
+
4. **Immediately** open **one** multi-input form with **all seven fields visible at the same time**.
|
|
57
|
+
5. **HARD STOP and WAIT** for the user to submit the form, reply with values, or say **"skip"**.
|
|
58
|
+
- Fields being optional means blanks keep defaults **after** the user responds.
|
|
59
|
+
- Fields being optional does **NOT** mean you may continue scaffolding without a user reply.
|
|
60
|
+
- **Forbidden:** "fields are optional so I'll proceed with defaults" / auto-skip / thinking past Q3 in the same turn.
|
|
56
61
|
|
|
57
62
|
**Title:** Brand your TomorrowOS experience
|
|
58
|
-
**Helper:** All fields optional. Leave blank to keep starter brand.json defaults. Submit once.
|
|
63
|
+
**Helper:** All fields optional. Leave blank to keep starter brand.json defaults. Submit once (or reply "skip"). I will wait for your reply before continuing setup.
|
|
59
64
|
|
|
60
65
|
**Create exactly these seven controls - all on screen together (no Next / no wizard steps):**
|
|
61
66
|
|
|
@@ -71,12 +76,15 @@ Reply with **1** or **2** (or "Cloudinary" / "Vercel Blob").
|
|
|
71
76
|
|
|
72
77
|
**Pass / fail check before showing the user:** count the text inputs. If you see fewer than **six** text fields (Product Name, Tagline, Primary, Background, Text, Secondary) plus the Logo Upload+URL row, **do not show it** - rebuild the form with all seven.
|
|
73
78
|
|
|
74
|
-
**Forbidden Q3 UIs:**
|
|
79
|
+
**Forbidden Q3 UIs / behaviors:**
|
|
75
80
|
|
|
76
81
|
- Any multiple-choice / radio / "1 or 2" / Option A vs B
|
|
77
82
|
- A preliminary question before the form
|
|
78
83
|
- Only Name + Primary colour (or any 1-2 field subset)
|
|
79
84
|
- One freeform / Other box for all brand values
|
|
80
85
|
- Field-by-field Next wizard
|
|
86
|
+
- Auto-continuing to init/scaffold/deploy without a user Q3 reply or explicit "skip"
|
|
87
|
+
- Interpreting "do not re-ask" as "do not wait for the first reply"
|
|
81
88
|
|
|
89
|
+
After the user submits (including all-blank) or says "skip": apply `brand.json`, then continue. **Do not** re-ask Q3 a second time.
|
|
82
90
|
Blank fields -> keep existing `brand.json`. Logo blank -> `./assets/logo.svg`.
|
package/VERCEL_SETUP.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
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.68+** (`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
|
-
> **Q1/Q2 = choice questions. Q3 = form only:**
|
|
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**.
|
|
12
12
|
> **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).
|
|
13
13
|
> **`TOMORROWOS_STORE` and `DATABASE_SSL` are agent-only** - set them automatically after Q1; never put them in a user-facing Env form for the customer to type.
|
|
14
14
|
|
|
@@ -38,7 +38,7 @@ Follow @tomorrowos/sdk (an NPM package) VERCEL_SETUP.md and set up my TomorrowOS
|
|
|
38
38
|
|
|
39
39
|
Open VERCEL_QUESTIONS.md first.
|
|
40
40
|
For Q1, Q2, Q3: paste each question block VERBATIM. Do not rephrase options.
|
|
41
|
-
For Q3: DO NOT ask a branding
|
|
41
|
+
For Q3: DO NOT ask a branding choice. Open one form with ALL SEVEN optional inputs at once, then HARD STOP and WAIT for my submit or "skip". Never auto-continue with defaults without my reply. Never only Name + Primary colour. Logo = Upload above Logo URL.
|
|
42
42
|
After Q1 database choice, YOU set TOMORROWOS_STORE and DATABASE_SSL. Do not ask me to fill those.
|
|
43
43
|
After Q2 media storage choice, YOU set TOMORROWOS_MEDIA. Do not ask me to fill those.
|
|
44
44
|
Run: npx @tomorrowos/sdk@latest init . --hosting v0 --force
|
|
@@ -219,9 +219,9 @@ Then ask **Question 1**.
|
|
|
219
219
|
16. **No inventing CMS login** from a branding URL.
|
|
220
220
|
17. **Skip ??invent.** Stay on Q1?Q3 only.
|
|
221
221
|
18. **Publish gate:** `/status` JSON + WebSocket 101; no static `public/` trap.
|
|
222
|
-
19. **Q3 is a form, not a question.** After Q2,
|
|
222
|
+
19. **Q3 is a form, not a question ? and you must WAIT.** After Q2, open one dialog with **all seven** optional inputs visible together, then **stop and wait** for submit or explicit **"skip"**. **Never** auto-continue because fields are optional. **Never** ask a preliminary branding choice. **Never** show only Product Name + Primary colour. **Never** use multiple-choice / Option A-B for branding. Logo = **Upload** above **Logo URL**.
|
|
223
223
|
|
|
224
|
-
**Question order:** Q1 -> (auto-set store env) -> Q2 -> Q3 form -> execution checklist.
|
|
224
|
+
**Question order:** Q1 -> (auto-set store env) -> Q2 -> Q3 form (**wait for user**) -> execution checklist.
|
|
225
225
|
|
|
226
226
|
---
|
|
227
227
|
|
|
@@ -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.68+` (`@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,45 +735,41 @@ 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.68+` 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
|
-
## Question 3 - Brand form (`brand.json` only; **not a choice question**)
|
|
747
|
+
## Question 3 - Brand form (`brand.json` only; **not a choice question**; **must wait**)
|
|
756
748
|
|
|
757
749
|
> Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, `vercel.json`, or `server.ts` store wiring beyond what Q1-Q2 already required.
|
|
758
750
|
>
|
|
759
|
-
> **IRON RULE - form, not question:** As soon as Q2 is done, **open the brand form**. Do **not** ask
|
|
751
|
+
> **IRON RULE - form, not question; show then WAIT:** As soon as Q2 is done, **open the brand form**. Do **not** ask a choice first. Show **all seven optional fields on one screen at the same time**. Then **HARD STOP** until the user submits the form, replies with values, or says **"skip"**.
|
|
752
|
+
>
|
|
753
|
+
> **Optional fields ? optional step.** Leaving a field blank keeps the starter default **after the user responds**. It does **not** authorize you to skip waiting or auto-apply defaults in the same turn.
|
|
760
754
|
>
|
|
761
755
|
> - **Forbidden:** any preliminary question ("customize branding?", "skip or set brand?", Option A/B)
|
|
762
756
|
> - **Forbidden:** multiple-choice / radio / "reply 1 or 2" for branding
|
|
763
757
|
> - **Forbidden:** showing only Product Name + Primary colour (or any subset under 7)
|
|
764
758
|
> - **Forbidden:** field-by-field Next wizard (name -> wait -> tagline -> ...)
|
|
765
759
|
> - **Forbidden:** one freeform / Other box for all brand values
|
|
766
|
-
> - **
|
|
760
|
+
> - **Forbidden:** auto-continuing to init / scaffold / env / deploy because "fields are optional" or "I shouldn't re-ask"
|
|
761
|
+
> - **Required:** six text inputs + Logo Upload above Logo URL, all visible together
|
|
762
|
+
> - **Required:** wait for one user action (submit / values / "skip") before scaffolding continues
|
|
767
763
|
>
|
|
768
764
|
> **Self-check:** If the UI you are about to show has fewer than six text fields for name/tagline/four colours, **abort and rebuild** - that UI is off-protocol.
|
|
769
765
|
>
|
|
770
|
-
> **Every field is optional
|
|
766
|
+
> **Every field is optional** once the form is shown. After the user submits (including all-blank) or says "skip": keep starter **`brand.json`** for blanks; **do not** re-ask Q3 a second time. **Do not** invent a second branding prompt.
|
|
771
767
|
>
|
|
772
768
|
> Default TomorrowOS Control Panel has **no login**. Do **not** scaffold login/auth unless the user explicitly asks.
|
|
773
769
|
|
|
774
|
-
### Preferred UI (open immediately - all seven at once)
|
|
770
|
+
### Preferred UI (open immediately - all seven at once - then wait)
|
|
775
771
|
|
|
776
|
-
Open **one** dialog titled **Brand your TomorrowOS experience**. Render **each row as its own control**. All rows must appear **together** on first paint - not revealed after a choice, not after filling a previous field.
|
|
772
|
+
Open **one** dialog titled **Brand your TomorrowOS experience**. Render **each row as its own control**. All rows must appear **together** on first paint - not revealed after a choice, not after filling a previous field. After opening, **end your turn** and wait.
|
|
777
773
|
|
|
778
774
|
| # | Exact label | Control type | Blank default |
|
|
779
775
|
|---|-------------|--------------|---------------|
|
|
@@ -785,15 +781,15 @@ Open **one** dialog titled **Brand your TomorrowOS experience**. Render **each r
|
|
|
785
781
|
| 6 | **Secondary colour** | text input (hex) | e.g. `#F5F3EF` |
|
|
786
782
|
| 7 | **Logo** | **Upload** (SVG/PNG file picker) **above** **Logo URL** (text input) | keep `./assets/logo.svg` |
|
|
787
783
|
|
|
788
|
-
Helper text: *All fields optional. Leave blank to keep starter brand.json defaults. Submit once
|
|
784
|
+
Helper text: *All fields optional. Leave blank to keep starter brand.json defaults. Submit once, or reply "skip". I will wait before continuing setup.*
|
|
789
785
|
|
|
790
786
|
**Logo UX:** Upload control directly above Logo URL. Either is fine; if both provided, prefer the uploaded file.
|
|
791
787
|
|
|
792
|
-
### If a form UI is unavailable - one chat message only (still
|
|
788
|
+
### If a form UI is unavailable - one chat message only (still wait)
|
|
793
789
|
|
|
794
|
-
Only when the platform **cannot** render multi-field forms. Do **not** ask a choice first. Send **exactly one** message listing all seven fields (user may answer partially, attach a logo, or say "skip"):
|
|
790
|
+
Only when the platform **cannot** render multi-field forms. Do **not** ask a choice first. Send **exactly one** message listing all seven fields, then **end your turn and wait** (user may answer partially, attach a logo, or say "skip"):
|
|
795
791
|
|
|
796
|
-
> Brand your TomorrowOS experience (**all optional - reply once**). Leave any field blank to keep starter `brand.json` defaults:
|
|
792
|
+
> Brand your TomorrowOS experience (**all optional - reply once**). Leave any field blank to keep starter `brand.json` defaults. I will wait for your reply before scaffolding:
|
|
797
793
|
>
|
|
798
794
|
> 1. **Product Name**
|
|
799
795
|
> 2. **Tagline**
|
|
@@ -802,6 +798,8 @@ Only when the platform **cannot** render multi-field forms. Do **not** ask a cho
|
|
|
802
798
|
> 5. **Text colour** (hex)
|
|
803
799
|
> 6. **Secondary colour** (hex)
|
|
804
800
|
> 7. **Logo** - attach SVG/PNG or paste a Logo URL
|
|
801
|
+
>
|
|
802
|
+
> Or reply **skip** to keep all defaults.
|
|
805
803
|
|
|
806
804
|
### After the user submits
|
|
807
805
|
|
|
@@ -868,7 +866,7 @@ Then (**Scheme A ??mandatory for Publish**):
|
|
|
868
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.
|
|
869
867
|
5. Do **not** add root `api/` catch-all for raw `http.Server`.
|
|
870
868
|
6. Do **not** add `.replit*`.
|
|
871
|
-
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).
|
|
872
870
|
|
|
873
871
|
### B. Environment Variables (Vercel dashboard + local)
|
|
874
872
|
|
|
@@ -880,6 +878,7 @@ Then (**Scheme A ??mandatory for Publish**):
|
|
|
880
878
|
| `DATABASE_SSL` | Yes for Postgres | **Agent auto-sets only** ??never a blank field for the user (`true`) |
|
|
881
879
|
| `CLOUDINARY_*` | If Q2 = Cloudinary | User via **one Env popup** (all fields) |
|
|
882
880
|
| `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | User / Blob link |
|
|
881
|
+
| `TOMORROWOS_MEDIA` | If Q2 = Vercel Blob | **Agent auto-sets** `vercel-blob` |
|
|
883
882
|
| `TOMORROWOS_INTERNAL_PORT` | Preview only | Agent (e.g. `3001`) |
|
|
884
883
|
| `PORT` | Optional | Vercel injects for Production |
|
|
885
884
|
|
|
@@ -965,6 +964,7 @@ Tell the user:
|
|
|
965
964
|
| Agent asked for OpenAI / `sk-` key during setup | Hallucinated ?AI pairing??requirement | **Refuse.** TomorrowOS does not need LLM keys. Return to Q2 media choices only |
|
|
966
965
|
| Agent asked a branding choice / Option A-B / "customize?" before the form | Off-protocol Q3 | Close it; immediately open the **7-field form** (no choice step) |
|
|
967
966
|
| Agent showed only Name + Primary colour (or any subset) | Off-protocol Q3 | Rebuild with **all seven** inputs visible at once |
|
|
967
|
+
| Agent auto-continued after showing Q3 because "fields are optional" / "shouldn't re-ask" | Off-protocol Q3 | **HARD STOP** and wait for submit or explicit "skip"; do not scaffold yet |
|
|
968
968
|
| Agent asked branding as Option A URL vs Option B, or field-by-field Next | Off-protocol Q3 | Re-open Q3 as **seven separate optional inputs** in one dialog; blanks keep `brand.json` |
|
|
969
969
|
| Agent built a login page from branding answers | Misread brand step as product scope | Remove login; keep starter Control Panel; apply answers only to `brand.json` |
|
|
970
970
|
| Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
|
|
@@ -1013,7 +1013,11 @@ Protocol complete when:
|
|
|
1013
1013
|
|
|
1014
1014
|
## Protocol version
|
|
1015
1015
|
|
|
1016
|
-
`vercel-setup/1.
|
|
1016
|
+
`vercel-setup/1.14` - pairs with `@tomorrowos/sdk` 0.9.68+ 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.
|
|
1019
|
+
|
|
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".
|
|
1017
1021
|
|
|
1018
1022
|
**Changelog 1.12:** Q3 is a **form, not a question**. No preliminary branding choice. Must show **all seven** optional inputs at once (never only Name + Primary colour). Logo = Upload above Logo URL.
|
|
1019
1023
|
|
|
@@ -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.68",
|
|
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.68",
|
|
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.68",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.68",
|
|
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.68",
|
|
21
21
|
"dotenv": "^17.2.3",
|
|
22
22
|
"tsx": "^4.19.0"
|
|
23
23
|
},
|