diff --git a/.env.example b/.env.example deleted file mode 100644 index 9391b37..0000000 --- a/.env.example +++ /dev/null @@ -1,56 +0,0 @@ -# BeatDesign -# Copy to .env.development for local use. - -VITE_APP_URL=http://localhost:3020 -VITE_APP_NAME=BeatDesign -VITE_APP_DESCRIPTION=The open-source, local-first AI canvas for image and video creation. -VITE_APP_LOGO=/logo.png -VITE_DEFAULT_LOCALE=en -GENERATION_PROVIDER=beatapi - -# Local persistence -DATABASE_PROVIDER=sqlite -DATABASE_URL=file:data/workspace.db - -# BeatAPI provider. The official endpoint is fixed; the key stays server-side -# and must never use a VITE_ prefix. -BEATAPI_API_BASE_URL=https://api.beatapi.io -BEATAPI_API_KEY= - -# Optional override for provider-secret encryption. Local SQLite installs -# automatically create data/.workspace-key; hosted deployments must set this. -CONFIG_ENCRYPTION_KEY= - -# Generation recovery -EFFECTS_POLL_INTERVAL_MS=20000 -EFFECTS_GENERATION_TIMEOUT_MS=1800000 - -# Storage mode: beatapi (official managed Files/R2) or s3 (your own bucket). -# Files stay local until a generation precheck succeeds. beatapi then sends -# supported references to BeatAPI managed storage; an official deployment may -# use its managed R2 fallback for video. s3 sends those generation references -# to the operator's R2/S3-compatible bucket instead. -WORKSPACE_STORAGE_MODE=beatapi - -# Official hosted deployments only: BeatAPI-managed fallback for generation -# input types that /v1/files does not accept. -BEATAPI_MANAGED_R2_REGION=auto -BEATAPI_MANAGED_R2_ENDPOINT= -BEATAPI_MANAGED_R2_ACCESS_KEY_ID= -BEATAPI_MANAGED_R2_SECRET_ACCESS_KEY= -BEATAPI_MANAGED_R2_BUCKET_NAME= -BEATAPI_MANAGED_R2_PUBLIC_URL= -BEATAPI_MANAGED_R2_FORCE_PATH_STYLE=true - -# Self-hoster-owned R2/S3, used only when WORKSPACE_STORAGE_MODE=s3. -R2_REGION=auto -R2_ENDPOINT= -R2_ACCESS_KEY_ID= -R2_SECRET_ACCESS_KEY= -R2_BUCKET_NAME= -R2_PUBLIC_URL= -R2_FORCE_PATH_STYLE=true - -# Optional for operator-controlled private MinIO/S3 networks only. Keep false -# for normal Cloudflare R2 and public S3-compatible endpoints. -WORKSPACE_ALLOW_PRIVATE_STORAGE_ENDPOINTS=false diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 047251e..d0251b5 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -32,5 +32,4 @@ jobs: - run: pnpm typecheck - run: pnpm test - run: pnpm i18n:check - - run: pnpm cf:build - run: pnpm audit --prod --audit-level high diff --git a/.gitignore b/.gitignore index 8587721..b4dc8f7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,11 +16,6 @@ src/routeTree.gen.ts src/paraglide/ -# cloudflare — wrangler.jsonc is your working copy (real D1 id, URLs); -# the committed template is wrangler.example.jsonc -/.wrangler/ -wrangler.jsonc - # local agent state /.claude/ /.agents/ @@ -31,9 +26,6 @@ wrangler.jsonc # production /build -# deploy -.vercel - # editors .idea .vscode @@ -49,10 +41,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env — local files hold secrets, never commit (keep .env.example tracked) +# local secret files .env .env.* -!.env.example # typescript *.tsbuildinfo diff --git a/AGENTS.md b/AGENTS.md index 33a11a9..0632ac1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,14 +13,14 @@ Do not add authentication, accounts, payments, subscriptions, credits, API-key i - TanStack Start, React 19, TypeScript - TanStack Query - Tailwind CSS 4 and Base UI/shadcn primitives -- Drizzle ORM with SQLite locally and D1 optionally +- Drizzle ORM with local SQLite - Paraglide for English and Chinese ## Rules - Browser components call typed local API helpers; they do not import the database. - Provider keys stay server-side. -- Storage entitlement follows billing. BeatAPI managed R2/Files is allowed only with the official `https://api.beatapi.io` billing endpoint; any custom API host must use an operator-owned R2/S3-compatible bucket. File selection stays local and upload is allowed only after generation precheck. Never commit shared storage credentials. +- BeatAPI Files is the default generation-input upload path. Users may configure their own public R2/S3-compatible bucket; credentials stay encrypted in local SQLite. File selection stays local and upload is allowed only after generation precheck. Never commit shared storage credentials. - The model catalog lives in `src/core/effects/effect-registry.ts`. - BeatAPI request mapping lives in `src/core/adapters/beatapi-adapter.ts`. - Studio, Canvas, and Assets share projects, tasks, and assets. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f8903df..463bb59 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -30,9 +30,9 @@ The shared product model is asset-first: The browser calls local `/api` routes. Server routes validate input and resolve a logical model through the active Generation Provider contract. BeatAPI is the built-in/default provider; forks can register another source-level provider without changing Canvas, Editor, MCP, or the asset-first request contract. -Provider credentials are read from environment variables or the local `config` table. Browser components never receive the raw API key. +Provider and optional R2/S3 credentials are encrypted in the local `config` table. Browser components never receive raw credentials. -Upload storage is a separate adapter boundary. File selection remains browser-local. A successful generation precheck creates a short-lived, one-time SQLite intent that binds the project, model, exact upload count, uploaded URLs, and final generation submission. Required references are promoted only after that point and immediately before task submission; they become project assets only after BeatAPI accepts the task. The built-in provider is fixed to the official `https://api.beatapi.io` endpoint. Official `BEATAPI_MANAGED_R2_*` secrets and self-hosted `R2_*` credentials are deliberately separate and never fall through to each other. +Upload storage is a separate adapter boundary. File selection remains browser-local. A successful generation precheck creates a short-lived, one-time SQLite intent that binds the project, model, exact upload count, uploaded URLs, and final generation submission. Required references are promoted only after that point and immediately before task submission; they become project assets only after the provider accepts the task. The default path uploads supported references to BeatAPI Files. Users may instead configure a public R2/S3-compatible bucket; those credentials remain local and are used only for confirmed generation inputs. ## Command boundary @@ -56,7 +56,7 @@ Canvas layout persistence is the deliberate exception on the UI side: drag, resi ## Persistence -The SQLite/D1 schema contains twelve tables: +The local SQLite schema contains twelve tables: - `project` - `project_canvas_state` @@ -79,6 +79,6 @@ No user, session, role, order, subscription, payment, credit, API-key, ticket, o `src/core/effects/effect-registry.ts` is the canonical user-facing logical catalog. `src/core/generation-providers/` maps those logical IDs to provider bindings and adapters. `src/core/adapters/beatapi-adapter.ts` contains BeatAPI request mapping. Do not leak upstream effect IDs or field names into MCP tools, and do not add a second database-backed model registry. -## Deployment boundary +## Runtime boundary -Local SQLite is the default. Cloudflare D1 is supported for hosted deployments. A hosted deployment is still logically single-user; put access control at the network/platform layer if the workspace must be private. +BeatDesign is a localhost application backed by one SQLite database and project-owned files under `data/`. Cloud database and hosted deployment adapters are intentionally outside this repository. diff --git a/CHANGELOG.md b/CHANGELOG.md index 743d64e..a81fde1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to BeatDesign are documented in this file. +## [0.2.1] - 2026-08-30 + +### Changed + +- Fixed the open-source runtime to one local SQLite database and removed environment-file configuration from application startup. +- Made BeatAPI Files the default confirmed-generation upload path while retaining optional, locally encrypted public R2/S3-compatible storage. +- Kept provider selection as a source-level extension point with BeatAPI as the upstream default. + +### Removed + +- Cloudflare D1, Wrangler, Vercel, hosted deployment scripts, and obsolete SaaS-era assets that were not part of the local workbench. + ## [0.2.0] - 2026-08-30 ### Added @@ -29,4 +41,5 @@ All notable changes to BeatDesign are documented in this file. - MP4 export remains browser-driven and is not yet available as a headless MCP tool. - Captions, transitions, speed controls, multiple named timelines, and native desktop packaging remain follow-up work. +[0.2.1]: https://github.com/BeatAPI/BeatDesign/releases/tag/v0.2.1 [0.2.0]: https://github.com/BeatAPI/BeatDesign/releases/tag/v0.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1e0aa3..b1641e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,6 @@ Use Node.js 22+ and pnpm 10+. ```bash pnpm install -cp .env.example .env.development pnpm db:push pnpm typecheck pnpm test @@ -22,7 +21,7 @@ pnpm i18n:check pnpm build ``` -Add or update tests for behavior changes. Add user-facing copy to both `messages/en.json` and `messages/zh.json`. Never include API keys, local databases, generated uploads, or deployment credentials. +Add or update tests for behavior changes. Add user-facing copy to both `messages/en.json` and `messages/zh.json`. Never include API keys, local databases, generated uploads, or storage credentials. ## Pull requests diff --git a/PROVIDERS.md b/PROVIDERS.md index 246b782..008dd8e 100644 --- a/PROVIDERS.md +++ b/PROVIDERS.md @@ -2,12 +2,7 @@ BeatAPI is the built-in and default generation/analysis provider. The official BeatAPI adapter keeps its upstream URL fixed to `https://api.beatapi.io`; users only provide their own BeatAPI API key. -Configure it in either place: - -1. Set `BEATAPI_API_KEY` in the server environment. -2. Use the Provider dialog in the workspace header. The key is saved in the local `config` table and takes precedence over the environment fallback. - -Set `CONFIG_ENCRYPTION_KEY` to encrypt saved API keys at rest. Keep that key stable: changing it makes previously encrypted values unreadable. +Configure it in the Provider dialog in the workspace header. The key is encrypted in the local `config` table with a per-install key stored under `data/`. The adapter uses: @@ -23,7 +18,7 @@ Kling 2.6 and Kling 3.0 Motion Control are exposed as BeatAPI models. Each run r Video Analysis is exposed as a stable BeatAPI workflow with Standard and Deep depth controls. The Workspace uploads one MP4/MOV input, submits the analysis task, polls `GET /v1/tasks/:id`, and stores the returned report text and usage in the local project history. Provider-specific Gemini routing remains private to BeatAPI. -An API with a different request or polling contract needs its own adapter. Forks can register one in `src/config/generation-providers.ts`, bind only the logical models they support, and set server-side `GENERATION_PROVIDER=`. BeatAPI is the default only when no custom provider is selected; an unknown configured id fails explicitly so it cannot accidentally submit a task to another provider. This repository does not ship placeholder KIE, Vidu, Evolink, Gemini, Fal, Replicate, or payment-provider integrations. +An API with a different request or polling contract needs its own adapter. Forks can register one in `src/config/generation-providers.ts`, bind only the logical models they support, and change `ACTIVE_GENERATION_PROVIDER_ID` in the same file. BeatAPI remains the upstream default; an unknown configured id fails explicitly so it cannot accidentally submit a task to another provider. This repository does not ship placeholder KIE, Vidu, Evolink, Gemini, Fal, Replicate, or payment-provider integrations. A custom provider definition owns adapter construction, readiness checks, parameter validation, model bindings, upstream model names, and upload paths. Provider credentials must stay server-side. Switching providers does not change Canvas nodes, Editor clips, Asset IDs, or MCP requests; each submitted task also records its provider/model identity so polling does not silently follow a later default-provider change. @@ -31,9 +26,8 @@ A custom provider definition owns adapter construction, readiness checks, parame Storage is independently configurable from generation: -- `beatapi` uses the official `https://api.beatapi.io` endpoint with the user's BeatAPI API key. File selection stays local; after generation precheck, supported references go to `POST /v1/files` or the official deployment's managed R2 immediately before task submission. +- `beatapi` uses the official `https://api.beatapi.io` endpoint with the user's BeatAPI API key. File selection stays local; after generation precheck, supported references go to `POST /v1/files` immediately before task submission. - Precheck creates a one-time SQLite generation intent that binds project, model, upload count, uploaded URLs, and final task submission. Selecting a file alone never uploads it, and uploaded inputs are not indexed as project assets until BeatAPI accepts the task. -- Self-hosters may select `s3` to send generation references to their own Cloudflare R2 or S3-compatible bucket under the same intent rules. -- The official hosted Workspace may inject `BEATAPI_MANAGED_R2_*` deployment secrets, giving users managed video-input uploads without exposing shared credentials. User-owned storage uses only `R2_*`; the two credential sets are isolated. +- Users may select `s3` to send generation references to their own public R2/S3-compatible bucket under the same intent rules. Credentials are encrypted in local SQLite rather than read from environment files. -Remote generation providers require public HTTPS media URLs. A custom bucket therefore needs `R2_PUBLIC_URL`, normally an R2 custom domain or public bucket domain. +Remote generation providers require public HTTPS media URLs. A custom bucket therefore needs a public base URL, normally an R2 custom domain or public bucket domain. diff --git a/README.md b/README.md index 8db9c7d..7f4e587 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ Requirements: Node.js 22+, pnpm 10+, and current Chrome on macOS or Windows. ```bash pnpm install -cp .env.example .env.development pnpm db:push pnpm dev ``` @@ -117,7 +116,6 @@ See [MCP setup and tool boundaries](./docs/MCP.md). - Project-owned media under `data/project-assets//`. - Revision-aware Canvas and Editor saving. - English and Chinese UI. -- Optional Cloudflare D1 deployment target. ## Data and provider boundary @@ -133,9 +131,9 @@ Generation adapter (BeatAPI by default) Output copied back into the local Asset library ``` -API keys and storage credentials remain server-side. Selecting or dragging a local file does not send it to a provider. BeatDesign persists the file locally first and uploads only the durable project Asset required by a confirmed generation request. +API keys and optional R2/S3 credentials are encrypted in the local SQLite workspace. Selecting or dragging a local file does not send it to a provider. BeatDesign persists the file locally first and uploads only the durable project Asset required by a confirmed generation request. BeatAPI Files is the default upload path; users can select their own public R2/S3-compatible bucket in Connections. -BeatDesign does not reproduce provider billing, balance, or rate-limit logic. It returns the provider's result or error to the UI/MCP caller. The upstream repository ships the official BeatAPI adapter; a fork can implement `BaseAdapter` and register it in `src/config/generation-providers.ts`. +BeatDesign does not reproduce provider billing, balance, or rate-limit logic. It returns the provider's result or error to the UI/MCP caller. The upstream repository ships the official BeatAPI adapter; a fork can implement `BaseAdapter`, register it, and select it in `src/config/generation-providers.ts`. Read [provider architecture](./PROVIDERS.md) and [system architecture](./ARCHITECTURE.md) for the full contract. @@ -173,7 +171,7 @@ Start with [CONTRIBUTING.md](./CONTRIBUTING.md), then use: - [WORKSPACE_MODES.md](./WORKSPACE_MODES.md) for product surfaces. - [docs/MCP.md](./docs/MCP.md) for Agent integration. - [DESIGN.md](./DESIGN.md) for the BeatDesign visual language. -- [SECURITY.md](./SECURITY.md) for local and deployment safety. +- [SECURITY.md](./SECURITY.md) for local-workspace safety. ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 4c7e678..055d45f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -60,7 +60,6 @@ Editor ────────── 裁剪、切分、移动、混音、AI 重 ```bash pnpm install -cp .env.example .env.development pnpm db:push pnpm dev ``` @@ -117,7 +116,6 @@ MCP 不会模拟点击界面像素。Agent 读取 Project、提交稳定命令 - 项目媒体保存在 `data/project-assets//`。 - Canvas 与 Editor 使用 revision-aware 保存。 - 中英文界面。 -- 可选 Cloudflare D1 部署目标。 ## 数据与 Provider 边界 @@ -133,9 +131,9 @@ Generation adapter(默认 BeatAPI) 输出复制回本地 Asset 素材库 ``` -API Key 和存储凭据只留在服务端。选择或拖入本地文件不会把它发送给 Provider;BeatDesign 会先把它持久化为本地 Project Asset,只有用户确认生成后,才上传该次生成真正需要的素材。 +API Key 和可选的 R2/S3 凭据会加密保存在本地 SQLite。选择或拖入本地文件不会把它发送给 Provider;BeatDesign 会先把它持久化为本地 Project Asset,只有用户确认生成后,才上传该次生成真正需要的素材。默认使用 BeatAPI Files,用户也可以在“连接配置”中选择自己的公网 R2/S3 兼容存储桶。 -BeatDesign 不重复实现 Provider 的余额、计费或限流逻辑,只把 Provider 的结果或错误返回给 UI / MCP。上游仓库内置官方 BeatAPI 适配器;fork 可以实现 `BaseAdapter`,并在 `src/config/generation-providers.ts` 中注册。 +BeatDesign 不重复实现 Provider 的余额、计费或限流逻辑,只把 Provider 的结果或错误返回给 UI / MCP。上游仓库内置官方 BeatAPI 适配器;fork 可以实现 `BaseAdapter`,并在 `src/config/generation-providers.ts` 中注册和选择。 完整约定见 [Provider 架构](./PROVIDERS.md) 与 [系统架构](./ARCHITECTURE.md)。 @@ -173,7 +171,7 @@ BeatDesign v0.2 聚焦本地 AI 短视频工作流: - [WORKSPACE_MODES.md](./WORKSPACE_MODES.md):产品视图。 - [docs/MCP.md](./docs/MCP.md):Agent 接入。 - [DESIGN.md](./DESIGN.md):BeatDesign 视觉语言。 -- [SECURITY.md](./SECURITY.md):本地与部署安全。 +- [SECURITY.md](./SECURITY.md):本地工作区安全。 ## License diff --git a/RELEASE_SCOPE.md b/RELEASE_SCOPE.md index 5edee36..7d8d0c6 100644 --- a/RELEASE_SCOPE.md +++ b/RELEASE_SCOPE.md @@ -2,15 +2,15 @@ This repository contains BeatDesign only. The separate BeatAPI SaaS Template is not part of this codebase. -Included: homepage, projects, Studio, Canvas, provider configuration, supported model and video-analysis registry, generation/analysis lifecycle, uploads, assets, local history, i18n, SQLite/D1 persistence, tests, and deployment examples. +Included: homepage, projects, Studio, Canvas, provider and upload-storage configuration, supported model and video-analysis registry, generation/analysis lifecycle, uploads, assets, local history, i18n, SQLite persistence, tests, and localhost runtime examples. Excluded: authentication, login, accounts, payments, subscriptions, credits, API-key issuing, invitations, RBAC, admin, support tickets, CMS, email delivery, and unrelated AI-provider adapters. Release verification requires a clean install, schema creation, production build (which generates Paraglide and route types), typecheck, test, i18n check, MCP stdio handshake, and local route smoke test. A real paid BeatAPI generation is a separate credentialed end-to-end check. -## v0.2.0 release gate +## v0.2.1 release gate -- [x] Package, MCP server, and Codex plugin versions agree on `0.2.0`. +- [x] Package, MCP server, and Codex plugin versions agree on `0.2.1`. - [x] Canvas, Editor, and MCP contracts have automated coverage. - [x] MCP exposes Project, Asset, Canvas, Generation, and Editor groups without full-document replacement tools. - [x] Local media import and image clips are documented as shipped capabilities. diff --git a/SECURITY.md b/SECURITY.md index 6a4040a..30a118d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,6 +8,6 @@ Include the affected route or component, reproduction steps, impact, and any sug ## Deployment responsibility -BeatDesign is a single-user, local-first application and does not include authentication. The development server binds to localhost. Operators who expose it to a network must add access control in front of the application and provide provider and storage credentials through deployment secrets. +BeatDesign is a single-user, local-first application and does not include authentication. The development server binds to localhost and is not designed to be exposed as a hosted multi-user service. Provider and optional storage credentials are encrypted in the local SQLite workspace. The maintainers support the latest released `0.2.x` version. Security fixes are published in the next patch release when possible. diff --git a/docs/design-preview.html b/docs/design-preview.html index 6849d5a..430c111 100644 --- a/docs/design-preview.html +++ b/docs/design-preview.html @@ -30,7 +30,7 @@
Success#208A55
02 · Typography

Creative at display scale. Precise when the data starts.

Figtree Variable
From API call to finished product.

Figtree carries headlines, body copy, navigation, forms, and controls. The family has enough personality for marketing without becoming noisy inside the application.

Geist Mono
POST /v1/music/generations

{
"prompt": "soft cinematic pulse",
"duration": 32,
"webhook_url": "https://app.dev/hooks"
}

200 · task_01JAZ8…
-
03 · Components

Product primitives that look related without looking identical.

Actions & inputs

32 creditsAPI online

System feedback

Generation complete · output saved to R2
Webhook retry scheduled in 30 seconds
Payment signature could not be verified
D1 is available as an alternative database
+
03 · Components

Product primitives that look related without looking identical.

Actions & inputs

Local projectAPI online

System feedback

Generation complete · output saved locally
Provider task is still processing
Provider request could not be completed
SQLite keeps the project on this machine
04 · Shared application shell

Studio and Canvas are two views of one product.

Campaign workspace
1,240 creditsKK
01 · Source imagePNG

product-shot.png

02 · Generate video8s

Slow orbit, warm studio light

BeatAPI Design System · 2026PromptWise-inspired, owned components
diff --git a/docs/prd/VIDEO_TIMELINE_PHASE_1.md b/docs/prd/VIDEO_TIMELINE_PHASE_1.md index a77efde..c06209b 100644 --- a/docs/prd/VIDEO_TIMELINE_PHASE_1.md +++ b/docs/prd/VIDEO_TIMELINE_PHASE_1.md @@ -406,7 +406,7 @@ editor.projectChanged ### 8.4 替换 -- OpenReel IndexedDB 项目保存 -> BeatDesign Timeline service + SQLite/D1。 +- OpenReel IndexedDB 项目保存 -> BeatDesign Timeline service + local SQLite。 - OpenReel 最近项目 -> BeatDesign Project/Timeline 列表。 - OpenReel 媒体导入 -> BeatDesign Asset Adapter。 - OpenReel 下载导出 -> BeatDesign Render service;仍保留“下载文件”选项。 @@ -788,7 +788,6 @@ pnpm build ```bash pnpm install -cp .env.example .env.development pnpm db:push pnpm dev ``` diff --git a/drizzle.config.ts b/drizzle.config.ts index 6388880..9357054 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,19 +1,10 @@ import { defineConfig } from 'drizzle-kit'; -import { loadEnvFiles } from './src/lib/env'; - -loadEnvFiles(); - -const provider = process.env.DATABASE_PROVIDER || 'sqlite'; - -if (provider !== 'sqlite' && provider !== 'd1') { - throw new Error('BeatDesign supports DATABASE_PROVIDER=sqlite or d1'); -} export default defineConfig({ schema: './src/config/db/schema.ts', - out: './drizzle/d1', + out: './drizzle/sqlite', dialect: 'sqlite', dbCredentials: { - url: process.env.DATABASE_URL || 'file:data/workspace.db', + url: 'file:data/local.db', }, }); diff --git a/drizzle/d1/0000_past_mordo.sql b/drizzle/sqlite/0000_past_mordo.sql similarity index 100% rename from drizzle/d1/0000_past_mordo.sql rename to drizzle/sqlite/0000_past_mordo.sql diff --git a/drizzle/d1/0001_tiresome_xavin.sql b/drizzle/sqlite/0001_tiresome_xavin.sql similarity index 100% rename from drizzle/d1/0001_tiresome_xavin.sql rename to drizzle/sqlite/0001_tiresome_xavin.sql diff --git a/drizzle/d1/0002_lethal_micromacro.sql b/drizzle/sqlite/0002_lethal_micromacro.sql similarity index 100% rename from drizzle/d1/0002_lethal_micromacro.sql rename to drizzle/sqlite/0002_lethal_micromacro.sql diff --git a/drizzle/d1/0003_slow_the_phantom.sql b/drizzle/sqlite/0003_slow_the_phantom.sql similarity index 100% rename from drizzle/d1/0003_slow_the_phantom.sql rename to drizzle/sqlite/0003_slow_the_phantom.sql diff --git a/drizzle/d1/meta/0000_snapshot.json b/drizzle/sqlite/meta/0000_snapshot.json similarity index 100% rename from drizzle/d1/meta/0000_snapshot.json rename to drizzle/sqlite/meta/0000_snapshot.json diff --git a/drizzle/d1/meta/0001_snapshot.json b/drizzle/sqlite/meta/0001_snapshot.json similarity index 100% rename from drizzle/d1/meta/0001_snapshot.json rename to drizzle/sqlite/meta/0001_snapshot.json diff --git a/drizzle/d1/meta/0002_snapshot.json b/drizzle/sqlite/meta/0002_snapshot.json similarity index 100% rename from drizzle/d1/meta/0002_snapshot.json rename to drizzle/sqlite/meta/0002_snapshot.json diff --git a/drizzle/d1/meta/0003_snapshot.json b/drizzle/sqlite/meta/0003_snapshot.json similarity index 100% rename from drizzle/d1/meta/0003_snapshot.json rename to drizzle/sqlite/meta/0003_snapshot.json diff --git a/drizzle/d1/meta/_journal.json b/drizzle/sqlite/meta/_journal.json similarity index 100% rename from drizzle/d1/meta/_journal.json rename to drizzle/sqlite/meta/_journal.json diff --git a/integrations/codex/beatdesign/.codex-plugin/plugin.json b/integrations/codex/beatdesign/.codex-plugin/plugin.json index e7724ad..a95de69 100644 --- a/integrations/codex/beatdesign/.codex-plugin/plugin.json +++ b/integrations/codex/beatdesign/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "beatdesign", - "version": "0.2.0", + "version": "0.2.1", "description": "Control a local BeatDesign Canvas, generation workspace, and video Editor through MCP.", "author": { "name": "BeatAPI" diff --git a/messages/en.json b/messages/en.json index b81ab17..3049990 100644 --- a/messages/en.json +++ b/messages/en.json @@ -742,8 +742,6 @@ "endpointLabel": "Endpoint", "mediaLabel": "Media", "mediaValue": "Image + video", - "serverTitle": "Advanced: server environment", - "serverDescription": "You can also configure via environment variables; database values take precedence.", "keyLabel": "API Key", "keyPlaceholder": "sk_xxxxxxxxxxxx", "keyConfigured": "Configured", @@ -756,10 +754,10 @@ "cancelReplace": "Cancel", "getKey": "Get a BeatAPI API key →", "storage": { - "intro": "Hosted R2 with BeatAPI billing, or your own bucket.", - "managedTitle": "Managed R2", - "managedDescription": "Included with official BeatAPI billing", - "managedHint": "Included with official BeatAPI billing. Files upload only when you Generate.", + "intro": "Use BeatAPI Files by default, or connect your own public bucket.", + "managedTitle": "BeatAPI Files", + "managedDescription": "Default upload path for BeatAPI generation", + "managedHint": "Uses your BeatAPI connection. Files upload only after you confirm Generate.", "customTitle": "Your bucket", "customDescription": "Use your own R2 / S3-compatible bucket", "regionLabel": "Region", diff --git a/messages/zh.json b/messages/zh.json index 591bf24..a584eb6 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -743,8 +743,6 @@ "endpointLabel": "接口地址", "mediaLabel": "支持类型", "mediaValue": "图片 + 视频", - "serverTitle": "高级:服务端环境变量", - "serverDescription": "也可以用环境变量配置,数据库配置优先于环境变量。", "keyLabel": "API Key", "keyPlaceholder": "sk_xxxxxxxxxxxx", "keyConfigured": "已配置", @@ -757,10 +755,10 @@ "cancelReplace": "取消", "getKey": "获取 BeatAPI API Key →", "storage": { - "intro": "使用 BeatAPI 托管 R2,或接入你自己的存储桶。", - "managedTitle": "托管 R2", - "managedDescription": "仅随 BeatAPI 官方计费提供", - "managedHint": "随 BeatAPI 官方计费提供。确认生成后才会上传文件。", + "intro": "默认使用 BeatAPI Files,也可以接入你自己的公网存储桶。", + "managedTitle": "BeatAPI Files", + "managedDescription": "BeatAPI 生成的默认上传通道", + "managedHint": "使用你已连接的 BeatAPI。只有确认生成后才会上传文件。", "customTitle": "自己的存储", "customDescription": "使用你自己的 R2 / S3 兼容存储", "regionLabel": "区域", diff --git a/package.json b/package.json index 92336b8..c2f8556 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "beatdesign", - "version": "0.2.0", + "version": "0.2.1", "private": true, "description": "The open-source, local-first AI canvas for image and video creation and analysis", "license": "Apache-2.0", @@ -23,22 +23,17 @@ "test": "node scripts/run-tests.mjs", "typecheck": "tsc --noEmit", "i18n:check": "node scripts/check-i18n.mjs", - "media:localize": "tsx scripts/with-env.ts pnpm exec tsx scripts/localize-project-media.ts", + "media:localize": "tsx scripts/localize-project-media.ts", "mcp": "tsx scripts/mcp-server.ts", "prebuild": "node scripts/db-setup.mjs", "build": "tsx scripts/prepare-paraglide.ts && vite build && node scripts/sanitize-output.mjs", - "start": "tsx scripts/with-env.ts node .output/server/index.mjs", - "prevercel:build": "NODE_ENV=production node scripts/db-setup.mjs", - "vercel:build": "tsx scripts/prepare-paraglide.ts && NITRO_PRESET=vercel vite build && node scripts/sanitize-output.mjs", - "precf:build": "DATABASE_PROVIDER=d1 NODE_ENV=production node scripts/db-setup.mjs", - "cf:build": "tsx scripts/prepare-paraglide.ts && DATABASE_PROVIDER=d1 NITRO_PRESET=cloudflare_module vite build && node scripts/sanitize-output.mjs", - "cf:deploy": "tsx scripts/cf-deploy.ts", + "start": "node .output/server/index.mjs", "postinstall": "mkdir -p data && (test -f src/config/db/schema.ts || node scripts/db-setup.mjs)", "db:setup": "node scripts/db-setup.mjs", - "db:push": "tsx scripts/with-env.ts drizzle-kit push --config=drizzle.config.ts", - "db:generate": "tsx scripts/with-env.ts drizzle-kit generate --config=drizzle.config.ts", - "db:migrate": "tsx scripts/with-env.ts drizzle-kit migrate --config=drizzle.config.ts", - "db:studio": "tsx scripts/with-env.ts drizzle-kit studio --config=drizzle.config.ts" + "db:push": "drizzle-kit push --config=drizzle.config.ts", + "db:generate": "drizzle-kit generate --config=drizzle.config.ts", + "db:migrate": "drizzle-kit migrate --config=drizzle.config.ts", + "db:studio": "drizzle-kit studio --config=drizzle.config.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1069.0", @@ -95,8 +90,7 @@ "tailwindcss": "^4.1.0", "tsx": "^4.21.0", "typescript": "^5.9.0", - "vite": "^8.0.16", - "wrangler": "^4.98.0" + "vite": "^8.0.16" }, "pnpm": { "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4087f90..b8008d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,7 +161,7 @@ importers: version: 0.31.10 nitro: specifier: 3.0.260603-beta - version: 3.0.260603-beta(@libsql/client@0.14.0)(chokidar@5.0.0)(dotenv@17.4.1)(drizzle-orm@0.45.2(@libsql/client@0.14.0)(kysely@0.28.17))(jiti@2.7.0)(miniflare@4.20260603.0)(rollup@4.62.4)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)) + version: 3.0.260603-beta(@libsql/client@0.14.0)(chokidar@5.0.0)(dotenv@17.4.1)(drizzle-orm@0.45.2(@libsql/client@0.14.0)(kysely@0.28.17))(jiti@2.7.0)(rollup@4.62.4)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)) rollup: specifier: 4.62.4 version: 4.62.4 @@ -180,9 +180,6 @@ importers: vite: specifier: ^8.0.16 version: 8.0.16(@types/node@22.19.17)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0) - wrangler: - specifier: ^4.98.0 - version: 4.98.0 packages: @@ -472,53 +469,6 @@ packages: '@types/react': optional: true - '@cloudflare/kv-asset-handler@0.5.0': - resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} - engines: {node: '>=22.0.0'} - - '@cloudflare/unenv-preset@2.16.1': - resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: '>1.20260305.0 <2.0.0-0' - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/workerd-darwin-64@1.20260603.1': - resolution: {integrity: sha512-cEXDWu6V3ZrpmwWkM4OJE9AeXjdAgOY5rh8EHhcBVCuP5rxnzUbPzLtrVOHx0UUUAcCrFq0Xsa6mZKL1VUZsKQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260603.1': - resolution: {integrity: sha512-uBPK4LaWJNbbCYwPnUAehlHbbVulhVZPZsdcAhBPfZhHb3QAuAEPAQepO/P67R3V6Cni4YGx1fLbL8A5wwoaNA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20260603.1': - resolution: {integrity: sha512-ht9l6/8Tk7Rp6kA4S9oFZ4X8u0VjnnFdmU/6B3fnABYKREYTKh2RdOqXqXxcp5eNJseireKnWik/hQOPK1CutQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260603.1': - resolution: {integrity: sha512-LJZ6x00rAjSrobV4m0ZW0TpH5ilBbKcWBzlH+y+KOUsIE/CpTuhAzKV43TbSnFLRX5+jrWKiz2v0hO91lPXy6A==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-windows-64@1.20260603.1': - resolution: {integrity: sha512-DvwqkXMAJRPoDN4PxapAwhlz/6ouD+6R1ttbAEK3cWD/QBvFF5STx7Ds/9Irf+rBly3np3uHWkeX+wZnNFEuzA==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -763,159 +713,6 @@ packages: peerDependencies: hono: ^4 - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@inlang/paraglide-js@2.18.2': resolution: {integrity: sha512-H2ksOE2dy9M4iJ+oDu8VPgk+B52C+OCFQpuJHBol0ZYLs3H17PCewVWIppkJNC36ClmLZnGhF+URjbcglTM8XQ==} hasBin: true @@ -948,9 +745,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@libsql/client@0.14.0': resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} @@ -1103,15 +897,6 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -1534,10 +1319,6 @@ packages: '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -1578,9 +1359,6 @@ packages: resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@speed-highlight/core@1.2.15': - resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} - '@sqlite.org/sqlite-wasm@3.48.0-build4': resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} hasBin: true @@ -1999,9 +1777,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2140,10 +1915,6 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2462,9 +2233,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -3198,11 +2966,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - miniflare@4.20260603.0: - resolution: {integrity: sha512-+kMQYB82gC8MPOuojHur3icQsUeZUEJ+Sphuo5rVC3Ri9txBLAW/mH33b9OVrpmkogQeaaqPS4tPtugJZhk5Kw==} - engines: {node: '>=22.0.0'} - hasBin: true - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -3383,9 +3146,6 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -3599,11 +3359,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -3633,10 +3388,6 @@ packages: engines: {node: '>=20.18.1'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3755,10 +3506,6 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -3829,10 +3576,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.24.8: - resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} - engines: {node: '>=20.18.1'} - undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} @@ -4084,21 +3827,6 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true - workerd@1.20260603.1: - resolution: {integrity: sha512-NPcbhI1++CS+fnELyXtsIR52en+5kwr/OrKeiQeYXGy10HxmPdsQBv9N+DU7hJIOOmBHhOGAAsoGDjyiQ2YCaA==} - engines: {node: '>=16'} - hasBin: true - - wrangler@4.98.0: - resolution: {integrity: sha512-cXfFUuF4rMIvE0hiMnXjEAB27ERryaCgquBJdUoPIjFzYYE1rbRdMUkEdQ18qDPUtsPvhJdqxLntixT9OfSzQw==} - engines: {node: '>=22.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260603.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4137,12 +3865,6 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - - youch@4.1.0-beta.10: - resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -4673,33 +4395,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@cloudflare/kv-asset-handler@0.5.0': {} - - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260603.1 - - '@cloudflare/workerd-darwin-64@1.20260603.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260603.1': - optional: true - - '@cloudflare/workerd-linux-64@1.20260603.1': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20260603.1': - optional: true - - '@cloudflare/workerd-windows-64@1.20260603.1': - optional: true - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - '@dnd-kit/accessibility@3.1.1(react@19.2.5)': dependencies: react: 19.2.5 @@ -4882,102 +4577,6 @@ snapshots: dependencies: hono: 4.12.12 - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.10.0 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - '@inlang/paraglide-js@2.18.2(typescript@5.9.3)': dependencies: '@inlang/recommend-sherlock': 0.2.1 @@ -5025,11 +4624,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@libsql/client@0.14.0': dependencies: '@libsql/core': 0.14.0 @@ -5230,18 +4824,6 @@ snapshots: '@oxc-project/types@0.133.0': {} - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.6.5': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': @@ -5523,8 +5105,6 @@ snapshots: '@sinclair/typebox@0.31.28': {} - '@sindresorhus/is@7.2.0': {} - '@sindresorhus/merge-streams@4.0.0': {} '@smithy/core@3.25.0': @@ -5575,8 +5155,6 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@speed-highlight/core@1.2.15': {} - '@sqlite.org/sqlite-wasm@3.48.0-build4': {} '@tailwindcss/node@4.3.0': @@ -6033,8 +5611,6 @@ snapshots: baseline-browser-mapping@2.10.17: {} - blake3-wasm@2.1.5: {} - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -6150,8 +5726,6 @@ snapshots: cookie@0.7.2: {} - cookie@1.1.1: {} - core-util-is@1.0.3: {} cors@2.8.6: @@ -6306,21 +5880,17 @@ snapshots: env-paths@2.2.1: {} - env-runner@0.1.9(miniflare@4.20260603.0): + env-runner@0.1.9: dependencies: crossws: 0.4.5(srvx@0.11.16) exsolve: 1.0.8 httpxy: 0.5.3 srvx: 0.11.16 - optionalDependencies: - miniflare: 4.20260603.0 error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - error-stack-parser-es@1.0.5: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -7255,18 +6825,6 @@ snapshots: mimic-function@5.0.1: {} - miniflare@4.20260603.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.24.8 - workerd: 1.20260603.1 - ws: 8.21.2 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -7300,12 +6858,12 @@ snapshots: nf3@0.3.17: {} - nitro@3.0.260603-beta(@libsql/client@0.14.0)(chokidar@5.0.0)(dotenv@17.4.1)(drizzle-orm@0.45.2(@libsql/client@0.14.0)(kysely@0.28.17))(jiti@2.7.0)(miniflare@4.20260603.0)(rollup@4.62.4)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)): + nitro@3.0.260603-beta(@libsql/client@0.14.0)(chokidar@5.0.0)(dotenv@17.4.1)(drizzle-orm@0.45.2(@libsql/client@0.14.0)(kysely@0.28.17))(jiti@2.7.0)(rollup@4.62.4)(vite@8.0.16(@types/node@22.19.17)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.16) db0: 0.3.4(@libsql/client@0.14.0)(drizzle-orm@0.45.2(@libsql/client@0.14.0)(kysely@0.28.17)) - env-runner: 0.1.9(miniflare@4.20260603.0) + env-runner: 0.1.9 h3: 2.0.1-rc.22(crossws@0.4.5(srvx@0.11.16)) hookable: 6.1.1 nf3: 0.3.17 @@ -7457,8 +7015,6 @@ snapshots: path-key@4.0.0: {} - path-to-regexp@6.3.0: {} - path-to-regexp@8.4.2: {} pathe@2.0.3: {} @@ -7733,8 +7289,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} - send@1.2.1: dependencies: debug: 4.4.3 @@ -7810,37 +7364,6 @@ snapshots: - supports-color - typescript - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7955,8 +7478,6 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - supports-color@10.2.2: {} - tabbable@6.4.0: {} tailwind-merge@3.5.0: {} @@ -8018,8 +7539,6 @@ snapshots: undici-types@6.21.0: {} - undici@7.24.8: {} - undici@7.29.0: {} unenv@2.0.0-rc.24: @@ -8172,30 +7691,6 @@ snapshots: dependencies: isexe: 3.1.5 - workerd@1.20260603.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260603.1 - '@cloudflare/workerd-darwin-arm64': 1.20260603.1 - '@cloudflare/workerd-linux-64': 1.20260603.1 - '@cloudflare/workerd-linux-arm64': 1.20260603.1 - '@cloudflare/workerd-windows-64': 1.20260603.1 - - wrangler@4.98.0: - dependencies: - '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1) - blake3-wasm: 2.1.5 - esbuild: 0.28.1 - miniflare: 4.20260603.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260603.1 - optionalDependencies: - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - wrappy@1.0.2: {} ws@8.21.2: {} @@ -8222,19 +7717,6 @@ snapshots: yoctocolors@2.1.2: {} - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.15 - cookie: 1.1.1 - youch-core: 0.3.3 - zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/public/imgs/icons/creem.png b/public/imgs/icons/creem.png deleted file mode 100644 index ebf60bb..0000000 Binary files a/public/imgs/icons/creem.png and /dev/null differ diff --git a/public/yandex_12549660fec7ecb2.html b/public/yandex_12549660fec7ecb2.html deleted file mode 100644 index 906f3b4..0000000 --- a/public/yandex_12549660fec7ecb2.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - Verification: 12549660fec7ecb2 - diff --git a/public/yandex_a384e0d41cd1d8fe.html b/public/yandex_a384e0d41cd1d8fe.html deleted file mode 100644 index 421876a..0000000 --- a/public/yandex_a384e0d41cd1d8fe.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - Verification: a384e0d41cd1d8fe - diff --git a/scripts/cf-deploy.ts b/scripts/cf-deploy.ts deleted file mode 100644 index c2d217c..0000000 --- a/scripts/cf-deploy.ts +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env node - -import { existsSync, readFileSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; -import { resolve } from 'node:path'; - -const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/; - -function loadEnvFile(filePath: string) { - const fullPath = resolve(filePath); - if (!existsSync(fullPath)) return false; - - for (const line of readFileSync(fullPath, 'utf8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - - const separator = trimmed.indexOf('='); - if (separator <= 0) continue; - - const key = trimmed.slice(0, separator).trim(); - if (!ENV_NAME.test(key) || process.env[key] !== undefined) continue; - - let value = trimmed.slice(separator + 1).trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - process.env[key] = value; - } - return true; -} - -const loaded = loadEnvFile('.env.production') || loadEnvFile('.env.local'); -if (loaded) console.log('Loaded deployment environment file.'); - -function run(command: string, args: string[]) { - const result = spawnSync(command, args, { - cwd: process.cwd(), - env: process.env, - stdio: 'inherit', - shell: false, - }); - if (result.error) throw result.error; - if (result.status !== 0) process.exit(result.status ?? 1); -} - -run('pnpm', ['cf:build']); -run('pnpm', ['exec', 'wrangler', 'deploy']); diff --git a/scripts/db-setup.mjs b/scripts/db-setup.mjs index deb549d..ef8c4cd 100644 --- a/scripts/db-setup.mjs +++ b/scripts/db-setup.mjs @@ -1,52 +1,8 @@ -// Copies the single-user SQLite/D1 schema into the generated working schema. -// -// Env-file loading mirrors scripts/with-env.ts so this script picks up -// DATABASE_PROVIDER from .env. / .env.local / .env when run from -// `pnpm install` postinstall (which doesn't go through with-env.ts). -import { copyFileSync, existsSync, readFileSync } from "node:fs"; +// Copies the single-user local SQLite schema into the generated working schema. +import { copyFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; -function loadEnvFile(filePath) { - if (!existsSync(filePath)) return false; - const content = readFileSync(filePath, "utf-8"); - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq === -1) continue; - const key = trimmed.slice(0, eq).trim(); - let value = trimmed.slice(eq + 1).trim(); - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - if (!process.env[key]) process.env[key] = value; - } - return true; -} - -const nodeEnv = process.env.NODE_ENV || "development"; -const envFiles = process.env.ENV_FILE - ? [process.env.ENV_FILE] - : [`.env.${nodeEnv}.local`, `.env.${nodeEnv}`, ".env.local", ".env"]; -for (const f of envFiles) loadEnvFile(resolve(f)); - -const TEMPLATE_BY_PROVIDER = { - sqlite: "sqlite", - d1: "sqlite", -}; - -const provider = (process.env.DATABASE_PROVIDER || "sqlite").toLowerCase(); -const templateName = TEMPLATE_BY_PROVIDER[provider]; - -if (!templateName) { - console.error( - `db-setup: unknown DATABASE_PROVIDER=${provider} (supported: ${Object.keys(TEMPLATE_BY_PROVIDER).join(", ")})`, - ); - process.exit(1); -} - -const src = resolve(`src/config/db/schema.${templateName}.ts`); +const src = resolve("src/config/db/schema.sqlite.ts"); const dst = resolve("src/config/db/schema.ts"); if (!existsSync(src)) { @@ -55,4 +11,4 @@ if (!existsSync(src)) { } copyFileSync(src, dst); -console.log(`db-setup: schema.ts ← schema.${templateName}.ts (DATABASE_PROVIDER=${provider})`); +console.log("db-setup: schema.ts ← schema.sqlite.ts"); diff --git a/scripts/mcp-server.ts b/scripts/mcp-server.ts index 43f13b2..545ab8d 100644 --- a/scripts/mcp-server.ts +++ b/scripts/mcp-server.ts @@ -1,36 +1,5 @@ #!/usr/bin/env node -import { existsSync, readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; - -function loadEnvFile(path: string) { - if (!existsSync(path)) return false; - for (const line of readFileSync(path, 'utf8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const separator = trimmed.indexOf('='); - if (separator < 1) continue; - const key = trimmed.slice(0, separator).trim(); - let value = trimmed.slice(separator + 1).trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - process.env[key] ??= value; - } - return true; -} - -const nodeEnv = process.env.NODE_ENV || 'development'; -for (const filename of [ - `.env.${nodeEnv}.local`, - `.env.${nodeEnv}`, - '.env.local', - '.env', -]) { - if (loadEnvFile(resolve(filename))) break; -} +export {}; const { startBeatDesignMcpServer } = await import('../src/mcp/server'); startBeatDesignMcpServer(); diff --git a/scripts/sanitize-output.mjs b/scripts/sanitize-output.mjs index 21869c7..f9ccbef 100644 --- a/scripts/sanitize-output.mjs +++ b/scripts/sanitize-output.mjs @@ -1,7 +1,7 @@ import { readdir, readFile, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; -const roots = ['.output', '.wrangler']; +const roots = ['.output']; const textExtensions = new Set([ '', '.cjs', diff --git a/scripts/with-env.ts b/scripts/with-env.ts deleted file mode 100644 index 35e22fa..0000000 --- a/scripts/with-env.ts +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env node -/** - * Environment-aware script wrapper - * - * Loads env file then executes the given command. - * - * Usage: - * tsx scripts/with-env.ts [args...] - * NODE_ENV=production tsx scripts/with-env.ts [args...] - * ENV_FILE=.env.production tsx scripts/with-env.ts [args...] - * - * Priority: ENV_FILE > .env.{NODE_ENV} > .env.local > .env - */ -import { spawnSync } from 'child_process'; -import { existsSync, readFileSync } from 'fs'; -import { resolve } from 'path'; - -function loadEnv(filePath: string) { - if (!existsSync(filePath)) return false; - const content = readFileSync(filePath, 'utf-8'); - for (const line of content.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const eqIndex = trimmed.indexOf('='); - if (eqIndex === -1) continue; - const key = trimmed.slice(0, eqIndex).trim(); - let value = trimmed.slice(eqIndex + 1).trim(); - // Strip surrounding quotes (single or double) - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - if (!process.env[key]) { - process.env[key] = value; - } - } - return true; -} - -// Determine which env files to load -const nodeEnv = process.env.NODE_ENV || 'development'; -const envFile = process.env.ENV_FILE; - -const filesToTry = envFile - ? [envFile] - : [`.env.${nodeEnv}.local`, `.env.${nodeEnv}`, '.env.local', '.env']; - -let loaded = false; -for (const file of filesToTry) { - const fullPath = resolve(file); - if (loadEnv(fullPath)) { - console.log(`📄 Loaded: ${file}`); - loaded = true; - } -} - -if (!loaded) { - console.log('⚠️ No env file found, using process environment'); -} - -// Get command -const args = process.argv.slice(2); -if (args.length === 0) { - console.error('❌ No command provided'); - process.exit(1); -} - -const [command, ...commandArgs] = args; -console.log(`▶️ ${[command, ...commandArgs].join(' ')}\n`); - -const result = spawnSync(command, commandArgs, { - stdio: 'inherit', - cwd: process.cwd(), - env: process.env, - shell: false, -}); - -if (result.error) { - console.error(`❌ Failed to start ${command}: ${result.error.message}`); - process.exit(1); -} - -if (result.status !== 0) { - process.exit(1); -} diff --git a/src/components/app/create-project-route-page.test.ts b/src/components/app/create-project-route-page.test.ts index 6ba4749..94bf924 100644 --- a/src/components/app/create-project-route-page.test.ts +++ b/src/components/app/create-project-route-page.test.ts @@ -11,7 +11,7 @@ test('visiting Studio or Canvas does not create a project automatically', () => assert.doesNotMatch(source, /useEffect/); assert.match(source, /createProject\('studio'\)/); assert.match(source, /createProject\('canvas'\)/); - assert.match(source, /envConfigs\.app_logo/); + assert.match(source, /appConfig\.app_logo/); assert.match(source, /'\/api\/app\/projects'/); }); diff --git a/src/components/app/create-project-route-page.tsx b/src/components/app/create-project-route-page.tsx index c94ba00..71a12b7 100644 --- a/src/components/app/create-project-route-page.tsx +++ b/src/components/app/create-project-route-page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { AlertCircleIcon, Loader2Icon } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import type { WorkspaceMode } from '@/config/workspace-modes'; import { buildPostCreateProjectDetailPath } from '@/core/projects/project-entry'; import { apiJsonPost } from '@/lib/api-client'; @@ -108,8 +108,8 @@ export function CreateProjectRoutePage({ ) : ( {envConfigs.app_name} )} diff --git a/src/components/app/product-page-shell.tsx b/src/components/app/product-page-shell.tsx index 620ab5a..eacaddf 100644 --- a/src/components/app/product-page-shell.tsx +++ b/src/components/app/product-page-shell.tsx @@ -9,7 +9,8 @@ import { GitHubIcon } from '@/components/icons/github'; import { Link } from '@/core/i18n/navigation'; import { useTranslations } from '@/core/workspace-lib/shims/next-intl'; import { apiJsonPatch, apiJsonPost } from '@/lib/api-client'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; +import { ACTIVE_GENERATION_PROVIDER_ID } from '@/config/generation-providers'; import type { WorkspaceMode } from '@/config/workspace-modes'; export function ProductPageShell({ @@ -124,8 +125,8 @@ export function ProductPageShell({ className="inline-flex size-9 items-center justify-center rounded-xl text-[var(--beat-text-1)] transition hover:bg-white/[0.06]" > {envConfigs.app_name} @@ -180,7 +181,7 @@ export function ProductPageShell({ ) : null} - {envConfigs.app_name} + {appConfig.app_name} ); } diff --git a/src/components/pricing/beatapi-pricing-page.tsx b/src/components/pricing/beatapi-pricing-page.tsx index 1879c60..dad84d5 100644 --- a/src/components/pricing/beatapi-pricing-page.tsx +++ b/src/components/pricing/beatapi-pricing-page.tsx @@ -14,7 +14,7 @@ import { type PricingComparisonLabels, } from '@/components/pricing/pricing-comparison-table'; import { Input } from '@/components/ui/input'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { Link } from '@/core/i18n/navigation'; import { cn } from '@/lib/utils'; import { m } from '@/paraglide/messages.js'; @@ -74,7 +74,7 @@ export function BeatApiPricingPage({ locale }: { locale: string }) { const labels: PricingComparisonLabels = { spec: copy.colSpec, - beatapi: envConfigs.app_name, + beatapi: appConfig.app_name, competitor: copy.colCompetitor, higgsfield: copy.colHiggsfield, discount: copy.colDiscount, diff --git a/src/config/database-defaults.test.ts b/src/config/database-defaults.test.ts index 1d05310..044a3a9 100644 --- a/src/config/database-defaults.test.ts +++ b/src/config/database-defaults.test.ts @@ -12,7 +12,8 @@ test('Drizzle and the runtime use the same default local SQLite database', () => 'utf8' ); - assert.match(runtimeConfig, /file:data\/workspace\.db/); - assert.match(drizzleConfig, /file:data\/workspace\.db/); - assert.doesNotMatch(drizzleConfig, /file:data\/local\.db/); + assert.match(runtimeConfig, /file:data\/local\.db/); + assert.match(drizzleConfig, /file:data\/local\.db/); + assert.doesNotMatch(runtimeConfig, /DATABASE_PROVIDER|process\.env/); + assert.doesNotMatch(drizzleConfig, /DATABASE_PROVIDER|process\.env/); }); diff --git a/src/config/db/schema.sqlite.ts b/src/config/db/schema.sqlite.ts index 4c8a96b..3b2aebd 100644 --- a/src/config/db/schema.sqlite.ts +++ b/src/config/db/schema.sqlite.ts @@ -1,5 +1,5 @@ /** - * Single-user BeatDesign schema for local SQLite and Cloudflare D1. + * Single-user BeatDesign schema for local SQLite. * The workspace deliberately has no auth, billing, credits, or RBAC tables. */ diff --git a/src/config/generation-providers.ts b/src/config/generation-providers.ts index ff328d4..dfdc51f 100644 --- a/src/config/generation-providers.ts +++ b/src/config/generation-providers.ts @@ -4,9 +4,11 @@ import type { GenerationProviderRegistrar } from '@/core/generation-providers/co * Source-level extension point for open-source forks. * * BeatAPI is registered by the core and remains the default. A fork can add a - * provider implementation here, then set GENERATION_PROVIDER to its id. Keep - * credentials inside that provider's server-only implementation. + * provider implementation here and change ACTIVE_GENERATION_PROVIDER_ID. + * Keep credentials inside that provider's server-only implementation. */ +export const ACTIVE_GENERATION_PROVIDER_ID = 'beatapi'; + export function registerProjectGenerationProviders( _register: GenerationProviderRegistrar ) {} diff --git a/src/config/index.ts b/src/config/index.ts index 17262d9..c462bcb 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,59 +1,9 @@ -const metaEnv: Record = - (import.meta as { env?: Record }).env ?? {}; -const procEnv: Record = - typeof process !== 'undefined' && process.env ? process.env : {}; - -const publicEnv = (key: string) => metaEnv[key] ?? procEnv[key]; - -export const envConfigs: Record = { - app_url: publicEnv('VITE_APP_URL') ?? 'http://localhost:3020', - app_name: publicEnv('VITE_APP_NAME') ?? 'BeatDesign', +/** Fixed product configuration for the local BeatDesign workbench. */ +export const appConfig = { + app_url: 'http://127.0.0.1:3020', + app_name: 'BeatDesign', app_description: - publicEnv('VITE_APP_DESCRIPTION') ?? 'The open-source, local-first AI canvas for image and video creation.', - app_logo: publicEnv('VITE_APP_LOGO') ?? '/logo.png', - generation_provider: - procEnv.GENERATION_PROVIDER ?? - publicEnv('VITE_GENERATION_PROVIDER') ?? - 'beatapi', - - database_provider: procEnv.DATABASE_PROVIDER ?? 'sqlite', - database_url: procEnv.DATABASE_URL ?? 'file:data/workspace.db', - database_auth_token: procEnv.DATABASE_AUTH_TOKEN ?? '', - db_schema: procEnv.DB_SCHEMA ?? 'main', - db_singleton_enabled: procEnv.DB_SINGLETON_ENABLED ?? 'true', - db_max_connections: procEnv.DB_MAX_CONNECTIONS ?? '1', - - workspace_storage_mode: procEnv.WORKSPACE_STORAGE_MODE ?? 'beatapi', - beatapi_managed_r2_region: procEnv.BEATAPI_MANAGED_R2_REGION ?? 'auto', - beatapi_managed_r2_endpoint: procEnv.BEATAPI_MANAGED_R2_ENDPOINT ?? '', - beatapi_managed_r2_access_key_id: - procEnv.BEATAPI_MANAGED_R2_ACCESS_KEY_ID ?? '', - beatapi_managed_r2_secret_access_key: - procEnv.BEATAPI_MANAGED_R2_SECRET_ACCESS_KEY ?? '', - beatapi_managed_r2_bucket_name: - procEnv.BEATAPI_MANAGED_R2_BUCKET_NAME ?? '', - beatapi_managed_r2_public_url: - procEnv.BEATAPI_MANAGED_R2_PUBLIC_URL ?? '', - beatapi_managed_r2_force_path_style: - procEnv.BEATAPI_MANAGED_R2_FORCE_PATH_STYLE ?? 'true', - r2_region: procEnv.R2_REGION ?? 'auto', - r2_endpoint: procEnv.R2_ENDPOINT ?? '', - r2_access_key_id: procEnv.R2_ACCESS_KEY_ID ?? '', - r2_secret_access_key: procEnv.R2_SECRET_ACCESS_KEY ?? '', - r2_image_bucket_name: procEnv.R2_IMAGE_BUCKET_NAME ?? '', - r2_image_public_url: procEnv.R2_IMAGE_PUBLIC_URL ?? '', - r2_video_bucket_name: procEnv.R2_VIDEO_BUCKET_NAME ?? '', - r2_video_public_url: procEnv.R2_VIDEO_PUBLIC_URL ?? '', - r2_bucket_name: - procEnv.R2_BUCKET_NAME ?? - procEnv.R2_IMAGE_BUCKET_NAME ?? - procEnv.R2_VIDEO_BUCKET_NAME ?? - '', - r2_public_url: - procEnv.R2_PUBLIC_URL ?? - procEnv.R2_IMAGE_PUBLIC_URL ?? - procEnv.R2_VIDEO_PUBLIC_URL ?? - '', - r2_force_path_style: procEnv.R2_FORCE_PATH_STYLE ?? 'true', -}; + app_logo: '/logo.png', + database_url: 'file:data/local.db', +} as const; diff --git a/src/core/db/create-db.ts b/src/core/db/create-db.ts index 6167b34..44f6e1c 100644 --- a/src/core/db/create-db.ts +++ b/src/core/db/create-db.ts @@ -1,17 +1,15 @@ -import { createD1Db } from './d1'; import { createSqliteDb } from './sqlite'; import type { DbConfig } from './types'; const sqliteCompatProxyCache = new WeakMap(); /** - * SQLite/D1 compatibility shim used by the D1 runtime and local test fixture. + * SQLite compatibility shim used by the local workbench. * It keeps the service layer dialect-neutral without advertising another * production database. */ function withSqliteCompat( dbInstance: T, - provider?: string ): T { if (dbInstance && typeof dbInstance === 'object') { const cached = sqliteCompatProxyCache.get(dbInstance); @@ -38,15 +36,12 @@ function withSqliteCompat( const proxied = new Proxy(dbInstance, { get(target, prop, receiver) { if (prop === 'transaction') { - if (provider === 'd1') { - return (fn: any) => fn(proxied); - } const original = Reflect.get(target, prop, receiver); if (typeof original !== 'function') return original; return (fn: any, ...rest: any[]) => original.call( target, - (tx: any) => fn(withSqliteCompat(tx, provider)), + (tx: any) => fn(withSqliteCompat(tx)), ...rest ); } @@ -67,19 +62,7 @@ function withSqliteCompat( } export function createDb(config: DbConfig): any { - if (config.database_provider === 'd1') { - return withSqliteCompat(createD1Db() as any, 'd1'); - } - - if (config.database_provider === 'sqlite') { - return withSqliteCompat(createSqliteDb(config) as any, 'sqlite'); - } - - throw new Error( - 'Unsupported DATABASE_PROVIDER=' + - config.database_provider + - '. Use sqlite or d1.' - ); + return withSqliteCompat(createSqliteDb(config) as any); } export async function closeDb(_config: DbConfig) {} diff --git a/src/core/db/d1.ts b/src/core/db/d1.ts deleted file mode 100644 index 1fb2ff3..0000000 --- a/src/core/db/d1.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { drizzle } from 'drizzle-orm/d1'; - -// Minimal D1Database type to avoid pulling in @cloudflare/workers-types globally -type D1Database = { - prepare(query: string): any; - batch(statements: any[]): Promise; - exec(query: string): Promise; - dump(): Promise; -}; - -// D1 singleton instance -let d1DbInstance: ReturnType | null = null; - -/** - * Resolve the D1 binding named `DB` (see wrangler.jsonc `d1_databases`). - * - * On Cloudflare Workers the binding env is stashed on `globalThis.__CF_ENV__` - * by the server entry (src/server.ts, via `cloudflare:workers`). Nitro's - * cloudflare presets also expose it as `globalThis.__env__` — check both. - */ -function getD1Binding(): D1Database { - const g = globalThis as any; - const env = g.__CF_ENV__ ?? g.__env__; - const binding = env?.DB; - if (!binding) { - throw new Error( - 'D1 binding "DB" not found. DATABASE_PROVIDER=d1 only works on Cloudflare Workers ' + - 'with a d1_databases binding named "DB" in wrangler.jsonc.' - ); - } - return binding as D1Database; -} - -export function createD1Db() { - if (d1DbInstance) return d1DbInstance; - - const binding = getD1Binding(); - d1DbInstance = drizzle(binding); - return d1DbInstance; -} diff --git a/src/core/db/index.ts b/src/core/db/index.ts index b7c0fc3..4ab59db 100644 --- a/src/core/db/index.ts +++ b/src/core/db/index.ts @@ -1,5 +1,5 @@ import { createDb } from './create-db'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; let dbInstance: any = null; @@ -7,12 +7,7 @@ export function db() { if (dbInstance) return dbInstance; const instance = createDb({ - database_provider: envConfigs.database_provider, - database_url: envConfigs.database_url || 'file:data/local.db', - database_auth_token: envConfigs.database_auth_token || undefined, - db_schema: envConfigs.db_schema, - db_singleton_enabled: envConfigs.db_singleton_enabled, - db_max_connections: envConfigs.db_max_connections, + database_url: appConfig.database_url, }); dbInstance = instance; diff --git a/src/core/db/sqlite.ts b/src/core/db/sqlite.ts index 244b382..41326bc 100644 --- a/src/core/db/sqlite.ts +++ b/src/core/db/sqlite.ts @@ -3,48 +3,17 @@ import { drizzle } from 'drizzle-orm/libsql'; import type { DbConfig } from './types'; -const isCloudflareWorker = - typeof globalThis !== 'undefined' && 'Cloudflare' in globalThis; - // SQLite/libsql singleton let sqliteDbInstance: ReturnType | null = null; export function createSqliteDb(config: DbConfig) { const databaseUrl = config.database_url; if (!databaseUrl) { - throw new Error('DATABASE_URL is not set'); - } - - const options: Record = {}; - if (config.database_auth_token) { - options.authToken = config.database_auth_token; - } - - // In Cloudflare Workers, create new connection each time - if (isCloudflareWorker) { - const client = createClient({ - url: databaseUrl, - ...options, - }); - return drizzle({ client }); - } - - // Singleton mode: reuse existing instance - if (config.db_singleton_enabled === 'true') { - if (sqliteDbInstance) return sqliteDbInstance; - - const client = createClient({ - url: databaseUrl, - ...options, - }); - sqliteDbInstance = drizzle({ client }); - return sqliteDbInstance; + throw new Error('Local SQLite database path is not configured'); } - // Non-singleton mode: create new connection each time - const client = createClient({ - url: databaseUrl, - ...options, - }); - return drizzle({ client }); + if (sqliteDbInstance) return sqliteDbInstance; + const client = createClient({ url: databaseUrl }); + sqliteDbInstance = drizzle({ client }); + return sqliteDbInstance; } diff --git a/src/core/db/types.ts b/src/core/db/types.ts index 2bb0069..9bcf568 100644 --- a/src/core/db/types.ts +++ b/src/core/db/types.ts @@ -1,8 +1,3 @@ export interface DbConfig { - database_provider: string; database_url: string; - database_auth_token?: string; - db_schema?: string; - db_singleton_enabled?: string; - db_max_connections?: string; } diff --git a/src/core/effects/runtime-config.ts b/src/core/effects/runtime-config.ts index b01ed7d..f965693 100644 --- a/src/core/effects/runtime-config.ts +++ b/src/core/effects/runtime-config.ts @@ -1,12 +1,5 @@ const DEFAULT_EFFECTS_POLL_INTERVAL_MS = 20_000; const DEFAULT_EFFECTS_GENERATION_TIMEOUT_MS = 30 * 60 * 1000; -const procEnv = - typeof process !== 'undefined' && process.env ? process.env : {}; - -const readPositiveInt = (value: string | undefined, fallback: number) => { - const parsed = Number.parseInt(value ?? '', 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; -}; const formatDuration = (ms: number) => { const minutes = Math.round(ms / 60_000); @@ -17,15 +10,10 @@ const formatDuration = (ms: number) => { return `${seconds} second${seconds === 1 ? '' : 's'}`; }; -export const EFFECTS_POLL_INTERVAL_MS = readPositiveInt( - procEnv.EFFECTS_POLL_INTERVAL_MS, - DEFAULT_EFFECTS_POLL_INTERVAL_MS -); +export const EFFECTS_POLL_INTERVAL_MS = DEFAULT_EFFECTS_POLL_INTERVAL_MS; -export const EFFECTS_GENERATION_TIMEOUT_MS = readPositiveInt( - procEnv.EFFECTS_GENERATION_TIMEOUT_MS, - DEFAULT_EFFECTS_GENERATION_TIMEOUT_MS -); +export const EFFECTS_GENERATION_TIMEOUT_MS = + DEFAULT_EFFECTS_GENERATION_TIMEOUT_MS; export const EFFECTS_GENERATION_TIMEOUT_MESSAGE = `Task timed out after ${formatDuration( EFFECTS_GENERATION_TIMEOUT_MS diff --git a/src/core/generation-providers/registry.ts b/src/core/generation-providers/registry.ts index cfa4839..f0becf4 100644 --- a/src/core/generation-providers/registry.ts +++ b/src/core/generation-providers/registry.ts @@ -1,5 +1,7 @@ -import { envConfigs } from '@/config'; -import { registerProjectGenerationProviders } from '@/config/generation-providers'; +import { + ACTIVE_GENERATION_PROVIDER_ID, + registerProjectGenerationProviders, +} from '@/config/generation-providers'; import { MockAdapter } from '@/core/adapters/mock-adapter'; import { beatApiGenerationProvider, BEATAPI_PROVIDER_ID } from './beatapi-provider'; @@ -60,10 +62,10 @@ export function getGenerationProvider(providerId: string) { export function getActiveGenerationProviderId() { ensureProvidersRegistered(); - const requested = envConfigs.generation_provider?.trim() || BEATAPI_PROVIDER_ID; + const requested = ACTIVE_GENERATION_PROVIDER_ID.trim() || BEATAPI_PROVIDER_ID; if (!providers.has(requested)) { throw new Error( - `Generation provider ${requested} is not registered. Register it in src/config/generation-providers.ts or set GENERATION_PROVIDER=${BEATAPI_PROVIDER_ID}.` + `Generation provider ${requested} is not registered. Register it and select it in src/config/generation-providers.ts.` ); } return requested; diff --git a/src/core/projects/import-local-asset.ts b/src/core/projects/import-local-asset.ts index eb71d32..fb7134c 100644 --- a/src/core/projects/import-local-asset.ts +++ b/src/core/projects/import-local-asset.ts @@ -1,7 +1,6 @@ import { readFile, stat } from 'node:fs/promises'; import { basename, isAbsolute, resolve } from 'node:path'; -import { envConfigs } from '@/config'; import { detectUploadedMediaType, getCanonicalUploadedMediaMimeType, @@ -57,9 +56,6 @@ export async function importLocalProjectAsset({ projectId: string; filePath: string; }) { - if (envConfigs.database_provider !== 'sqlite') { - throw new Error('Local asset import is only available in SQLite mode.'); - } const trimmedPath = filePath.trim(); if (!isAbsolute(trimmedPath)) { throw new Error('Asset import requires an absolute file path.'); diff --git a/src/core/workspace-storage/config/storage-config.ts b/src/core/workspace-storage/config/storage-config.ts deleted file mode 100644 index 0be5fd2..0000000 --- a/src/core/workspace-storage/config/storage-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { envConfigs } from '@/config'; -import type { StorageConfig } from '../types'; - -/** - * Default storage configuration - * - * This configuration is loaded from environment variables - */ -export const storageConfig: StorageConfig = { - region: envConfigs.r2_region || 'auto', - endpoint: envConfigs.r2_endpoint || undefined, - accessKeyId: envConfigs.r2_access_key_id || '', - secretAccessKey: envConfigs.r2_secret_access_key || '', - bucketName: envConfigs.r2_bucket_name || '', - publicUrl: envConfigs.r2_public_url || undefined, - forcePathStyle: envConfigs.r2_force_path_style !== 'false', -}; diff --git a/src/core/workspace-storage/endpoint-policy.ts b/src/core/workspace-storage/endpoint-policy.ts index 61fbe46..a716536 100644 --- a/src/core/workspace-storage/endpoint-policy.ts +++ b/src/core/workspace-storage/endpoint-policy.ts @@ -66,7 +66,7 @@ export function validateStorageEndpoint( return { ok: false, message: - 'Private storage endpoints require WORKSPACE_ALLOW_PRIVATE_STORAGE_ENDPOINTS=true', + 'Private storage endpoints are not supported by the local workspace', }; } diff --git a/src/core/workspace-storage/index.ts b/src/core/workspace-storage/index.ts deleted file mode 100644 index 78b912d..0000000 --- a/src/core/workspace-storage/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { storageConfig } from './config/storage-config'; -import { S3Provider } from './provider/s3'; -import type { - PresignedUploadResult, - StorageConfig, - StorageProvider, - UploadFileResult, -} from './types'; - -/** - * Default storage configuration - */ -export const defaultStorageConfig: StorageConfig = storageConfig; - -/** - * Global storage provider instance - */ -let storageProvider: StorageProvider | null = null; - -/** - * Get the storage provider - * @returns current storage provider instance - * @throws Error if provider is not initialized - */ -export const getStorageProvider = (): StorageProvider => { - if (!storageProvider) { - return initializeStorageProvider(); - } - return storageProvider; -}; - -/** - * Initialize the storage provider - * @returns initialized storage provider - */ -export const initializeStorageProvider = (): StorageProvider => { - if (!storageProvider) { - storageProvider = new S3Provider(); - } - return storageProvider!; -}; - -/** - * Uploads a file to the configured storage provider - * - * @param file - The file to upload (Buffer or Blob) - * @param filename - Original filename with extension - * @param contentType - MIME type of the file - * @param folder - Optional folder path to store the file in - * @returns Promise with the URL of the uploaded file and its storage key - */ -export const uploadFile = async ( - file: Buffer | Blob, - filename: string, - contentType: string, - folder?: string, - options?: { - bucketName?: string; - publicUrl?: string; - } -): Promise => { - const provider = getStorageProvider(); - return provider.uploadFile({ - file, - filename, - contentType, - folder, - bucketName: options?.bucketName, - publicUrl: options?.publicUrl, - }); -}; - -export const createPresignedUpload = async ( - filename: string, - contentType: string, - key: string, - options?: { - bucketName?: string; - } -): Promise => { - const provider = getStorageProvider(); - return provider.createPresignedUpload({ - filename, - contentType, - key, - bucketName: options?.bucketName, - }); -}; - -export const objectExists = async ( - key: string, - options?: { - bucketName?: string; - } -): Promise => { - const provider = getStorageProvider(); - return provider.objectExists(key, { - bucketName: options?.bucketName, - }); -}; - -/** - * Deletes a file from the storage provider - * - * @param key - The storage key of the file to delete - * @returns Promise that resolves when the file is deleted - */ -export const deleteFile = async (key: string): Promise => { - const provider = getStorageProvider(); - return provider.deleteFile(key); -}; diff --git a/src/core/workspace-storage/provider/s3.ts b/src/core/workspace-storage/provider/s3.ts index 7d36230..c0376c6 100644 --- a/src/core/workspace-storage/provider/s3.ts +++ b/src/core/workspace-storage/provider/s3.ts @@ -6,7 +6,6 @@ import { } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { S3mini as s3mini } from 's3mini'; -import { storageConfig } from '../config/storage-config'; import { ConfigurationError, type PresignedUploadParams, @@ -26,7 +25,7 @@ import { * VisuGen internal docs * * This provider works with Amazon S3 and compatible services like Cloudflare R2 - * using s3mini for better Cloudflare Workers compatibility + * using s3mini for S3-compatible object storage * https://github.com/good-lly/s3mini * https://developers.cloudflare.com/r2/ */ @@ -35,7 +34,7 @@ export class S3Provider implements StorageProvider { private s3Client: s3mini | null = null; private awsS3Client: S3Client | null = null; - constructor(config: StorageConfig = storageConfig) { + constructor(config: StorageConfig) { this.config = config; } diff --git a/src/hooks/use-user-permissions.ts b/src/hooks/use-user-permissions.ts deleted file mode 100644 index ecc4c2c..0000000 --- a/src/hooks/use-user-permissions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { apiGet } from '@/lib/api-client'; - -export interface UserPermissions { - isAdmin: boolean; - permissions?: string[]; -} - -// Current user's permission summary — shared by site-user-menu and -// app-layout (single network call, react-query dedupes). -export function useUserPermissions(enabled = true) { - return useQuery({ - queryKey: ['user-permissions'], - queryFn: () => apiGet('/api/user/permissions'), - staleTime: 5 * 60_000, - enabled, - }); -} diff --git a/src/lib/crypto.test.ts b/src/lib/crypto.test.ts index 79aaa7d..525532f 100644 --- a/src/lib/crypto.test.ts +++ b/src/lib/crypto.test.ts @@ -7,38 +7,11 @@ import test from 'node:test'; import { decryptSecret, encryptSecret, isEncryptedSecret } from './crypto'; -const restoreEnv = (name: string, value: string | undefined) => { - if (value === undefined) delete process.env[name]; - else process.env[name] = value; -}; - -test('encrypts configured provider secrets instead of persisting plaintext', async () => { - const originalKey = process.env.CONFIG_ENCRYPTION_KEY; - try { - process.env.CONFIG_ENCRYPTION_KEY = 'test-only-encryption-key'; - const encrypted = await encryptSecret('beatapi_test_secret'); - assert.equal(isEncryptedSecret(encrypted), true); - assert.notEqual(encrypted, 'beatapi_test_secret'); - assert.equal(await decryptSecret(encrypted), 'beatapi_test_secret'); - } finally { - restoreEnv('CONFIG_ENCRYPTION_KEY', originalKey); - } -}); - -test('fails closed outside local SQLite when no encryption key is configured', async () => { - const originalKey = process.env.CONFIG_ENCRYPTION_KEY; - const originalProvider = process.env.DATABASE_PROVIDER; - try { - delete process.env.CONFIG_ENCRYPTION_KEY; - process.env.DATABASE_PROVIDER = 'd1'; - await assert.rejects( - encryptSecret('must-not-be-plaintext'), - /Secret encryption is unavailable/ - ); - } finally { - restoreEnv('CONFIG_ENCRYPTION_KEY', originalKey); - restoreEnv('DATABASE_PROVIDER', originalProvider); - } +test('encrypts provider secrets with the local installation key', async () => { + const encrypted = await encryptSecret('beatapi_test_secret'); + assert.equal(isEncryptedSecret(encrypted), true); + assert.notEqual(encrypted, 'beatapi_test_secret'); + assert.equal(await decryptSecret(encrypted), 'beatapi_test_secret'); }); test('local SQLite creates a per-install key and encrypts without OS-specific setup', () => { @@ -53,15 +26,10 @@ test('local SQLite creates a per-install key and encrypts without OS-specific se `; try { - const env: NodeJS.ProcessEnv = { - ...process.env, - DATABASE_PROVIDER: 'sqlite', - }; - delete env.CONFIG_ENCRYPTION_KEY; const result = spawnSync( process.execPath, ['--import', tsxImport, '--input-type=module', '--eval', script], - { cwd: installDir, env, encoding: 'utf8' } + { cwd: installDir, env: process.env, encoding: 'utf8' } ); assert.equal(result.status, 0, result.stderr); const keyPath = join(installDir, 'data', '.workspace-key'); diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 6ce0a0c..479376f 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,20 +1,16 @@ /** * AES-256-GCM encryption for provider secrets stored in the local database. * - * Built on Web Crypto (crypto.subtle) — works natively on Node 18+, Cloudflare - * Workers, and Edge runtimes with no nodejs_compat requirements. + * Built on Web Crypto (crypto.subtle), available in the supported local Node runtime. * * Encrypted values are self-describing: `enc:v1:`. * Plain values (no prefix) pass through decryptSecret unchanged so the config * service can migrate legacy rows after a verified encrypted write. * - * Key source: CONFIG_ENCRYPTION_KEY when explicitly configured; otherwise a - * per-install key is created at data/.workspace-key for local SQLite mode. - * Secret writes fail closed when neither source is available. + * A per-install key is created at data/.workspace-key. * - * This protects against database-only compromise (leaked backups, SQL - * injection dumps). It does NOT protect against a compromised app server — - * the key lives in env on the same machine. + * This protects against database-only compromise. It does not protect against + * a fully compromised local machine that can read both files. */ const ENC_PREFIX = 'enc:v1:'; @@ -44,15 +40,7 @@ async function deriveKey(secret: string): Promise { } function getEncryptionSecret(): string | undefined { - const configured = process.env.CONFIG_ENCRYPTION_KEY?.trim(); - if (configured) return configured; if (cachedEncryptionSecret) return cachedEncryptionSecret; - if ( - process.env.DATABASE_PROVIDER && - process.env.DATABASE_PROVIDER !== 'sqlite' - ) { - return undefined; - } if (typeof process.getBuiltinModule !== 'function') return undefined; const fs = process.getBuiltinModule('node:fs') as typeof import('node:fs'); @@ -99,9 +87,7 @@ export async function encryptSecret(plain: string): Promise { const secret = getEncryptionSecret(); if (!secret) { - throw new Error( - 'Secret encryption is unavailable. Configure CONFIG_ENCRYPTION_KEY.' - ); + throw new Error('Local secret encryption is unavailable.'); } const key = await deriveKey(secret); diff --git a/src/lib/env.ts b/src/lib/env.ts deleted file mode 100644 index de4cd9a..0000000 --- a/src/lib/env.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -export const isProduction = process.env.NODE_ENV === 'production'; - -export const isCloudflareWorker = - typeof globalThis !== 'undefined' && 'Cloudflare' in globalThis; - -/** - * Load env files in the same priority order as the db scripts: - * .env.{development,production}.local > .env.{development,production} > .env.local > .env - * - * Earlier files win — if a key is already set, later files don't overwrite. - * Useful for scripts and configs that run outside of Next.js (drizzle-kit, init scripts, etc.). - */ -export function loadEnvFiles() { - const nodeEnv = process.env.NODE_ENV || 'development'; - const files = [`.env.${nodeEnv}.local`, `.env.${nodeEnv}`, '.env.local', '.env']; - - for (const file of files) { - const envPath = path.resolve(file); - if (!fs.existsSync(envPath)) continue; - const content = fs.readFileSync(envPath, 'utf-8'); - for (const line of content.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const eqIndex = trimmed.indexOf('='); - if (eqIndex === -1) continue; - const key = trimmed.slice(0, eqIndex).trim(); - let value = trimmed.slice(eqIndex + 1).trim(); - // Strip surrounding quotes (single or double) - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - if (!process.env[key]) { - process.env[key] = value; - } - } - } -} diff --git a/src/lib/with-env-wrapper.test.ts b/src/lib/with-env-wrapper.test.ts deleted file mode 100644 index ecb7e03..0000000 --- a/src/lib/with-env-wrapper.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import test from 'node:test'; - -const source = readFileSync( - new URL('../../scripts/with-env.ts', import.meta.url), - 'utf8' -); - -test('the environment wrapper forwards arguments without a shell', () => { - assert.match(source, /spawnSync\(command, commandArgs/); - assert.match(source, /shell: false/); - assert.doesNotMatch(source, /execSync/); - assert.doesNotMatch(source, /args\.join\(' '\).*exec/); -}); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 25266db..08e2e2c 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import * as z from 'zod/v4'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { beatDesignCommandSchema, canvasCardSchema, @@ -34,7 +34,7 @@ import { listProjectAssets, } from '@/core/workspace-lib/assets/user-assets'; -const VERSION = '0.2.0'; +const VERSION = '0.2.1'; const idSchema = z.string().trim().min(1).max(200); const toCommandCanvasCards = (cards: unknown[]) => @@ -492,7 +492,7 @@ export function createBeatDesignMcpServer() { annotations: { readOnlyHint: true }, }, withToolErrors(async ({ projectId, time }) => ({ - editorUrl: `${envConfigs.app_url.replace(/\/$/, '')}/editor/${encodeURIComponent(projectId)}?t=${time}`, + editorUrl: `${appConfig.app_url.replace(/\/$/, '')}/editor/${encodeURIComponent(projectId)}?t=${time}`, snapshot: semanticTimelineSnapshot(await loadProjectTimeline(projectId), time), })) ); diff --git a/src/modules/config/service.ts b/src/modules/config/service.ts index 16277c3..06fd202 100644 --- a/src/modules/config/service.ts +++ b/src/modules/config/service.ts @@ -28,7 +28,7 @@ export async function getDbConfigs(): Promise { if (isEncryptedSecret(row.value)) { const plain = await decryptSecret(row.value); if (plain === null) { - // Wrong/rotated encryption key — skip so env value (if any) applies. + // Wrong, rotated, or missing installation key: skip the unusable value. console.warn(`[config] failed to decrypt "${row.name}", skipping`); continue; } @@ -53,14 +53,12 @@ export async function getDbConfigs(): Promise { } } -/** - * Get all workspace configs. Environment fallbacks are resolved per key. - */ +/** Get all workspace configs stored in local SQLite. */ export async function getAllConfigs(): Promise { return getDbConfigs(); } -/** Provider settings writable from the local workspace dialog. */ +/** Provider and upload settings writable from the local workspace dialog. */ const WRITABLE_CONFIG_KEYS: ReadonlySet = new Set([ 'BEATAPI_API_BASE_URL', 'BEATAPI_API_KEY', @@ -75,8 +73,7 @@ const WRITABLE_CONFIG_KEYS: ReadonlySet = new Set([ ]); /** - * Provider secrets are always encrypted at rest. Local SQLite mode generates - * a per-install key; hosted modes require CONFIG_ENCRYPTION_KEY. + * Provider secrets are always encrypted at rest with a per-install local key. */ const SECRET_KEY_PATTERN = /(_secret|_secret_key|_token|_password|_private_key|_api_key|_access_key|_access_key_id|_api_v3_key)$/; @@ -142,5 +139,5 @@ export async function saveConfigs(configs: ConfigMap) { */ export async function getConfig(name: string): Promise { const configs = await getDbConfigs(); - return configs[name] || process.env[name] || undefined; + return configs[name] || undefined; } diff --git a/src/routes/(pages)/-static-page.tsx b/src/routes/(pages)/-static-page.tsx index 84b3509..9aa4baf 100644 --- a/src/routes/(pages)/-static-page.tsx +++ b/src/routes/(pages)/-static-page.tsx @@ -2,7 +2,7 @@ import { notFound, useLoaderData } from '@tanstack/react-router'; import { m } from "@/paraglide/messages.js"; import type { ComponentType } from 'react'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { baseLocale, getLocale, localizeUrl } from '@/paraglide/runtime.js'; type PageMeta = { @@ -47,7 +47,7 @@ export function staticPageRouteOptions(slug: string) { head: ({ loaderData }: { loaderData?: LoaderData }) => { if (!loaderData) return {}; const { meta, locale } = loaderData; - const canonical = localizeUrl(`${envConfigs.app_url}/${slug}`, { + const canonical = localizeUrl(`${appConfig.app_url}/${slug}`, { locale: locale as ReturnType, }).href; return { diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index ace83d6..c04549a 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -11,7 +11,7 @@ import { ThemeProvider } from 'next-themes'; import type { ReactNode } from 'react'; import { Toaster } from '@/components/ui/sonner'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { getQueryClient } from '@/lib/query-client'; import { m } from '@/paraglide/messages.js'; import { getLocale, locales, localizeUrl } from '@/paraglide/runtime.js'; @@ -23,13 +23,13 @@ import '@/styles/globals.css'; export const Route = createRootRoute({ head: () => { - const appUrl = envConfigs.app_url || ''; + const appUrl = appConfig.app_url || ''; return { meta: [ { charSet: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, - { title: envConfigs.app_name }, - { name: 'description', content: envConfigs.app_description }, + { title: appConfig.app_name }, + { name: 'description', content: appConfig.app_description }, ], links: [ ...locales.map((loc) => ({ diff --git a/src/routes/api/app/projects/$projectId/assets/index.ts b/src/routes/api/app/projects/$projectId/assets/index.ts index da4ff0c..f124243 100644 --- a/src/routes/api/app/projects/$projectId/assets/index.ts +++ b/src/routes/api/app/projects/$projectId/assets/index.ts @@ -1,6 +1,5 @@ import { createFileRoute } from '@tanstack/react-router'; -import { envConfigs } from '@/config'; import { detectUploadedMediaType, getCanonicalUploadedMediaMimeType, @@ -49,16 +48,6 @@ async function POST({ { status: 415 } ); } - if (envConfigs.database_provider !== 'sqlite') { - return Response.json( - { - error: - 'Local project assets are only available in local SQLite mode. Configure R2/S3 on hosted deployments.', - }, - { status: 501 } - ); - } - const { projectId } = params; const currentProject = await getProject({ projectId }); if (!currentProject || currentProject.status !== 'active') { diff --git a/src/routes/api/config/-storage-config.contract.test.ts b/src/routes/api/config/-storage-config.contract.test.ts index 0f22ad1..70336db 100644 --- a/src/routes/api/config/-storage-config.contract.test.ts +++ b/src/routes/api/config/-storage-config.contract.test.ts @@ -8,7 +8,7 @@ const uploadSource = readFileSync( 'utf8' ); -test('storage configuration offers managed BeatAPI and custom R2/S3 modes', () => { +test('storage configuration keeps BeatAPI default and local custom R2/S3 settings', () => { assert.match(configSource, /'beatapi' \| 's3'/); assert.match(configSource, /WORKSPACE_STORAGE_MODE/); assert.match(configSource, /R2_ENDPOINT/); @@ -17,12 +17,13 @@ test('storage configuration offers managed BeatAPI and custom R2/S3 modes', () = assert.match(configSource, /validateTrustedLocalJsonMutation/); }); -test('generation-authorized upload defaults to BeatAPI managed storage and can switch to S3', () => { +test('generation-authorized upload defaults to BeatAPI files and can switch to S3', () => { assert.match(uploadSource, /\/v1\/files/); assert.match(uploadSource, /storageMode === 's3'/); assert.match(uploadSource, /new S3Provider\(config\)/); assert.match(uploadSource, /file\.type\.startsWith\('video\/'\)/); - assert.match(uploadSource, /BEATAPI_MANAGED_R2_ENDPOINT/); + assert.doesNotMatch(uploadSource, /BEATAPI_MANAGED_R2/); + assert.doesNotMatch(uploadSource, /process\.env/); assert.match(uploadSource, /loadCustomStorageConfig/); assert.match(uploadSource, /generationIntentToken/); assert.match(uploadSource, /claimGenerationUploadSlot/); diff --git a/src/routes/api/config/beatapi.ts b/src/routes/api/config/beatapi.ts index 9ddc503..6b477b0 100644 --- a/src/routes/api/config/beatapi.ts +++ b/src/routes/api/config/beatapi.ts @@ -16,8 +16,7 @@ import { */ async function GET({ request }: { request: Request }) { try { - const apiKey = - (await getConfig('BEATAPI_API_KEY')) || process.env.BEATAPI_API_KEY || ''; + const apiKey = (await getConfig('BEATAPI_API_KEY')) || ''; return respData({ baseUrl: DEFAULT_BEATAPI_BASE_URL, @@ -53,7 +52,6 @@ async function POST({ request }: { request: Request }) { const apiKey = next.BEATAPI_API_KEY || (await getConfig('BEATAPI_API_KEY')) || - process.env.BEATAPI_API_KEY || ''; if (!apiKey) return respErr('BeatAPI API key is required', 400); diff --git a/src/routes/api/config/storage.ts b/src/routes/api/config/storage.ts index fd7c009..033a40c 100644 --- a/src/routes/api/config/storage.ts +++ b/src/routes/api/config/storage.ts @@ -149,8 +149,7 @@ async function POST({ request }: { request: Request }) { const endpoint = next.R2_ENDPOINT || currentEndpoint || ''; const endpointPolicy = validateStorageEndpoint(endpoint, { - allowPrivate: - process.env.WORKSPACE_ALLOW_PRIVATE_STORAGE_ENDPOINTS === 'true', + allowPrivate: false, }); if (!endpointPolicy.ok) { return respErr(endpointPolicy.message, 400); diff --git a/src/routes/api/storage/upload.ts b/src/routes/api/storage/upload.ts index 7a0bf0b..f8d5b1a 100644 --- a/src/routes/api/storage/upload.ts +++ b/src/routes/api/storage/upload.ts @@ -135,8 +135,7 @@ async function loadCustomStorageConfig(): Promise { return null; } const endpointPolicy = validateStorageEndpoint(endpoint, { - allowPrivate: - process.env.WORKSPACE_ALLOW_PRIVATE_STORAGE_ENDPOINTS === 'true', + allowPrivate: false, }); if (!endpointPolicy.ok) throw new Error(endpointPolicy.message); return { @@ -150,44 +149,15 @@ async function loadCustomStorageConfig(): Promise { }; } -async function loadManagedStorageConfig(): Promise { - const config = { - region: process.env.BEATAPI_MANAGED_R2_REGION || 'auto', - endpoint: process.env.BEATAPI_MANAGED_R2_ENDPOINT || '', - accessKeyId: process.env.BEATAPI_MANAGED_R2_ACCESS_KEY_ID || '', - secretAccessKey: process.env.BEATAPI_MANAGED_R2_SECRET_ACCESS_KEY || '', - bucketName: process.env.BEATAPI_MANAGED_R2_BUCKET_NAME || '', - publicUrl: process.env.BEATAPI_MANAGED_R2_PUBLIC_URL || '', - forcePathStyle: - process.env.BEATAPI_MANAGED_R2_FORCE_PATH_STYLE !== 'false', - }; - if ( - !config.endpoint || - !config.accessKeyId || - !config.secretAccessKey || - !config.bucketName || - !config.publicUrl - ) { - return null; - } - return config; -} - async function uploadToS3Storage({ file, config, - provider, }: { file: File; config: StorageConfig | null; - provider: 'beatapi' | 's3'; }) { if (!config) { - throw new Error( - provider === 'beatapi' - ? 'Managed video input storage is not configured on this deployment. Use your own R2/S3 or configure BeatAPI managed R2.' - : 'Configure your own Cloudflare R2/S3 connection before generating with local references.' - ); + throw new Error('Configure your own R2/S3 connection before using custom upload storage.'); } const folder = file.type.startsWith('video/') ? 'workspace/videos' @@ -200,7 +170,7 @@ async function uploadToS3Storage({ contentType: file.type, folder, }); - return { ...result, provider } as const; + return { ...result, provider: 's3' as const }; } async function POST({ request }: { request: Request }) { @@ -332,18 +302,11 @@ async function POST({ request }: { request: Request }) { ? await uploadToS3Storage({ file, config: await loadCustomStorageConfig(), - provider: 's3', }) : null : canUseBeatApi ? await uploadToBeatApi(file) - : file.type.startsWith('video/') - ? await uploadToS3Storage({ - file, - config: await loadManagedStorageConfig(), - provider: 'beatapi', - }) - : null; + : null; if (!result) { await releaseGenerationUploadSlot({ intentId: authorizedIntent, diff --git a/src/routes/index.tsx b/src/routes/index.tsx index e65846e..706cc69 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from '@tanstack/react-router'; import { BeatApiProductHome } from '@/components/marketing/beatapi-product-home'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { getLocale } from '@/core/workspace-lib/shims/next-intl-server'; import { m } from '@/paraglide/messages.js'; @@ -16,7 +16,7 @@ export const Route = createFileRoute('/')({ name: 'description', content: m['product.home.metaDescription']({}, { locale }), }, - { property: 'og:url', content: envConfigs.app_url }, + { property: 'og:url', content: appConfig.app_url }, ], }; }, diff --git a/src/routes/pricing.tsx b/src/routes/pricing.tsx index 90e3069..7467462 100644 --- a/src/routes/pricing.tsx +++ b/src/routes/pricing.tsx @@ -1,14 +1,14 @@ import { createFileRoute } from '@tanstack/react-router'; import { BeatApiPricingPage } from '@/components/pricing/beatapi-pricing-page'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { getLocale } from '@/core/workspace-lib/shims/next-intl-server'; export const Route = createFileRoute('/pricing')({ loader: () => ({ locale: getLocale() }), head: () => ({ meta: [ - { title: `Pricing · ${envConfigs.app_name}` }, + { title: `Pricing · ${appConfig.app_name}` }, { name: 'description', content: diff --git a/src/routes/robots[.]txt.ts b/src/routes/robots[.]txt.ts index 5b1d8ac..a955c26 100644 --- a/src/routes/robots[.]txt.ts +++ b/src/routes/robots[.]txt.ts @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; export const Route = createFileRoute('/robots.txt')({ server: { @@ -12,7 +12,7 @@ export const Route = createFileRoute('/robots.txt')({ 'Disallow: /api/', 'Disallow: /*?*', '', - `Sitemap: ${envConfigs.app_url}/sitemap.xml`, + `Sitemap: ${appConfig.app_url}/sitemap.xml`, '', ].join('\n'); return new Response(body, { diff --git a/src/routes/sitemap[.]xml.ts b/src/routes/sitemap[.]xml.ts index cf1ac6e..7fffd8b 100644 --- a/src/routes/sitemap[.]xml.ts +++ b/src/routes/sitemap[.]xml.ts @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router'; -import { envConfigs } from '@/config'; +import { appConfig } from '@/config'; import { baseLocale, locales, localizeUrl } from '@/paraglide/runtime.js'; const STATIC_PATHS = [ @@ -17,7 +17,7 @@ type Entry = { }; function urlFor(path: string, locale: string): string { - return localizeUrl(`${envConfigs.app_url}${path || '/'}`, { + return localizeUrl(`${appConfig.app_url}${path || '/'}`, { locale: locale as (typeof locales)[number], }).href; } diff --git a/src/server.ts b/src/server.ts index da36125..33a3f13 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,32 +1,9 @@ import handler from '@tanstack/react-start/server-entry'; -import { runDueGenerationStatusPasses } from './core/effects/server-poller'; -import { cleanupStaleGenerations } from './core/effects/stale-generations'; import { getWwwRedirectLocation } from './lib/canonical-url'; import { normalizeProjectAssetMediaRequest } from './lib/project-asset-media-request'; import { paraglideMiddleware } from './paraglide/server.js'; -// On Cloudflare Workers, stash the binding env (D1, ASSETS, …) on globalThis -// so synchronous code paths (e.g. the db() singleton with DATABASE_PROVIDER=d1) -// can reach bindings without threading the request context through every call. -// The specifier is kept non-literal so bundlers leave the import to runtime; -// outside workerd the import rejects and we just move on. -const CF_WORKERS_MODULE = 'cloudflare:workers'; -let cfEnvPromise: Promise | null = null; - -function ensureCloudflareEnv(): Promise { - if (!cfEnvPromise) { - cfEnvPromise = import(/* @vite-ignore */ CF_WORKERS_MODULE) - .then((mod) => { - (globalThis as any).__CF_ENV__ = mod.env; - }) - .catch(() => { - // Not running on Cloudflare Workers — nothing to stash. - }); - } - return cfEnvPromise; -} - // Custom server entry — wraps every request in Paraglide's middleware so // getLocale() resolves per-request (AsyncLocalStorage) during SSR. export default { @@ -41,32 +18,7 @@ export default { }); } - await ensureCloudflareEnv(); const routedRequest = normalizeProjectAssetMediaRequest(req); return paraglideMiddleware(routedRequest, () => handler.fetch(routedRequest)); }, - async scheduled( - _controller: unknown, - _env: unknown, - ctx?: { waitUntil?: (promise: Promise) => void } - ): Promise { - const task = (async () => { - await ensureCloudflareEnv(); - const [statusResult, cleanupResult] = await Promise.all([ - runDueGenerationStatusPasses({ limit: 25 }), - cleanupStaleGenerations(), - ]); - console.log('effects scheduled pass complete', { - statusResult, - cleanupResult, - }); - })(); - - if (ctx?.waitUntil) { - ctx.waitUntil(task); - return; - } - - await task; - }, }; diff --git a/src/styles/globals.css b/src/styles/globals.css index c0d7df7..33ab82a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -26,7 +26,7 @@ button[aria-disabled="true"] { /* * Shared design tokens for BeatAPI dark product surfaces (public shell, home, - * pricing, projects, auth). One accent, three text levels, two radii — + * pricing, and projects). One accent, three text levels, two radii — * components should reference these instead of inventing near-identical * greys/oranges. */ @@ -608,37 +608,12 @@ body { @apply overscroll-none bg-background text-foreground; } -.text-gradient_indigo-purple { - background: linear-gradient(90deg, #6366f1 0%, rgb(168 85 247 / 0.8) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - /* https://github.com/shadcn-ui/ui/issues/4227#issuecomment-2438290165 */ html body[data-scroll-locked] { overflow: visible !important; margin-right: 0 !important; } -/* Fix for Fumadocs empty banner appearing in Cloudflare Worker */ -/* This targets the specific banner issue where empty:hidden doesn't work properly */ -div[class*="border-t"][class*="bg-fd-secondary"]:not(:has(*)):not( - [data-content] - ) { - display: none !important; -} - -/* Fallback for banners with the exact classes found in the issue */ -.border-t.bg-fd-secondary\/50.p-3:empty { - display: none !important; -} - -/* Additional safety for any empty Fumadocs banner */ -[class*="fd-secondary"]:empty:not([data-banner-content]) { - display: none !important; -} - body.modal-open { overflow: hidden; } diff --git a/vite.config.ts b/vite.config.ts index 0786d98..81c7ff6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,15 +7,8 @@ import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; import { paraglideCompilerOptions } from './src/config/paraglide'; -import { loadEnvFiles } from './src/lib/env'; import { shouldNormalizeProjectAssetMediaRequest } from './src/lib/project-asset-media-request'; -// Populate process.env from .env.local / .env.{NODE_ENV} / .env for the -// dev server and build process (Vite only exposes VITE_* via import.meta.env; -// server code reads secrets from process.env). In production, env comes -// from the actual host/container environment. -loadEnvFiles(); - export default defineConfig({ server: { host: '127.0.0.1', diff --git a/wrangler.example.jsonc b/wrangler.example.jsonc deleted file mode 100644 index 5f3fcdd..0000000 --- a/wrangler.example.jsonc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "name": "beatdesign", - "compatibility_date": "2026-06-23", - "compatibility_flags": ["nodejs_compat"], - "observability": { "enabled": true }, - "triggers": { "crons": ["*/2 * * * *"] }, - "d1_databases": [ - { - "binding": "DB", - "database_name": "beatdesign", - "database_id": "replace-with-your-d1-database-id" - } - ], - "vars": { - "DATABASE_PROVIDER": "d1", - "VITE_APP_URL": "https://your-domain.com", - "VITE_APP_NAME": "BeatDesign", - "WORKSPACE_STORAGE_MODE": "beatapi", - "EFFECTS_GENERATION_TIMEOUT_MS": "1800000", - "EFFECTS_POLL_INTERVAL_MS": "20000" - } -}