From b16cd9e2f73e1fe5991c2cd544125d52dfca801c Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 21 Jun 2026 23:22:05 -0500 Subject: [PATCH] Update stuff --- .gitea/workflows/build-next.yml | 2 +- apps/next/src/instrumentation-client.ts | 1 + apps/next/src/proxy.ts | 2 +- package.json | 10 ++++ packages/backend/convex/auth.ts | 45 +++++++++----- packages/backend/convex/diagnostics.ts | 34 +++++++++++ scripts/build-next-app | 4 +- scripts/export-env | 17 ++++-- scripts/sync-convex-env | 80 +++++++++++++++++++++---- 9 files changed, 161 insertions(+), 34 deletions(-) create mode 100644 packages/backend/convex/diagnostics.ts diff --git a/.gitea/workflows/build-next.yml b/.gitea/workflows/build-next.yml index 76694f9..e2cc93b 100644 --- a/.gitea/workflows/build-next.yml +++ b/.gitea/workflows/build-next.yml @@ -51,7 +51,7 @@ jobs: env_file="$(mktemp)" trap 'rm -f "$env_file"' EXIT printf '%s\n' "$DOTENV_PROD" > "$env_file" - CI_ENV_FILE="$env_file" ./scripts/build-next-app staging + CI_ENV_FILE="$env_file" ./scripts/build-next-app production - name: Tag and push image run: | docker tag spoon-next:latest git.gbrown.org/gib/spoon-next:${{ gitea.sha }} diff --git a/apps/next/src/instrumentation-client.ts b/apps/next/src/instrumentation-client.ts index e540cb9..1ffe376 100644 --- a/apps/next/src/instrumentation-client.ts +++ b/apps/next/src/instrumentation-client.ts @@ -4,6 +4,7 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: env.NEXT_PUBLIC_SENTRY_DSN, + tunnel: '/monitoring', integrations: [ Sentry.replayIntegration({ maskAllText: false, diff --git a/apps/next/src/proxy.ts b/apps/next/src/proxy.ts index c1c30b6..2d93ac3 100644 --- a/apps/next/src/proxy.ts +++ b/apps/next/src/proxy.ts @@ -30,7 +30,7 @@ export default convexAuthNextjsMiddleware( export const config = { matcher: [ - '/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + '/((?!_next/static|_next/image|favicon.ico|monitoring|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', '/((?!.*\\..*|_next).*)', '/', '/(api)(.*)', diff --git a/package.json b/package.json index 0643e4a..32e01ec 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,24 @@ "dev": "turbo run dev", "dev:tunnel": "turbo run dev:tunnel", "dev:next": "turbo run dev -F @spoon/next -F @spoon/backend", + "dev:next:staging": "INFISICAL_ENV=staging turbo run dev -F @spoon/next -F @spoon/backend", "dev:agent": "turbo run dev -F @spoon/agent-worker", + "dev:agent:staging": "INFISICAL_ENV=staging turbo run dev -F @spoon/agent-worker", "dev:next:web": "turbo run dev:web -F @spoon/next -F @spoon/backend", + "dev:next:web:staging": "INFISICAL_ENV=staging turbo run dev:web -F @spoon/next -F @spoon/backend", "dev:expo": "turbo run dev -F @spoon/expo -F @spoon/backend", + "dev:expo:staging": "INFISICAL_ENV=staging turbo run dev -F @spoon/expo -F @spoon/backend", "dev:backend": "turbo run dev -F @spoon/backend", + "dev:backend:staging": "INFISICAL_ENV=staging turbo run dev -F @spoon/backend", "dev:staging": "INFISICAL_ENV=staging turbo run dev -F @spoon/next -F @spoon/backend", "dev:expo:tunnel": "turbo run dev:tunnel -F @spoon/expo -F @spoon/backend", + "dev:expo:tunnel:staging": "INFISICAL_ENV=staging turbo run dev:tunnel -F @spoon/expo -F @spoon/backend", "codegen:convex": "bash scripts/convex-codegen", "sync:convex": "scripts/sync-convex-env ${INFISICAL_ENV:-dev}", + "sync:convex:staging": "scripts/sync-convex-env staging", + "sync:convex:production": "scripts/sync-convex-env production", + "sync:convex:prod": "scripts/sync-convex-env prod", + "auth:keys": "node scripts/generate-convex-auth-keys.mjs", "db:up": "bash scripts/db/up", "db:down": "bash scripts/db/down", "db:down:wipe": "bash scripts/db/down --wipe", diff --git a/packages/backend/convex/auth.ts b/packages/backend/convex/auth.ts index ca64e53..e214332 100644 --- a/packages/backend/convex/auth.ts +++ b/packages/backend/convex/auth.ts @@ -14,22 +14,37 @@ import { api } from './_generated/api'; import { action, mutation, query } from './_generated/server'; import { Password, validatePassword } from './custom/auth'; +const authProviders = [ + ...(process.env.AUTH_AUTHENTIK_ID && + process.env.AUTH_AUTHENTIK_SECRET && + process.env.AUTH_AUTHENTIK_ISSUER + ? [ + Authentik({ + allowDangerousEmailAccountLinking: true, + clientId: process.env.AUTH_AUTHENTIK_ID, + clientSecret: process.env.AUTH_AUTHENTIK_SECRET, + issuer: process.env.AUTH_AUTHENTIK_ISSUER, + }), + ] + : []), + ...((process.env.AUTH_GITHUB_ID ?? process.env.GITHUB_APP_CLIENT_ID) && + (process.env.AUTH_GITHUB_SECRET ?? process.env.GITHUB_APP_CLIENT_SECRET) + ? [ + GitHub({ + allowDangerousEmailAccountLinking: true, + clientId: + process.env.AUTH_GITHUB_ID ?? process.env.GITHUB_APP_CLIENT_ID, + clientSecret: + process.env.AUTH_GITHUB_SECRET ?? + process.env.GITHUB_APP_CLIENT_SECRET, + }), + ] + : []), + Password, +]; + export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ - providers: [ - Authentik({ - allowDangerousEmailAccountLinking: true, - clientId: process.env.AUTH_AUTHENTIK_ID, - clientSecret: process.env.AUTH_AUTHENTIK_SECRET, - issuer: process.env.AUTH_AUTHENTIK_ISSUER, - }), - GitHub({ - allowDangerousEmailAccountLinking: true, - clientId: process.env.AUTH_GITHUB_ID ?? process.env.GITHUB_APP_CLIENT_ID, - clientSecret: - process.env.AUTH_GITHUB_SECRET ?? process.env.GITHUB_APP_CLIENT_SECRET, - }), - Password, - ], + providers: authProviders, }); const getUserById = async ( diff --git a/packages/backend/convex/diagnostics.ts b/packages/backend/convex/diagnostics.ts new file mode 100644 index 0000000..4de7cee --- /dev/null +++ b/packages/backend/convex/diagnostics.ts @@ -0,0 +1,34 @@ +import { query } from './_generated/server'; + +const hasEnv = (name: string) => Boolean(process.env[name]?.trim()); + +export const envStatus = query({ + args: {}, + handler: () => ({ + auth: { + authentikId: hasEnv('AUTH_AUTHENTIK_ID'), + authentikSecret: hasEnv('AUTH_AUTHENTIK_SECRET'), + authentikIssuer: hasEnv('AUTH_AUTHENTIK_ISSUER'), + githubId: hasEnv('AUTH_GITHUB_ID') || hasEnv('GITHUB_APP_CLIENT_ID'), + githubSecret: + hasEnv('AUTH_GITHUB_SECRET') || hasEnv('GITHUB_APP_CLIENT_SECRET'), + jwtPrivateKey: hasEnv('JWT_PRIVATE_KEY'), + jwks: hasEnv('JWKS'), + siteUrl: hasEnv('SITE_URL'), + }, + githubApp: { + appId: hasEnv('GITHUB_APP_ID'), + privateKey: hasEnv('GITHUB_APP_PRIVATE_KEY'), + installationId: hasEnv('GITHUB_APP_INSTALLATION_ID'), + }, + email: { + useSendApiKey: hasEnv('USESEND_API_KEY'), + useSendUrl: hasEnv('USESEND_URL'), + useSendFromEmail: hasEnv('USESEND_FROM_EMAIL'), + }, + spoon: { + encryptionKey: hasEnv('SPOON_ENCRYPTION_KEY'), + workerToken: hasEnv('SPOON_WORKER_TOKEN'), + }, + }), +}); diff --git a/scripts/build-next-app b/scripts/build-next-app index 65d5dd8..33dbe9e 100755 --- a/scripts/build-next-app +++ b/scripts/build-next-app @@ -3,14 +3,16 @@ set -euo pipefail ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" ENVIRONMENT="${1:-staging}" -[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: build-next-app [dev|staging]" >&2; exit 2; } +[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging || "$ENVIRONMENT" == production || "$ENVIRONMENT" == prod ]] || { echo "usage: build-next-app [dev|staging|production|prod]" >&2; exit 2; } ENV_FILE="${CI_ENV_FILE:-}" cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; } trap cleanup EXIT if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then ENV_FILE="$(mktemp)"; sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"; fi if [[ -n "$ENV_FILE" ]]; then + bun dotenv -e "$ENV_FILE" -- bash "$ROOT_DIR/scripts/sync-convex-env" "$ENVIRONMENT" --from-current-env bun dotenv -e "$ENV_FILE" -- bash "$ROOT_DIR/scripts/convex-codegen" else + bash "$ROOT_DIR/scripts/sync-convex-env" "$ENVIRONMENT" bash "$ROOT_DIR/scripts/convex-codegen" fi args=(); [[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE") diff --git a/scripts/export-env b/scripts/export-env index 14412fe..3132ef9 100755 --- a/scripts/export-env +++ b/scripts/export-env @@ -1,12 +1,19 @@ #!/usr/bin/env sh set -eu -[ "$#" -eq 1 ] || { echo "usage: export-env " >&2; exit 2; } +[ "$#" -eq 1 ] || { echo "usage: export-env " >&2; exit 2; } ENVIRONMENT="$1" -case "$ENVIRONMENT" in dev|staging) ;; *) echo "export-env: expected dev or staging" >&2; exit 2 ;; esac +case "$ENVIRONMENT" in + dev|staging|production|prod) ;; + *) echo "export-env: expected dev, staging, production, or prod" >&2; exit 2 ;; +esac +INFISICAL_ENV="$ENVIRONMENT" +case "$INFISICAL_ENV" in + production) INFISICAL_ENV=prod ;; +esac ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env" +STATE_FILE="$ROOT_DIR/.local/$INFISICAL_ENV.generated.env" if [ -n "${CI:-}" ]; then echo "export-env: refusing to export secrets in CI; use injected variables or CI_ENV_FILE." >&2 @@ -16,8 +23,8 @@ fi [ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; } command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; } -(cd "$ROOT_DIR" && infisical export --env="$ENVIRONMENT" --format=dotenv --silent) || { - echo "export-env: failed to export '$ENVIRONMENT'; check login and project access." >&2 +(cd "$ROOT_DIR" && infisical export --env="$INFISICAL_ENV" --format=dotenv --silent) || { + echo "export-env: failed to export '$INFISICAL_ENV'; check login and project access." >&2 exit 1 } diff --git a/scripts/sync-convex-env b/scripts/sync-convex-env index dc61f8e..50950e6 100755 --- a/scripts/sync-convex-env +++ b/scripts/sync-convex-env @@ -4,26 +4,32 @@ set -euo pipefail ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" usage() { - printf 'usage: sync-convex-env \n' >&2 + printf 'usage: sync-convex-env \n' >&2 exit 2 } ENVIRONMENT="${1:-}" -[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || usage +[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging || "$ENVIRONMENT" == production || "$ENVIRONMENT" == prod ]] || usage +INFISICAL_ENV="$ENVIRONMENT" +case "$INFISICAL_ENV" in + production) INFISICAL_ENV=prod ;; +esac if [[ "${2:-}" != "--from-current-env" ]]; then ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/spoon-convex-env.XXXXXX.env")" trap 'rm -f "$ENV_FILE"' EXIT INT TERM HUP sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE" - exec bunx dotenv -e "$ENV_FILE" -- "$0" "$ENVIRONMENT" --from-current-env + exec bun dotenv -e "$ENV_FILE" -- "$0" "$ENVIRONMENT" --from-current-env fi info() { printf '▶ %s\n' "$*"; } warn() { printf 'Warning: %s\n' "$*" >&2; } -STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env" +STATE_FILE="$ROOT_DIR/.local/$INFISICAL_ENV.generated.env" +MISSING_REQUIRED=0 +MISSING_REQUIRED_NAMES=() convex_env_names() { - (cd "$ROOT_DIR/packages/backend" && bunx convex env list) 2>/dev/null \ + (cd "$ROOT_DIR/packages/backend" && bun convex env list) 2>/dev/null \ | sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' } @@ -44,11 +50,61 @@ set_convex_env() { tmp="$(mktemp "${TMPDIR:-/tmp}/spoon-convex-value.XXXXXX")" printf '%s' "$value" > "$tmp" - (cd "$ROOT_DIR/packages/backend" && bunx convex env set "$name" --from-file "$tmp" >/dev/null) + (cd "$ROOT_DIR/packages/backend" && bun convex env set "$name" --from-file "$tmp" >/dev/null) rm -f "$tmp" printf ' synced %s\n' "$name" } +require_exported_env() { + local name="$1" + if [[ -z "${!name-}" ]]; then + printf 'Error: required %s is missing from exported %s environment.\n' "$name" "$ENVIRONMENT" >&2 + MISSING_REQUIRED=1 + MISSING_REQUIRED_NAMES+=("$name") + fi +} + +require_available_env() { + local name="$1" + if [[ -n "${!name-}" ]] || convex_env_has "$name"; then + return 0 + fi + printf 'Error: required %s is missing from exported %s environment and Convex env.\n' "$name" "$ENVIRONMENT" >&2 + MISSING_REQUIRED=1 + MISSING_REQUIRED_NAMES+=("$name") +} + +require_non_dev_env() { + [[ "$ENVIRONMENT" != dev ]] || return 0 + for name in \ + JWT_PRIVATE_KEY \ + JWKS \ + AUTH_AUTHENTIK_ID \ + AUTH_AUTHENTIK_SECRET \ + AUTH_AUTHENTIK_ISSUER \ + AUTH_GITHUB_ID \ + AUTH_GITHUB_SECRET \ + SPOON_ENCRYPTION_KEY + do + require_available_env "$name" + done + if [[ -z "${SITE_URL:-}" && -z "${NEXT_PUBLIC_SITE_URL:-}" ]]; then + if convex_env_has SITE_URL; then + return 0 + fi + printf 'Error: required SITE_URL or NEXT_PUBLIC_SITE_URL is missing from exported %s environment and SITE_URL is missing from Convex env.\n' "$ENVIRONMENT" >&2 + MISSING_REQUIRED=1 + MISSING_REQUIRED_NAMES+=("SITE_URL") + fi +} + +finish_required_env_check() { + [[ "$MISSING_REQUIRED" -eq 0 ]] && return 0 + printf '\nConvex env sync completed for available values, but required values are still missing: %s\n' "${MISSING_REQUIRED_NAMES[*]}" >&2 + printf '\nGenerate missing Convex Auth keys with:\n bun auth:keys\n\nStore missing values in Infisical/Gitea, then rerun sync.\n' >&2 + exit 1 +} + set_literal_convex_env() { local name="$1" local value="$2" @@ -56,7 +112,7 @@ set_literal_convex_env() { tmp="$(mktemp "${TMPDIR:-/tmp}/spoon-convex-value.XXXXXX")" printf '%s' "$value" > "$tmp" - (cd "$ROOT_DIR/packages/backend" && bunx convex env set "$name" --from-file "$tmp" >/dev/null) + (cd "$ROOT_DIR/packages/backend" && bun convex env set "$name" --from-file "$tmp" >/dev/null) rm -f "$tmp" printf ' synced %s\n' "$name" } @@ -95,8 +151,7 @@ sync_generated_dev_auth_keys() { set_literal_convex_env JWKS "$jwks" } -sync_generated_dev_encryption_key() { - [[ "$ENVIRONMENT" == dev ]] || return 0 +sync_encryption_key() { if [[ -n "${SPOON_ENCRYPTION_KEY:-}" ]]; then set_convex_env SPOON_ENCRYPTION_KEY return 0 @@ -105,7 +160,7 @@ sync_generated_dev_encryption_key() { return 0 fi - info "Generating local Spoon encryption key" + info "Generating $ENVIRONMENT Spoon encryption key" local encryption_key encryption_key="$(generate_secret)" [[ -n "$encryption_key" ]] || { @@ -161,8 +216,9 @@ CURRENT_CONVEX_ENV_NAMES="$(convex_env_names || true)" info "Syncing $ENVIRONMENT environment variables into Convex" sync_generated_dev_auth_keys -sync_generated_dev_encryption_key +sync_encryption_key sync_generated_dev_worker_token +require_non_dev_env for name in \ JWT_PRIVATE_KEY \ @@ -193,4 +249,6 @@ done sync_site_url +finish_required_env_check + info "Convex environment sync complete"