fix(auth): handle a missing AUTH_SECRET instead of dying mid sign-in

`storeTokens()` and `createSessionToken()` ran outside any catch. Both read
AUTH_SECRET — one derives the AES key that encrypts the platform bundle, the
other signs the identity cookie — and in production both refuse to fall back to
the development key. So an unset AUTH_SECRET threw after the credentials had
already been accepted upstream, and the browser got a bare 500 on a sign-in
that was entirely valid.

The status was the smaller half. The upstream session minted moments earlier by
`authApi.login` was ORPHANED: a live refresh token, issued to somebody who did
not end up logged in, left to expire on its own. The platform-admin branch a few
lines above already revokes for precisely this reason — declining because the
server is broken is no different from declining because the account is wrong —
so this now revokes too, best-effort, on the same terms.

It fails closed. No cookie is set on this path, so a half-configured server
cannot hand out a session it will be unable to verify on the next request.

ConfigError moves to src/shared/errors/configError.ts because its throwers now
span two runtimes: apiClient and tokenStore are server-only, while sessionToken
is reached from src/proxy.ts, which Next compiles for Edge. Declaring it in
apiClient would have dragged the whole platform client, `server-only` guard and
all, into the proxy bundle to name one class. The new module imports nothing.

Verified by exercising both functions directly: with AUTH_SECRET unset under
NODE_ENV=production, sealTokens and createSessionToken each raise ConfigError
rather than a bare Error; with it set, both succeed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 19:43:57 +05:30
parent 3dc0bba6f4
commit c01c750436
6 changed files with 99 additions and 36 deletions

View File

@@ -0,0 +1,29 @@
/**
* A deployment is misconfigured — a required environment variable is missing,
* or set to something this app refuses to accept.
*
* ── Why it lives in its own module ───────────────────────────────────────
* Its throwers span two runtimes. `apiClient` and `tokenStore` are Node-only
* (`server-only`), while `sessionToken` is reached from `src/proxy.ts`, which
* Next compiles for the Edge runtime. Declaring this in apiClient meant the
* proxy bundle would have to pull in the whole platform client — and its
* `server-only` guard — to name one error class. This file imports nothing, so
* both runtimes can share the type without sharing anything else.
*
* ── Why it is not just Error ─────────────────────────────────────────────
* Callers have to tell a misconfigured server apart from a failing one. Those
* read identically to a browser and have nothing in common as remedies: one is
* fixed by an operator in under a minute, the other by waiting. Collapsing them
* is what had a missing LOYALY_API_BASE reported as "Could not reach Loyaly,
* check your connection".
*
* Every caller treats this as a 500 whose detail is LOGGED, never returned:
* the message names environment variables, which is operator information.
* Grep production logs for `[loyaly] configuration error:`.
*/
export class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigError';
}
}

View File

@@ -1,6 +1,7 @@
import 'server-only';
import type {NextRequest} from 'next/server';
import {ConfigError, UpstreamError} from '@/services/api/apiClient';
import {UpstreamError} from '@/services/api/apiClient';
import {ConfigError} from '@/shared/errors/configError';
import {NoSessionError, withUpstream} from '@/features/auth/services/upstreamSession';
import {ok, parseQuery, type Query} from '@/shared/services/apiRoute';
import type {ApiErrorCode} from '@/shared/types/api';