diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a090307 --- /dev/null +++ b/.env.example @@ -0,0 +1,67 @@ +# Copy to `.env` and fill in. + +# --------------------------------------------------------------------------- +# OG image URL signing secret. nuxt-og-image renders OG images on demand at +# runtime; this HMAC secret signs the generated URLs so callers can't craft +# arbitrary image-generation requests against the endpoint (which would burn +# CPU and bandwidth). +# +# Generate with: npx nuxt-og-image generate-secret +# (or any 32-byte hex string — the CLI just calls randomBytes(32).toString('hex')) +# --------------------------------------------------------------------------- +NUXT_OG_IMAGE_SECRET=<32-byte hex string> + +# --------------------------------------------------------------------------- +# Public URL the app is reachable at. For local dev this is the loopback host +# (note: 127.0.0.1, not localhost — required by the AT Proto OAuth spec for +# the synthetic dev `client_id`). +# --------------------------------------------------------------------------- +NUXT_PUBLIC_URL=http://127.0.0.1:3000 + +# --------------------------------------------------------------------------- +# Database. Get a connection string from https://neon.tech (free tier is fine). +# Copy the "pooled" connection string for serverless workloads. +# --------------------------------------------------------------------------- +NUXT_DATABASE_URL=postgres://user:password@host.neon.tech/dbname?sslmode=require + +# --------------------------------------------------------------------------- +# AT Proto OAuth client signing key (ES256 private JWK). +# Generate with: pnpm gen:jwk +# Paste the full JSON object on a single line below. +# --------------------------------------------------------------------------- +NUXT_ATPROTO_PRIVATE_JWK={"kty":"EC","kid":"...","crv":"P-256","x":"...","y":"...","d":"..."} + +# --------------------------------------------------------------------------- +# Application encryption key (KEK) — wraps SSH private keys and AT Proto +# session blobs at rest. Base64-encoded 32 bytes. +# Generate with: pnpm gen:encryption-key +# --------------------------------------------------------------------------- +NUXT_ENCRYPTION_KEY= + +# --------------------------------------------------------------------------- +# GitHub App credentials. After creating the App at +# https://github.com/settings/apps/new, copy: +# - The numeric App ID (top of the App settings page). +# - The webhook secret you set during creation. +# - A generated private key (.pem). On Vercel, store with literal "\n" in +# place of newlines; locally, keep the real newlines. +# --------------------------------------------------------------------------- +NUXT_GITHUB_APP_ID= +NUXT_GITHUB_WEBHOOK_SECRET= +NUXT_GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- +... +-----END RSA PRIVATE KEY----- +" + +# --------------------------------------------------------------------------- +# Cron secret — protects the worker tick endpoint (`/api/jobs/run`) from +# unauthenticated callers. In prod, Vercel Cron sends this automatically; +# locally, `pnpm jobs:tick` reads it from this env var. +# Generate with: pnpm gen:cron-secret +# --------------------------------------------------------------------------- +NUXT_CRON_SECRET= + +# Optional: per-invocation worker time budget in milliseconds. +# Default 25_000. Set lower in dev so `pnpm jobs:tick` returns sooner when +# the queue is empty. +# NUXT_WORKER_BUDGET_MS=5000 diff --git a/package.json b/package.json index 42400cf..257c712 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "gen:jwk": "node scripts/gen-jwk.ts", "gen:encryption-key": "node -e \"console.log(require('node:crypto').randomBytes(32).toString('base64'))\"", "gen:cron-secret": "node -e \"console.log(require('node:crypto').randomBytes(32).toString('base64url'))\"", + "jobs:tick": "node --env-file=.env scripts/jobs-tick.ts", "test:types": "vue-tsc -b --noEmit", "test": "vp test", "test:watch": "vp test watch", diff --git a/scripts/jobs-tick.ts b/scripts/jobs-tick.ts new file mode 100644 index 0000000..8360d76 --- /dev/null +++ b/scripts/jobs-tick.ts @@ -0,0 +1,33 @@ +/** + * Trigger the worker tick endpoint locally. + * + * Reads `NUXT_CRON_SECRET` from `.env` (loaded by Node's --env-file when run + * via `pnpm jobs:tick`) and POSTs to /api/jobs/run with the matching + * `Authorization: Bearer …` header. + * + * In production, Vercel Cron does this automatically every minute; this + * script is the local equivalent. + */ + +import process from 'node:process' + +const url = process.env.JOBS_TICK_URL ?? 'http://127.0.0.1:3000/api/jobs/run' +const secret = process.env.NUXT_CRON_SECRET + +if (!secret) { + console.error('NUXT_CRON_SECRET not set; copy from .env or run `pnpm gen:cron-secret`') + process.exit(1) +} + +const response = await fetch(url, { + method: 'POST', + headers: { authorization: `Bearer ${secret}` }, +}) + +const body = await response.text() +if (!response.ok) { + console.error(`worker tick failed with ${response.status}: ${body}`) + process.exit(1) +} + +console.log(body)