The auth seam read `process.env.AUTH_ENFORCE === 'true'`, so an absent env var meant NO authentication: any fresh container, local run, DR restore or redeploy that dropped the variable served /api/0pt/* anonymously, taking the tenant from the caller-supplied X-Tenant-Id header. 0pt.app is deployed and was answering anonymously (verified: an invalid Bearer token plus an arbitrary X-Tenant-Id returned 200), so this was a live hole, not latent. * authEnforced() now returns true unless AUTH_ENFORCE is explicitly "false" (trimmed, case-insensitive). Unset/empty/misspelled => ENFORCED. * assertAuthConfigSafe() throws AuthMisconfigurationError when NODE_ENV=production and enforcement resolved off, naming AUTH_ENFORCE. It runs at middleware module load (startup) and on the env-driven request path, so a production instance can neither boot nor serve a single unauthenticated request with auth switched off. * .env.example no longer teaches AUTH_ENFORCE=false; README's Deploy section states the fail-closed posture (no DEPLOY.md in this repo). * 7 new tests, incl. the regression case: with AUTH_ENFORCE unset and no `enforce` option, an unauthenticated request is rejected with 401. No test was inverted or deleted -- none asserted the open default; the three enforce:false cases now describe the explicit local-dev escape hatch. Tests: 121 passed / 1 skipped before, 128 passed / 1 skipped after. Typecheck and build clean; advisory lint unchanged (16 pre-existing problems). Scope: auth default only. The matcher still covers /api/0pt/* alone, and the handlers still take tenant_id from the query/body rather than the verified principal -- both described in the PR body, neither fixed here.
81 lines
3.9 KiB
Bash
81 lines
3.9 KiB
Bash
# =============================================================================
|
|
# 0pt web console -- example environment
|
|
# =============================================================================
|
|
# Copy to `.env` (gitignored) and fill in real values. Every key below is
|
|
# actually referenced by the code (derived from `process.env.*` usage); nothing
|
|
# here is invented. All secrets are placeholders -- do NOT commit real values.
|
|
#
|
|
# The app is designed to build and boot with these UNSET: lib/db.ts and
|
|
# lib/stripe.ts are lazy + env-guarded, and the storage/payment/doc routes
|
|
# return HTTP 501 ("not configured") until the relevant keys are provided.
|
|
|
|
# ---- Core -------------------------------------------------------------------
|
|
# Public base URL of this app (used for absolute links / callbacks).
|
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
|
|
# ---- Database (Postgres) ----------------------------------------------------
|
|
# First one set wins: DATABASE_URL -> POSTGRES_URL -> POSTGRES_PRISMA_URL.
|
|
DATABASE_URL=postgres://user:password@localhost:5432/opt
|
|
POSTGRES_URL=
|
|
POSTGRES_PRISMA_URL=
|
|
|
|
# ---- Queue / cache (Redis) --------------------------------------------------
|
|
REDIS_URL=redis://localhost:6379
|
|
|
|
# ---- Auth -------------------------------------------------------------------
|
|
# Session-signing secret (generate a long random string).
|
|
AUTH_SECRET=change-me-long-random-string
|
|
# Optional dev-only login shortcut key (leave unset in production).
|
|
DEV_LOGIN_KEY=
|
|
# Encryption key for stored DB-connection grants (/api/connections/db-grant).
|
|
GRANT_ENC_KEY=change-me-32-byte-key
|
|
# Shared token for internal/service-to-service calls.
|
|
INTERNAL_TOKEN=change-me-internal-token
|
|
|
|
# ---- Zitadel / OIDC Bearer verification (Wave 2 hardening) ------------------
|
|
# 0pt verifies inbound Bearer tokens against the fleet Zitadel IdP
|
|
# (src/auth/zitadel.ts, wired via middleware.ts). JWKS are discovered from
|
|
# <issuer>/.well-known/openid-configuration, falling back to <issuer>/oauth/v2/keys.
|
|
#
|
|
# Fleet Zitadel issuer (OIDC). Default when unset: https://id-customers.siax.io
|
|
ZITADEL_ISSUER=https://id-customers.siax.io
|
|
# Enforcement is FAIL-CLOSED. Leave this UNSET (or set it to "true") and a
|
|
# valid, verified Bearer token is REQUIRED on /api/0pt/* -- 401 on
|
|
# missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant.
|
|
# ONLY the literal value "false" disables authentication, and that value is
|
|
# REFUSED when NODE_ENV=production: the app throws at startup instead of
|
|
# serving openly. Use "false" for local development only.
|
|
AUTH_ENFORCE=true
|
|
# Optional override for the claim key that carries the SIAX tenant id. When
|
|
# unset, tenant derivation uses the documented precedence:
|
|
# urn:zitadel:iam:org:id -> urn:zitadel:iam:user:resourceowner:id -> tenant_id -> tenant.
|
|
ZITADEL_TENANT_CLAIM=
|
|
|
|
# ---- Payments (Stripe) ------------------------------------------------------
|
|
STRIPE_SECRET_KEY=sk_test_xxx
|
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
|
|
# ---- Object storage (Cloudflare R2 / S3-compatible) -------------------------
|
|
# All three required to enable /api/uploads; otherwise it returns 501.
|
|
R2_ENDPOINT=https://<accountid>.r2.cloudflarestorage.com
|
|
R2_BUCKET=opt-uploads
|
|
R2_ACCESS_KEY_ID=change-me
|
|
# Optional local blob dir fallback used when R2 is not configured.
|
|
OPT_LOCAL_BLOB_DIR=./.opt-blobs
|
|
|
|
# ---- Document intelligence (Azure) ------------------------------------------
|
|
# Powers document extraction routes; unset => extraction "not configured".
|
|
DOC_INTELLIGENCE_ENDPOINT=https://<resource>.cognitiveservices.azure.com
|
|
DOC_INTELLIGENCE_KEY=change-me
|
|
DOC_INTELLIGENCE_API_VERSION=2024-11-30
|
|
|
|
# ---- Engine / integrations --------------------------------------------------
|
|
# URL of the 0pt data engine (services/api orchestrator).
|
|
ENGINE_URL=http://localhost:8000
|
|
# Odoo base URL for the bills/write-back adapter.
|
|
ODOO_BASE_URL=https://<your-odoo-host>
|
|
|
|
# ---- Test-only (tests/p0 real-infra suite; not needed to run the app) --------
|
|
# ST0RE_URL=
|
|
# ST0RE_TEST_TENANT_ID=
|