From 31abf03a48d52d7ec8cb83d068d5b5a890769802 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 19 Oct 2026 23:58:04 +0300 Subject: [PATCH] V17: Drop the hardcoded fallback JWT secret from the auth docs Co-Authored-By: Claude Opus 5.5 --- docs/auth.md | 18 +++++++++++++----- llms-full.txt | 18 +++++++++++++----- src/auth/jwt.test.ts | 2 ++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/auth.md b/docs/auth.md index f7b7e903e..a18698f08 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -8,7 +8,7 @@ accepts the request returns a **principal**, and that principal goes into the run, where your `model` / `instructions` / `tools` functions and memory scopes can read it. -```ts +```ts no-run import { createAgent, createRouteHandler } from '@lousho/build-ai-agent'; import { apiToken, jwt } from '@lousho/build-ai-agent/auth'; import { mockModel } from '@lousho/build-ai-agent/testing'; @@ -18,10 +18,18 @@ const agent = createAgent({ instructions: ({ principal }) => `You are helping ${principal?.id ?? 'a guest'}.`, }); +// Read secrets from the environment and fail at startup when one is missing. +// Never fall back to a literal: a default in your source is a secret everyone can read. +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`${name} is not set`); + return value; +} + export const { GET, POST } = createRouteHandler(agent, { auth: [ - jwt({ secret: process.env.JWT_SECRET ?? 'a-development-secret-of-32-bytes!!', issuer: 'https://auth.example.com', audience: 'agent-api' }), - apiToken(process.env.CI_TOKEN ?? 'ci-token', { id: 'ci' }), + jwt({ secret: requireEnv('JWT_SECRET'), issuer: 'https://auth.example.com', audience: 'agent-api' }), + apiToken(requireEnv('CI_TOKEN'), { id: 'ci' }), ], }); ``` @@ -133,9 +141,9 @@ show their login prompt. ```ts import { basic } from '@lousho/build-ai-agent/auth'; -const operators = basic({ users: { ops: process.env.OPS_PASSWORD ?? 'change-me' }, realm: 'agent' }); +const operators = basic({ users: { ops: process.env.OPS_PASSWORD! }, realm: 'agent' }); // Or check against your own store (compare in constant time there): -const fromDatabase = basic({ users: async (user, password) => user === 'svc' && password === (process.env.SVC_PASSWORD ?? '') }); +const fromDatabase = basic({ users: async (user, password) => user === 'svc' && password === process.env.SVC_PASSWORD }); ``` **Only use `basic()` over HTTPS**: the password travels with every request. diff --git a/llms-full.txt b/llms-full.txt index 71aa1e364..807ac4a70 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -3044,7 +3044,7 @@ accepts the request returns a **principal**, and that principal goes into the run, where your `model` / `instructions` / `tools` functions and memory scopes can read it. -```ts +```ts no-run import { createAgent, createRouteHandler } from '@lousho/build-ai-agent'; import { apiToken, jwt } from '@lousho/build-ai-agent/auth'; import { mockModel } from '@lousho/build-ai-agent/testing'; @@ -3054,10 +3054,18 @@ const agent = createAgent({ instructions: ({ principal }) => `You are helping ${principal?.id ?? 'a guest'}.`, }); +// Read secrets from the environment and fail at startup when one is missing. +// Never fall back to a literal: a default in your source is a secret everyone can read. +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`${name} is not set`); + return value; +} + export const { GET, POST } = createRouteHandler(agent, { auth: [ - jwt({ secret: process.env.JWT_SECRET ?? 'a-development-secret-of-32-bytes!!', issuer: 'https://auth.example.com', audience: 'agent-api' }), - apiToken(process.env.CI_TOKEN ?? 'ci-token', { id: 'ci' }), + jwt({ secret: requireEnv('JWT_SECRET'), issuer: 'https://auth.example.com', audience: 'agent-api' }), + apiToken(requireEnv('CI_TOKEN'), { id: 'ci' }), ], }); ``` @@ -3169,9 +3177,9 @@ show their login prompt. ```ts import { basic } from '@lousho/build-ai-agent/auth'; -const operators = basic({ users: { ops: process.env.OPS_PASSWORD ?? 'change-me' }, realm: 'agent' }); +const operators = basic({ users: { ops: process.env.OPS_PASSWORD! }, realm: 'agent' }); // Or check against your own store (compare in constant time there): -const fromDatabase = basic({ users: async (user, password) => user === 'svc' && password === (process.env.SVC_PASSWORD ?? '') }); +const fromDatabase = basic({ users: async (user, password) => user === 'svc' && password === process.env.SVC_PASSWORD }); ``` **Only use `basic()` over HTTPS**: the password travels with every request. diff --git a/src/auth/jwt.test.ts b/src/auth/jwt.test.ts index a94fffaa7..8609c8afc 100644 --- a/src/auth/jwt.test.ts +++ b/src/auth/jwt.test.ts @@ -101,6 +101,8 @@ describe('jwt()', () => { { publicKey: '-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----', audience: AUD }, { publicKey: { kty: 'oct', k: 'c2VjcmV0' }, audience: AUD }, { secret: SECRET, audience: '' }, + { secret: undefined, audience: AUD }, // an unset env var must not silently disable the key + { secret: '', audience: AUD }, ]; for (const options of cases) expect(invalid(() => jwt(options)), JSON.stringify(options)).toBe('LOUSHO_AUTH_CONFIG_INVALID'); });