@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 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 question or offer choices. Immediately open one form with ALL SEVEN optional inputs visible at once (Product Name, Tagline, Primary/Background/Text/Secondary colour, Logo Upload above Logo URL). Never only Name + Primary colour. Blanks = brand.json defaults.
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; blanks keep `brand.json` defaults. WebSockets require Fluid compute. The agent sets `cms.hostingTarget` to `"vercel"`.
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.
@@ -1,10 +1,11 @@
1
1
  # Vercel / v0 - Verbatim question bank
2
2
 
3
- > **Protocol:** `vercel-questions/1.4` (pairs with `VERCEL_SETUP.md` 1.12+ / `@tomorrowos/sdk` 0.9.66+)
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 immediately)
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 question and **not** a multiple-choice step.
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**. User submits **once**. Every field is **optional**.
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.12` - pairs with `@tomorrowos/sdk` **0.9.66+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
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:** immediately open **all seven optional inputs at once**. Never ask a branding choice first. Never show only Name + Primary colour. Logo = **Upload** above **Logo URL**. Blanks keep starter `brand.json`.
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 question or offer choices. Immediately open one form with ALL SEVEN optional inputs visible at once (Product Name, Tagline, Primary/Background/Text/Secondary colour, Logo Upload above Logo URL). Never only Name + Primary colour. Blanks = brand.json defaults.
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, **immediately** open one dialog with **all seven** optional inputs visible together. **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**.
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` only when the user chooses **Vercel Blob** in Question 2.
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 ??If **2 / Vercel Blob** (same Question 2)
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 ??Create ??Blob** (or link an existing Blob store to this project).
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. `npm install @vercel/blob` (add to `dependencies`).
739
- 3. Wire uploads so media returns **absolute HTTPS Blob URLs** stored in `uploaded_assets` (players need stable public URLs).
740
- - The SDK **natively auto-detects Cloudinary** today. For Vercel Blob, add a thin upload bridge in the CMS project (e.g. custom route or middleware that calls `put()` from `@vercel/blob` and persists the returned `url` the same way Cloudinary URLs are stored).
741
- - Minimum pattern:
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 the user anything in chat first. Do **not** offer choices. Show **all seven optional fields on one screen at the same time**. User fills what they want (or leaves blank) and submits **once**.
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
- > - **Required:** six text inputs + Logo Upload above Logo URL, all visible together before submit
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.** Blank / skip / "use defaults" -> keep starter **`brand.json`** values (from `templates/cms-starter-v0/brand.json` / project `brand.json` after `init`). **Do not** re-ask for missing fields. **Do not** invent a second branding prompt.
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. For logo: upload a file or paste a URL.*
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 no choice question)
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: install `@vercel/blob` and wire the upload bridge.
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.12` - pairs with `@tomorrowos/sdk` 0.9.66+ and `VERCEL_QUESTIONS.md` 1.4.
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":"AAaA,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;AAqUD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA6B7B"}
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"}
@@ -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 hasUrl = !!(env.SUPABASE_URL || env.DATABASE_URL);
19
- if (driver === "supabase" || (!driver && env.SUPABASE_URL)) {
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: "supabase",
35
+ provider: "Supabase",
22
36
  label: "Database",
23
- configured: !!env.SUPABASE_URL || !!env.DATABASE_URL
37
+ configured: hasUrl,
38
+ kind: "supabase"
24
39
  };
25
40
  }
26
- if (driver === "postgres" || (!driver && hasUrl && !env.SUPABASE_URL)) {
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: "postgres",
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 { provider: "memory", label: "Database", configured: true };
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 { provider: "sqlite", label: "Database", configured: true };
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: driver || "unknown",
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.provider === "supabase" || meta.provider === "postgres";
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.provider === "memory") {
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.provider === "sqlite" && isReplitHost(env)) {
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.label} reachable`
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: "cloudinary",
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: "cloudinary",
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: "cloudinary",
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: "replit-object-storage",
247
+ provider: "Replit Object Storage",
168
248
  state: "ok",
169
- detail: "Replit Object Storage (App Storage) reachable — uploads persist across Republish"
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: "replit-object-storage",
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: "replit-object-storage",
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 provider = isReplitHost(env) ? "replit-object-storage" : "local";
197
- const providerLabel = provider === "replit-object-storage"
198
- ? "Local uploads (Object Storage not active — files may not survive Republish)"
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 Cloudinary and staticRoot is unset — media uploads are unavailable."
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} writable at ${uploadsDir}. Set TOMORROWOS_MEDIA=replit-object-storage and link App Storage for durable media, or use Cloudinary.`
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 Supabase."
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: c.provider === "cloudinary"
332
+ fixHint: provider === "Cloudinary"
251
333
  ? "Fix Cloudinary Secrets (CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET) and restart."
252
- : c.provider === "replit-object-storage"
253
- ? "Link a Replit App Storage bucket, set TOMORROWOS_MEDIA=replit-object-storage, ensure @replit/object-storage is installed, then restart."
254
- : "Enable Object Storage for public/uploads, or configure Cloudinary for durable media URLs."
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 Secrets, or ensure the CMS listens with staticRoot pointing at public/ (uploads folder)."
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: "tomorrowos",
367
+ provider: "TomorrowOS",
284
368
  state: "ok",
285
369
  detail: "CMS process is running"
286
370
  };
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
@@ -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.66",
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.66",
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.66",
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.66",
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.66",
20
+ "@tomorrowos/sdk": "^0.9.68",
21
21
  "dotenv": "^17.2.3",
22
22
  "tsx": "^4.19.0"
23
23
  },