feat: dockerfile (node runtime), startup migration, README, runtime env config
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.git
|
||||||
|
.data
|
||||||
|
tests
|
||||||
|
docs
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# --- build ---
|
||||||
|
FROM oven/bun:1.3 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
# Install deps first (vendored @kuns/zitadel-auth must be present for the file: dependency).
|
||||||
|
COPY package.json bun.lock* ./
|
||||||
|
COPY vendor ./vendor
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
COPY . .
|
||||||
|
# Public (non-secret) OIDC config is baked into the client bundle at build time.
|
||||||
|
# Override with --build-arg if the Zitadel app/project ids change.
|
||||||
|
ARG NUXT_PUBLIC_ZITADEL_ISSUER=https://auth.kuns.dev
|
||||||
|
ARG NUXT_PUBLIC_ZITADEL_CLIENT_ID=376787352019861775
|
||||||
|
ARG NUXT_PUBLIC_ZITADEL_PROJECT_ID=376787351902355727
|
||||||
|
ENV NUXT_PUBLIC_ZITADEL_ISSUER=$NUXT_PUBLIC_ZITADEL_ISSUER \
|
||||||
|
NUXT_PUBLIC_ZITADEL_CLIENT_ID=$NUXT_PUBLIC_ZITADEL_CLIENT_ID \
|
||||||
|
NUXT_PUBLIC_ZITADEL_PROJECT_ID=$NUXT_PUBLIC_ZITADEL_PROJECT_ID
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# --- run ---
|
||||||
|
# Nitro's node-server output is traced for Node's export conditions, so run it on Node
|
||||||
|
# (not Bun, whose `bun` export condition resolves to files Nitro didn't copy).
|
||||||
|
FROM node:22-slim AS run
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
# The Nitro output bundles everything it needs (incl. postgres + jose + the migrate plugin).
|
||||||
|
COPY --from=build /app/.output ./.output
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "./.output/server/index.mjs"]
|
||||||
117
README.md
Normal file
117
README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# ClaudeDo Online Inbox
|
||||||
|
|
||||||
|
Optional online mirror of the [ClaudeDo](https://github.com/) desktop app's **Idle** task
|
||||||
|
backlog. Lets the single owner view their lists and jot new tasks from a phone or browser.
|
||||||
|
The desktop app (local, .NET/SQLite) remains the source of truth; this service only
|
||||||
|
stores/serves data — it never executes tasks.
|
||||||
|
|
||||||
|
**Live:** https://claudedo.kuns.dev · **API base:** `https://claudedo.kuns.dev/api`
|
||||||
|
|
||||||
|
## Governing rule
|
||||||
|
|
||||||
|
The online store mirrors **exactly** the desktop's Idle tasks. A task exists online only
|
||||||
|
while it is Idle; queuing it on the desktop deletes it here. Running/Done/Failed/review
|
||||||
|
tasks never appear online.
|
||||||
|
|
||||||
|
- **Lists** — desktop → online only (full-replace catalog).
|
||||||
|
- **Idle tasks** — two-way creation: the desktop pushes its Idle tasks (PUT) and pulls
|
||||||
|
web-created ones (`consumed=false` → import → consume).
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
Nuxt 3 (Vue 3, TypeScript, Bun) running as an SPA (`ssr: false`) with Nitro server routes as
|
||||||
|
the API, on shared PostgreSQL, behind Zitadel auth, deployed via Coolify.
|
||||||
|
|
||||||
|
- Web client login: [`@kuns/zitadel-auth`](../kuns-zitadel) (vendored under `vendor/`).
|
||||||
|
- API token validation: `jose` against Zitadel JWKS.
|
||||||
|
- DB access: `postgres` (postgres.js), parameterized queries only.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Every `/api/**` route requires a valid Zitadel **access token** (`Authorization: Bearer …`)
|
||||||
|
belonging to the owner. Missing/invalid/expired → `401`. No anonymous access.
|
||||||
|
|
||||||
|
| Method & path | Caller | Behaviour |
|
||||||
|
|---|---|---|
|
||||||
|
| `PUT /api/lists` | desktop | Body `[{id,name}]` = full catalog. Upserts all, deletes lists not in payload (cascades tasks). → 200 |
|
||||||
|
| `GET /api/lists` | web | → 200 `[{id,name}]` |
|
||||||
|
| `GET /api/lists/{id}/tasks` | web | → 200 tasks for the list; 404 if unknown |
|
||||||
|
| `POST /api/tasks` | web | Body `{title, description?, listId}`. Server-generated GUID, `source=web`, `consumed=false`. 404 if listId unknown. → 201 with the created task |
|
||||||
|
| `PUT /api/tasks/{id}` | desktop | Body `{listId, title, description?}`. Idempotent upsert (`source=desktop` on insert). → 201 (new) / 200 (existing) |
|
||||||
|
| `DELETE /api/tasks/{id}` | desktop | Idempotent. → 204 (even if absent) |
|
||||||
|
| `GET /api/tasks?consumed=false` | desktop | → 200 `[{id, listId, title, description, createdAt}]` web tasks not yet imported |
|
||||||
|
| `POST /api/tasks/{id}/consume` | desktop | Sets `consumed=true`. Idempotent. → 200; 404 if unknown |
|
||||||
|
|
||||||
|
Task ids are a **shared GUID space**: web-created ids are reused verbatim by the desktop on
|
||||||
|
import; all task writes are idempotent upserts keyed on `id`.
|
||||||
|
|
||||||
|
## Zitadel configuration (for the desktop client)
|
||||||
|
|
||||||
|
Both the desktop and the web client authenticate as the **same Zitadel user** (the owner).
|
||||||
|
Project **ClaudeDo** has two PKCE apps (both issue JWT access tokens):
|
||||||
|
|
||||||
|
| | Web | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| Client id | `376787352019861775` | `376787352137302287` |
|
||||||
|
| App type | User-Agent (SPA) | Native |
|
||||||
|
| Auth method | PKCE (none) | PKCE (none) |
|
||||||
|
| Grants | authorization_code | authorization_code, refresh_token |
|
||||||
|
| Redirect | `https://claudedo.kuns.dev/auth/callback` | `http://localhost:8765/callback`, `http://127.0.0.1:8765/callback` |
|
||||||
|
|
||||||
|
**Desktop OnlineInbox settings:**
|
||||||
|
|
||||||
|
- Issuer: `https://auth.kuns.dev`
|
||||||
|
- Client id: `376787352137302287`
|
||||||
|
- Scopes: `openid profile email offline_access urn:zitadel:iam:org:project:id:376787351902355727:aud`
|
||||||
|
- `offline_access` → refresh token for headless re-auth.
|
||||||
|
- the `…:aud` scope puts the project id into the token's `aud` so the API validates it.
|
||||||
|
- Flow: Authorization Code + PKCE on a loopback redirect for the initial interactive login,
|
||||||
|
then refresh-token grant headlessly. Store the refresh token securely.
|
||||||
|
- API base URL: `https://claudedo.kuns.dev/api`
|
||||||
|
|
||||||
|
Project id: `376787351902355727`. Owner user id (`sub`): `365090688972947729`
|
||||||
|
(`mika@kuns.dev`).
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Server-only values are read from `process.env` at runtime (set them in Coolify):
|
||||||
|
|
||||||
|
| Var | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `DATABASE_URL` | `postgres://mika:…@l8kogcggsc80sgcgk8kswww4:5432/claudedo` (shared PG, internal host) |
|
||||||
|
| `ZITADEL_ISSUER` | `https://auth.kuns.dev` |
|
||||||
|
| `ZITADEL_AUDIENCE` | accepted audiences (CSV): web id, desktop id, project id |
|
||||||
|
| `ALLOWED_USER_IDS` | owner `sub` allowlist (CSV) |
|
||||||
|
| `WEB_ORIGIN` | CORS allowed origin (`https://claudedo.kuns.dev`) |
|
||||||
|
|
||||||
|
Public web-client config is **baked at build time** (non-secret) via Dockerfile build args
|
||||||
|
(`NUXT_PUBLIC_ZITADEL_ISSUER`, `NUXT_PUBLIC_ZITADEL_CLIENT_ID`,
|
||||||
|
`NUXT_PUBLIC_ZITADEL_PROJECT_ID`); override with `--build-arg` if the ids change.
|
||||||
|
|
||||||
|
See `.env.example` for local development.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
# Point at a local/tunnelled Postgres and apply the schema:
|
||||||
|
DATABASE_URL=postgres://… bun run migrate
|
||||||
|
DATABASE_URL=postgres://… bun run dev # http://localhost:3000
|
||||||
|
|
||||||
|
# Tests (need a Postgres test DB):
|
||||||
|
DATABASE_URL=postgres://…/claudedo_test bun run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Local API smoke without a Zitadel token: set `AUTH_DEV_BYPASS=1` (dev mode only — it is
|
||||||
|
dead-code-eliminated from production builds).
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
DB `claudedo` on the shared PostgreSQL instance. Schema (`server/utils/schema.ts`) is applied
|
||||||
|
idempotently on server startup by `server/plugins/migrate.ts` (advisory-locked) and via
|
||||||
|
`bun run migrate`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Coolify builds the `Dockerfile` (Bun build → Node runtime) on push to Gitea; Traefik routes
|
||||||
|
`claudedo.kuns.dev` with automatic SSL.
|
||||||
12
bun.lock
12
bun.lock
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "claudedo-online",
|
"name": "claudedo-online",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kuns/zitadel-auth": "file:../kuns-zitadel/js",
|
"@kuns/zitadel-auth": "file:./vendor/zitadel-auth",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"nuxt": "^4.4.8",
|
"nuxt": "^4.4.8",
|
||||||
"oidc-client-ts": "^3.5.0",
|
"oidc-client-ts": "^3.5.0",
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
"@kuns/zitadel-auth": ["@kuns/zitadel-auth@file:../kuns-zitadel/js", { "dependencies": { "oidc-client-ts": "^3.5.0" }, "devDependencies": { "@types/react": "^19.2.14", "jsdom": "^29.0.1", "tsup": "^8.0.0", "typescript": "^5.5.0", "vitest": "^3.0.0", "vue": "^3.0.0", "vue-router": "^4.0.0" }, "peerDependencies": { "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/router": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-router-dom": "^6.0.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0", "vue-router": "^4.0.0" }, "optionalPeers": ["@angular/core", "@angular/router", "react", "react-router-dom", "svelte", "vue", "vue-router"] }],
|
"@kuns/zitadel-auth": ["@kuns/zitadel-auth@file:vendor/zitadel-auth", { "dependencies": { "oidc-client-ts": "^3.5.0" }, "devDependencies": { "@types/react": "^19.2.14", "jsdom": "^29.0.1", "tsup": "^8.0.0", "typescript": "^5.5.0", "vitest": "^3.0.0", "vue": "^3.0.0", "vue-router": "^4.0.0" }, "peerDependencies": { "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/router": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-router-dom": "^6.0.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0", "vue-router": "^4.0.0" }, "optionalPeers": ["@angular/core", "@angular/router", "react", "react-router-dom", "svelte", "vue", "vue-router"] }],
|
||||||
|
|
||||||
"@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
|
"@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
|
||||||
|
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||||
|
|
||||||
@@ -1466,6 +1466,8 @@
|
|||||||
|
|
||||||
"@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
|
"@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
|
||||||
|
|
||||||
|
"@bomb.sh/tab/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
"@dxup/nuxt/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"@dxup/nuxt/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
@@ -1602,9 +1604,9 @@
|
|||||||
|
|
||||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
"tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ export default defineNuxtConfig({
|
|||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
// Single-user private inbox: no SSR/SEO needs; SPA so @kuns/zitadel-auth runs in-browser.
|
// Single-user private inbox: no SSR/SEO needs; SPA so @kuns/zitadel-auth runs in-browser.
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
// server-only
|
// Server-only config is read directly from process.env at runtime (see server/utils),
|
||||||
databaseUrl: process.env.DATABASE_URL,
|
// so it is NOT declared here (Nuxt only overrides runtimeConfig via the NUXT_ prefix).
|
||||||
zitadelIssuer: process.env.ZITADEL_ISSUER || "https://auth.kuns.dev",
|
|
||||||
zitadelAudience: process.env.ZITADEL_AUDIENCE || "",
|
|
||||||
allowedUserIds: process.env.ALLOWED_USER_IDS || "",
|
|
||||||
webOrigin: process.env.WEB_ORIGIN || "",
|
|
||||||
public: {
|
public: {
|
||||||
zitadelIssuer: process.env.NUXT_PUBLIC_ZITADEL_ISSUER || "https://auth.kuns.dev",
|
zitadelIssuer: process.env.NUXT_PUBLIC_ZITADEL_ISSUER || "https://auth.kuns.dev",
|
||||||
zitadelClientId: process.env.NUXT_PUBLIC_ZITADEL_CLIENT_ID || "",
|
zitadelClientId: process.env.NUXT_PUBLIC_ZITADEL_CLIENT_ID || "",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"provision:zitadel": "tsx scripts/provision-zitadel.ts"
|
"provision:zitadel": "tsx scripts/provision-zitadel.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kuns/zitadel-auth": "file:../kuns-zitadel/js",
|
"@kuns/zitadel-auth": "file:./vendor/zitadel-auth",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"nuxt": "^4.4.8",
|
"nuxt": "^4.4.8",
|
||||||
"oidc-client-ts": "^3.5.0",
|
"oidc-client-ts": "^3.5.0",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// Idempotent migration runner. Run via `bun run migrate` / on container start.
|
// CLI migration runner (local / CI). Run via `bun run migrate`.
|
||||||
import { readFileSync } from "node:fs";
|
// Production migrations run automatically via server/plugins/migrate.ts on startup.
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
|
import { INIT_SQL } from "../utils/schema";
|
||||||
|
|
||||||
const url = process.env.DATABASE_URL;
|
const url = process.env.DATABASE_URL;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -10,14 +9,10 @@ if (!url) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const sql = postgres(url, { max: 1 });
|
const sql = postgres(url, { max: 1 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ddl = readFileSync(join(here, "migrations", "0001_init.sql"), "utf8");
|
await sql.unsafe(INIT_SQL);
|
||||||
// Trusted local DDL file, not user input.
|
console.log("schema applied");
|
||||||
await sql.unsafe(ddl);
|
|
||||||
console.log("migration 0001_init applied");
|
|
||||||
} finally {
|
} finally {
|
||||||
await sql.end();
|
await sql.end();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
if (!getRequestURL(event).pathname.startsWith("/api/")) return;
|
if (!getRequestURL(event).pathname.startsWith("/api/")) return;
|
||||||
|
|
||||||
const origin = useRuntimeConfig().webOrigin;
|
const origin = process.env.WEB_ORIGIN || "";
|
||||||
if (origin) {
|
if (origin) {
|
||||||
setResponseHeader(event, "Access-Control-Allow-Origin", origin);
|
setResponseHeader(event, "Access-Control-Allow-Origin", origin);
|
||||||
setResponseHeader(event, "Vary", "Origin");
|
setResponseHeader(event, "Vary", "Origin");
|
||||||
|
|||||||
17
server/plugins/migrate.ts
Normal file
17
server/plugins/migrate.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { INIT_SQL } from "../utils/schema";
|
||||||
|
|
||||||
|
// Apply the schema idempotently on server startup. An advisory lock prevents concurrent
|
||||||
|
// instances from racing on CREATE TABLE. Idempotent CREATE ... IF NOT EXISTS means this is
|
||||||
|
// safe to run on every boot.
|
||||||
|
export default defineNitroPlugin(async () => {
|
||||||
|
try {
|
||||||
|
const sql = getSql();
|
||||||
|
await sql.begin(async (tx) => {
|
||||||
|
await tx`select pg_advisory_xact_lock(871042)`;
|
||||||
|
await tx.unsafe(INIT_SQL);
|
||||||
|
});
|
||||||
|
console.log("[migrate] schema ensured");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[migrate] failed:", (e as Error).message);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -45,14 +45,13 @@ function splitCsv(v: unknown): string[] {
|
|||||||
|
|
||||||
let _cached: ReturnType<typeof makeVerifier> | null = null;
|
let _cached: ReturnType<typeof makeVerifier> | null = null;
|
||||||
|
|
||||||
/** Process-wide verifier built from runtime config (Nitro server context). */
|
/** Process-wide verifier built from environment (read at runtime, not baked at build). */
|
||||||
export function getVerifier() {
|
export function getVerifier() {
|
||||||
if (!_cached) {
|
if (!_cached) {
|
||||||
const c = useRuntimeConfig();
|
|
||||||
_cached = makeVerifier({
|
_cached = makeVerifier({
|
||||||
issuer: c.zitadelIssuer,
|
issuer: process.env.ZITADEL_ISSUER || "https://auth.kuns.dev",
|
||||||
audiences: splitCsv(c.zitadelAudience),
|
audiences: splitCsv(process.env.ZITADEL_AUDIENCE),
|
||||||
allowedSubs: splitCsv(c.allowedUserIds),
|
allowedSubs: splitCsv(process.env.ALLOWED_USER_IDS),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return _cached;
|
return _cached;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- ClaudeDo Online Inbox — initial schema.
|
// Canonical schema for the Online Inbox. Single source of truth, applied idempotently by
|
||||||
-- Mirrors the desktop's Idle task backlog. Idempotent.
|
// the Nitro startup plugin (server/plugins/migrate.ts) and the CLI (server/db/migrate.ts).
|
||||||
|
export const INIT_SQL = `
|
||||||
create table if not exists lists (
|
create table if not exists lists (
|
||||||
id text primary key, -- GUID supplied by the desktop, reused verbatim
|
id text primary key, -- GUID supplied by the desktop, reused verbatim
|
||||||
name text not null,
|
name text not null,
|
||||||
@@ -20,3 +20,4 @@ create table if not exists tasks (
|
|||||||
|
|
||||||
create index if not exists idx_tasks_list_id on tasks(list_id);
|
create index if not exists idx_tasks_list_id on tasks(list_id);
|
||||||
create index if not exists idx_tasks_unconsumed on tasks(consumed) where consumed = false;
|
create index if not exists idx_tasks_unconsumed on tasks(consumed) where consumed = false;
|
||||||
|
`;
|
||||||
88
vendor/zitadel-auth/package.json
vendored
Normal file
88
vendor/zitadel-auth/package.json
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"name": "@kuns/zitadel-auth",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Framework-agnostic Zitadel OIDC auth for kuns.dev frontends",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./vue": {
|
||||||
|
"import": "./dist/vue.js",
|
||||||
|
"require": "./dist/vue.cjs",
|
||||||
|
"types": "./dist/vue.d.ts"
|
||||||
|
},
|
||||||
|
"./react": {
|
||||||
|
"import": "./dist/react.js",
|
||||||
|
"require": "./dist/react.cjs",
|
||||||
|
"types": "./dist/react.d.ts"
|
||||||
|
},
|
||||||
|
"./svelte": {
|
||||||
|
"import": "./dist/svelte.js",
|
||||||
|
"require": "./dist/svelte.cjs",
|
||||||
|
"types": "./dist/svelte.d.ts"
|
||||||
|
},
|
||||||
|
"./angular": {
|
||||||
|
"import": "./dist/angular.js",
|
||||||
|
"require": "./dist/angular.cjs",
|
||||||
|
"types": "./dist/angular.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": ["dist"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"oidc-client-ts": "^3.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
||||||
|
"svelte": "^4.0.0 || ^5.0.0",
|
||||||
|
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"@angular/router": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue-router": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-router-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@angular/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@angular/router": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"jsdom": "^29.0.1",
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vitest": "^3.0.0",
|
||||||
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0"
|
||||||
|
},
|
||||||
|
"author": "kuns.dev",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user