diff --git a/CLAUDE.md b/CLAUDE.md index ca837dab2..af40cd5b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,7 @@ # VitNode - Always write and run unit tests in vitest for all new features and bug fixes. +- Don't write unit tests if app doesn't have configured vitest. - Don't use `React.FC` for defining React components. Instead, use the arrow function syntax. - Don't use `any` type in TypeScript and use `unknown` as less as possible. - Use `AutoForm` for forms instead of manually creating form components. diff --git a/apps/api/.env.example b/apps/api/.env.example index 946f602f5..1f17baca5 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -20,7 +20,6 @@ CRON_SECRET=your-secure-cron-secret-key # S3_PUBLIC_URL=https://cdn.example.com # optional CDN base # --- Supabase Storage (@vitnode/supabase-storage) --- # SUPABASE_URL=https://your-project.supabase.co -# SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # SUPABASE_STORAGE_BUCKET=your-bucket # === Docker Database Postgres === diff --git a/apps/api/package.json b/apps/api/package.json index 9107dfa8c..0bac9d452 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -18,6 +18,7 @@ "@hono/zod-validator": "^0.8.0", "@vitnode/core": "workspace:*", "@vitnode/node-cron": "workspace:*", + "@vitnode/supabase-storage": "workspace:*", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", "hono": "^4.12.27", diff --git a/apps/api/src/vitnode.api.config.ts b/apps/api/src/vitnode.api.config.ts index 095e71621..8acefffba 100644 --- a/apps/api/src/vitnode.api.config.ts +++ b/apps/api/src/vitnode.api.config.ts @@ -1,10 +1,10 @@ import { blogApiPlugin } from "@vitnode/blog/config.api"; -import { LocalStorageAdapter } from "@vitnode/core/api/adapters/storage/local"; +// import { LocalStorageAdapter } from "@vitnode/core/api/adapters/storage/local"; import { buildApiConfig } from "@vitnode/core/vitnode.config"; import { NodeCronAdapter } from "@vitnode/node-cron"; import { NodemailerEmailAdapter } from "@vitnode/nodemailer"; // import { S3StorageAdapter } from "@vitnode/s3"; -// import { SupabaseStorageAdapter } from "@vitnode/supabase-storage"; +import { SupabaseStorageAdapter } from "@vitnode/supabase-storage"; import { config } from "dotenv"; import { drizzle } from "drizzle-orm/postgres-js"; @@ -40,7 +40,7 @@ export const vitNodeApiConfig = buildApiConfig({ }, storage: { // Zero-config: writes to `public/uploads` and serves via Hono static files. - adapter: LocalStorageAdapter(), + // adapter: LocalStorageAdapter(), // Re-encode uploaded images with sharp to shrink them before storing. image: { quality: 85, @@ -53,11 +53,11 @@ export const vitNodeApiConfig = buildApiConfig({ // endpoint: process.env.S3_ENDPOINT, // Cloudflare R2 endpoint // publicUrl: process.env.S3_PUBLIC_URL, // }), - // adapter: SupabaseStorageAdapter({ - // url: process.env.SUPABASE_URL, - // serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY, - // bucket: process.env.SUPABASE_STORAGE_BUCKET, - // }), + adapter: SupabaseStorageAdapter({ + url: process.env.SUPABASE_URL, + secretKey: process.env.SUPABASE_SECRET_KEY, + bucket: process.env.SUPABASE_STORAGE_BUCKET, + }), }, metadata: { title: "VitNode API", diff --git a/apps/docs/content/docs/dev/storage/supabase.mdx b/apps/docs/content/docs/dev/storage/supabase.mdx index 1915043d3..c85bcc60d 100644 --- a/apps/docs/content/docs/dev/storage/supabase.mdx +++ b/apps/docs/content/docs/dev/storage/supabase.mdx @@ -3,7 +3,7 @@ title: Supabase Storage description: Store uploads in a Supabase Storage bucket. --- -| Cloud | Self-Hosted | Links | +| Cloud | Self-Hosted | Links | | ------------ | ------------ | ----------------------------------------------------------------- | | ✅ Supported | ✅ Supported | [NPM Package](https://www.npmjs.com/package/@supabase/storage-js) | @@ -45,7 +45,7 @@ export const vitNodeApiConfig = buildApiConfig({ // [!code ++:5] adapter: SupabaseStorageAdapter({ url: process.env.SUPABASE_URL, - serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY, + secretKey: process.env.SUPABASE_SECRET_KEY, bucket: process.env.SUPABASE_STORAGE_BUCKET, }), }, @@ -56,12 +56,13 @@ export const vitNodeApiConfig = buildApiConfig({ ### Environment variables -The **service role key** is required so the server can write to the bucket. Keep -it secret — never expose it to the client. +A **secret key** (`sb_secret_…`) is required so the server can write to the +bucket. Keep it secret — never expose it to the client. Create one under +**Project Settings → API Keys**. ```bash title=".env" SUPABASE_URL=https://your-project.supabase.co -SUPABASE_SERVICE_ROLE_KEY=your_service_role_key +SUPABASE_SECRET_KEY=sb_secret_... SUPABASE_STORAGE_BUCKET=your-bucket ``` @@ -83,8 +84,9 @@ import { TypeTable } from "fumadocs-ui/components/type-table"; description: "Your Supabase project URL.", type: "string", }, - serviceRoleKey: { - description: "Supabase service role key (server-side only).", + secretKey: { + description: + "Supabase secret key `sb_secret_…` (server-side only). Replaces the legacy service role key.", type: "string", }, bucket: { diff --git a/packages/s3/package.json b/packages/s3/package.json index 19db36a0f..9fe3bb54d 100644 --- a/packages/s3/package.json +++ b/packages/s3/package.json @@ -28,7 +28,9 @@ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json", "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@aws-sdk/client-s3": "^3.0.0" @@ -41,6 +43,7 @@ "concurrently": "^10.0.3", "eslint": "^10.5.0", "tsc-alias": "^1.8.17", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.9" } } diff --git a/packages/supabase-storage/package.json b/packages/supabase-storage/package.json index 0cb93a69c..b5c96e9ca 100644 --- a/packages/supabase-storage/package.json +++ b/packages/supabase-storage/package.json @@ -27,7 +27,9 @@ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json", "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@supabase/storage-js": "^2.0.0" @@ -40,6 +42,7 @@ "concurrently": "^10.0.3", "eslint": "^10.5.0", "tsc-alias": "^1.8.17", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.9" } } diff --git a/packages/supabase-storage/src/index.ts b/packages/supabase-storage/src/index.ts index 59465d734..f77212c78 100644 --- a/packages/supabase-storage/src/index.ts +++ b/packages/supabase-storage/src/index.ts @@ -6,25 +6,28 @@ import type { import { StorageClient } from "@supabase/storage-js"; +// Uses a Supabase secret key (`sb_secret_…`), the modern replacement for the +// legacy `service_role` key. A still-valid legacy key works too — the value is +// sent verbatim as the `apikey`/`Authorization` header. export const SupabaseStorageAdapter = ({ bucket = "", - serviceRoleKey = "", + secretKey = "", url = "", }: { bucket: string | undefined; - serviceRoleKey: string | undefined; + secretKey: string | undefined; url: string | undefined; }): StorageApiPlugin => { let client: StorageClient | undefined; const getClient = (): StorageClient => { - if (!(url && serviceRoleKey && bucket)) { + if (!(url && secretKey && bucket)) { throw new Error("Missing Supabase Storage configuration"); } client ??= new StorageClient(`${url.replace(/\/$/, "")}/storage/v1`, { - apikey: serviceRoleKey, - Authorization: `Bearer ${serviceRoleKey}`, + apikey: secretKey, + Authorization: `Bearer ${secretKey}`, }); return client; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 392774152..39493dd53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@vitnode/node-cron': specifier: workspace:* version: link:../../packages/node-cron + '@vitnode/supabase-storage': + specifier: workspace:* + version: link:../../packages/supabase-storage drizzle-kit: specifier: ^0.31.10 version: 0.31.10 @@ -457,6 +460,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.1)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/supabase-storage: dependencies: @@ -488,6 +494,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.1)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/vitnode: dependencies: @@ -12847,7 +12856,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.1)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.1)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/expect@4.1.9': dependencies: @@ -12866,6 +12875,14 @@ snapshots: optionalDependencies: vite: 8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 @@ -18061,7 +18078,6 @@ snapshots: jiti: 2.7.0 tsx: 4.22.4 yaml: 2.9.0 - optional: true vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.1)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: @@ -18093,6 +18109,36 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.1)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.2.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 26.0.1 + '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: