te.js 2.3.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,6 +161,12 @@ app.takeoff();
161
161
  // Visit http://localhost:1403/docs
162
162
  ```
163
163
 
164
+ Docs are disabled in production by default. Set `DOCS_PASSWORD` to enable protected access:
165
+
166
+ ```bash
167
+ DOCS_PASSWORD=my-secret
168
+ ```
169
+
164
170
  ## Documentation
165
171
 
166
172
  For comprehensive documentation, see the [docs folder](./docs) or visit [tejas-documentation.vercel.app](https://usetejas.com).
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Password protection for the docs UI. When a password is configured,
3
+ * visitors must authenticate via a login form before viewing the docs.
4
+ *
5
+ * Uses an HMAC-based cookie so no server-side session state is needed.
6
+ * All comparisons are timing-safe to prevent timing attacks.
7
+ */
8
+
9
+ import { createHmac, timingSafeEqual } from 'node:crypto';
10
+
11
+ const COOKIE_NAME = '_tejs_docs';
12
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
13
+ const HMAC_KEY = 'tejs-docs-auth-v1';
14
+
15
+ function createToken(password) {
16
+ return createHmac('sha256', password).update(HMAC_KEY).digest('hex');
17
+ }
18
+
19
+ /**
20
+ * Timing-safe password comparison. Both values are HMAC'd first so
21
+ * the comparison never leaks password length.
22
+ */
23
+ function verifyPassword(submitted, expected) {
24
+ const a = createHmac('sha256', HMAC_KEY)
25
+ .update(String(submitted ?? ''))
26
+ .digest();
27
+ const b = createHmac('sha256', HMAC_KEY).update(String(expected)).digest();
28
+ return timingSafeEqual(a, b);
29
+ }
30
+
31
+ function parseCookies(header) {
32
+ if (!header) return {};
33
+ return Object.fromEntries(
34
+ header.split(';').map((c) => {
35
+ const [k, ...v] = c.trim().split('=');
36
+ return [k.trim(), v.join('=').trim()];
37
+ }),
38
+ );
39
+ }
40
+
41
+ function isAuthenticated(headers, password) {
42
+ const token = parseCookies(headers?.cookie)[COOKIE_NAME];
43
+ if (!token) return false;
44
+ const expected = createToken(password);
45
+ if (token.length !== expected.length) return false;
46
+ try {
47
+ return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function setAuthCookie(res, password) {
54
+ const token = createToken(password);
55
+ res.setHeader(
56
+ 'Set-Cookie',
57
+ `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${COOKIE_MAX_AGE}`,
58
+ );
59
+ }
60
+
61
+ function buildLoginPage(error = null) {
62
+ const errorHtml = error ? `<div class="error">${error}</div>` : '';
63
+
64
+ return `<!DOCTYPE html>
65
+ <html lang="en">
66
+ <head>
67
+ <meta charset="UTF-8" />
68
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
69
+ <title>API Documentation</title>
70
+ <style>
71
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
72
+ body {
73
+ min-height: 100vh;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
78
+ background: #0f0f11;
79
+ color: #e4e4e7;
80
+ }
81
+ .card {
82
+ width: 100%;
83
+ max-width: 380px;
84
+ padding: 40px 32px;
85
+ background: #18181b;
86
+ border: 1px solid #27272a;
87
+ border-radius: 16px;
88
+ }
89
+ .icon {
90
+ width: 48px;
91
+ height: 48px;
92
+ margin: 0 auto 24px;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ background: #27272a;
97
+ border-radius: 12px;
98
+ }
99
+ .icon svg { width: 24px; height: 24px; color: #a1a1aa; }
100
+ h1 {
101
+ font-size: 20px;
102
+ font-weight: 600;
103
+ text-align: center;
104
+ margin-bottom: 6px;
105
+ color: #fafafa;
106
+ }
107
+ .subtitle {
108
+ font-size: 14px;
109
+ text-align: center;
110
+ color: #71717a;
111
+ margin-bottom: 28px;
112
+ }
113
+ .error {
114
+ background: #371520;
115
+ border: 1px solid #5c1d33;
116
+ color: #fca5a5;
117
+ font-size: 13px;
118
+ padding: 10px 14px;
119
+ border-radius: 8px;
120
+ margin-bottom: 20px;
121
+ }
122
+ label {
123
+ display: block;
124
+ font-size: 13px;
125
+ font-weight: 500;
126
+ color: #a1a1aa;
127
+ margin-bottom: 6px;
128
+ }
129
+ input[type="password"] {
130
+ width: 100%;
131
+ padding: 10px 14px;
132
+ font-size: 14px;
133
+ background: #09090b;
134
+ border: 1px solid #27272a;
135
+ border-radius: 8px;
136
+ color: #fafafa;
137
+ outline: none;
138
+ transition: border-color 0.15s;
139
+ }
140
+ input[type="password"]:focus {
141
+ border-color: #3b82f6;
142
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
143
+ }
144
+ input[type="password"]::placeholder { color: #52525b; }
145
+ button {
146
+ width: 100%;
147
+ margin-top: 18px;
148
+ padding: 10px 0;
149
+ font-size: 14px;
150
+ font-weight: 500;
151
+ color: #fff;
152
+ background: #2563eb;
153
+ border: none;
154
+ border-radius: 8px;
155
+ cursor: pointer;
156
+ transition: background 0.15s;
157
+ }
158
+ button:hover { background: #1d4ed8; }
159
+ button:active { background: #1e40af; }
160
+ </style>
161
+ </head>
162
+ <body>
163
+ <form class="card" method="POST">
164
+ <div class="icon">
165
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
166
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
167
+ </svg>
168
+ </div>
169
+ <h1>API Documentation</h1>
170
+ <p class="subtitle">Enter the password to continue</p>
171
+ ${errorHtml}
172
+ <label for="password">Password</label>
173
+ <input type="password" id="password" name="password" placeholder="Enter password" required autofocus />
174
+ <button type="submit">Continue</button>
175
+ </form>
176
+ </body>
177
+ </html>`;
178
+ }
179
+
180
+ const DOCS_URL = 'https://usetejas.com/docs/auto-docs';
181
+
182
+ function buildSetupPage() {
183
+ return `<!DOCTYPE html>
184
+ <html lang="en">
185
+ <head>
186
+ <meta charset="UTF-8" />
187
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
188
+ <title>API Documentation</title>
189
+ <style>
190
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
191
+ body {
192
+ min-height: 100vh;
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
197
+ background: #0f0f11;
198
+ color: #e4e4e7;
199
+ }
200
+ .card {
201
+ width: 100%;
202
+ max-width: 460px;
203
+ padding: 40px 32px;
204
+ background: #18181b;
205
+ border: 1px solid #27272a;
206
+ border-radius: 16px;
207
+ }
208
+ .icon {
209
+ width: 48px;
210
+ height: 48px;
211
+ margin: 0 auto 24px;
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ background: #27272a;
216
+ border-radius: 12px;
217
+ }
218
+ .icon svg { width: 24px; height: 24px; color: #eab308; }
219
+ h1 {
220
+ font-size: 20px;
221
+ font-weight: 600;
222
+ text-align: center;
223
+ margin-bottom: 6px;
224
+ color: #fafafa;
225
+ }
226
+ .subtitle {
227
+ font-size: 14px;
228
+ text-align: center;
229
+ color: #71717a;
230
+ margin-bottom: 28px;
231
+ line-height: 1.5;
232
+ }
233
+ .code-block {
234
+ background: #09090b;
235
+ border: 1px solid #27272a;
236
+ border-radius: 8px;
237
+ padding: 14px 16px;
238
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
239
+ font-size: 13px;
240
+ color: #a1a1aa;
241
+ margin-bottom: 20px;
242
+ overflow-x: auto;
243
+ }
244
+ .code-block .env-key { color: #22d3ee; }
245
+ .code-block .env-val { color: #a78bfa; }
246
+ .hint {
247
+ font-size: 13px;
248
+ color: #71717a;
249
+ text-align: center;
250
+ line-height: 1.5;
251
+ }
252
+ .hint a {
253
+ color: #3b82f6;
254
+ text-decoration: none;
255
+ }
256
+ .hint a:hover { text-decoration: underline; }
257
+ </style>
258
+ </head>
259
+ <body>
260
+ <div class="card">
261
+ <div class="icon">
262
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
263
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
264
+ </svg>
265
+ </div>
266
+ <h1>API Docs Disabled</h1>
267
+ <p class="subtitle">A password is required to serve API documentation in production. Set the <strong>DOCS_PASSWORD</strong> environment variable to enable access.</p>
268
+ <div class="code-block"><span class="env-key">DOCS_PASSWORD</span>=<span class="env-val">your-secret-here</span></div>
269
+ <p class="hint">Learn more about <a href="${DOCS_URL}" target="_blank" rel="noopener">serving and protecting API docs</a></p>
270
+ </div>
271
+ </body>
272
+ </html>`;
273
+ }
274
+
275
+ /**
276
+ * Returns true when docs should be disabled without a password:
277
+ * NODE_ENV is unset or set to 'production'.
278
+ */
279
+ function requiresPasswordForEnv() {
280
+ const env = process.env.NODE_ENV;
281
+ return !env || env === 'production';
282
+ }
283
+
284
+ export {
285
+ isAuthenticated,
286
+ setAuthCookie,
287
+ verifyPassword,
288
+ buildLoginPage,
289
+ buildSetupPage,
290
+ requiresPasswordForEnv,
291
+ };
@@ -9,6 +9,32 @@
9
9
 
10
10
  import Endpoint from '../../server/endpoint.js';
11
11
  import targetRegistry from '../../server/targets/registry.js';
12
+ import {
13
+ isAuthenticated,
14
+ setAuthCookie,
15
+ verifyPassword,
16
+ buildLoginPage,
17
+ buildSetupPage,
18
+ requiresPasswordForEnv,
19
+ } from './docs-auth.js';
20
+
21
+ /**
22
+ * Write an HTML response directly, bypassing ammo.fire so the response
23
+ * envelope (when enabled) does not wrap the HTML in JSON.
24
+ */
25
+ function sendHtml(res, statusCode, html) {
26
+ res.writeHead(statusCode, { 'Content-Type': 'text/html' });
27
+ res.end(html);
28
+ }
29
+
30
+ /**
31
+ * Write a JSON response directly, bypassing ammo.fire so the response
32
+ * envelope does not wrap internal docs responses.
33
+ */
34
+ function sendJson(res, statusCode, data) {
35
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
36
+ res.end(JSON.stringify(data));
37
+ }
12
38
 
13
39
  /** Scalar API Reference browser standalone (IIFE, sets window.Scalar). Pinned for stability. */
14
40
  const SCALAR_VERSION = '1.46.0';
@@ -98,38 +124,98 @@ function buildDocsPage(specUrl, scalarConfig = {}) {
98
124
 
99
125
  /**
100
126
  * Create endpoint that serves the docs HTML page at GET docsPath.
127
+ *
128
+ * Three modes:
129
+ * - **password set** → login form for unauthenticated visitors, POST to verify
130
+ * - **no password, production/missing NODE_ENV** → setup page urging developer to set DOCS_PASSWORD
131
+ * - **no password, development** → open access
132
+ *
101
133
  * @param {string} docsPath - e.g. '/docs'
102
134
  * @param {string} htmlContent - Full HTML document
135
+ * @param {string|null} [password] - Optional password to protect docs
103
136
  * @returns {Endpoint}
104
137
  */
105
- function createDocsHtmlEndpoint(docsPath, htmlContent) {
138
+ function createDocsHtmlEndpoint(docsPath, htmlContent, password) {
106
139
  const endpoint = new Endpoint();
107
140
  endpoint.setPath('', docsPath);
108
141
  endpoint.setMiddlewares([]);
142
+
143
+ if (password) {
144
+ const loginPage = buildLoginPage();
145
+ const loginError = buildLoginPage('Incorrect password');
146
+
147
+ endpoint.setHandler((ammo) => {
148
+ if (!ammo.GET && !ammo.POST) return ammo.notAllowed('GET', 'POST');
149
+
150
+ if (ammo.POST) {
151
+ if (verifyPassword(ammo.payload?.password, password)) {
152
+ setAuthCookie(ammo.res, password);
153
+ return ammo.redirect(docsPath);
154
+ }
155
+ sendHtml(ammo.res, 401, loginError);
156
+ return;
157
+ }
158
+
159
+ if (isAuthenticated(ammo.headers, password)) {
160
+ sendHtml(ammo.res, 200, htmlContent);
161
+ } else {
162
+ sendHtml(ammo.res, 200, loginPage);
163
+ }
164
+ });
165
+ return endpoint;
166
+ }
167
+
168
+ if (requiresPasswordForEnv()) {
169
+ const setupPage = buildSetupPage();
170
+ endpoint.setHandler((ammo) => {
171
+ if (!ammo.GET) return ammo.notAllowed();
172
+ sendHtml(ammo.res, 403, setupPage);
173
+ });
174
+ return endpoint;
175
+ }
176
+
109
177
  endpoint.setHandler((ammo) => {
110
178
  if (!ammo.GET) return ammo.notAllowed();
111
- ammo.fire(200, htmlContent, 'text/html');
179
+ sendHtml(ammo.res, 200, htmlContent);
112
180
  });
113
181
  return endpoint;
114
182
  }
115
183
 
116
184
  /**
117
185
  * Create endpoint that serves the OpenAPI spec JSON at GET specPath.
186
+ * Returns 401 for unauthenticated requests when a password is set,
187
+ * or 403 when docs are disabled (production without DOCS_PASSWORD).
118
188
  * @param {string} specPath - e.g. '/docs/openapi.json'
119
189
  * @param {() => object | Promise<object>} getSpec - Function that returns the current spec
190
+ * @param {string|null} [password] - Optional password to protect docs
120
191
  * @returns {Endpoint}
121
192
  */
122
- function createSpecJsonEndpoint(specPath, getSpec) {
193
+ function createSpecJsonEndpoint(specPath, getSpec, password) {
123
194
  const endpoint = new Endpoint();
124
195
  endpoint.setPath('', specPath);
125
196
  endpoint.setMiddlewares([]);
197
+
198
+ if (!password && requiresPasswordForEnv()) {
199
+ endpoint.setHandler((ammo) => {
200
+ if (!ammo.GET) return ammo.notAllowed();
201
+ sendJson(ammo.res, 403, {
202
+ error: 'Docs disabled. Set the DOCS_PASSWORD environment variable.',
203
+ });
204
+ });
205
+ return endpoint;
206
+ }
207
+
126
208
  endpoint.setHandler(async (ammo) => {
127
209
  if (!ammo.GET) return ammo.notAllowed();
210
+ if (password && !isAuthenticated(ammo.headers, password)) {
211
+ sendJson(ammo.res, 401, { error: 'Unauthorized' });
212
+ return;
213
+ }
128
214
  try {
129
215
  const spec = await Promise.resolve(getSpec());
130
- ammo.fire(200, spec);
216
+ sendJson(ammo.res, 200, spec);
131
217
  } catch (err) {
132
- ammo.fire(500, {
218
+ sendJson(ammo.res, 500, {
133
219
  error: 'Failed to generate OpenAPI spec',
134
220
  message: err?.message,
135
221
  });
@@ -147,6 +233,7 @@ function createSpecJsonEndpoint(specPath, getSpec) {
147
233
  * @param {string} [options.docsPath='/docs'] - Base path for docs (HTML page and spec URL). Routes: GET {docsPath}, GET {docsPath}/openapi.json.
148
234
  * @param {string} [options.specUrl] - Override for the spec URL shown in the docs page (default: '{docsPath}/openapi.json'). Use when serving behind a proxy with a different base path.
149
235
  * @param {object} [options.scalarConfig] - Optional Scalar API Reference config (e.g. { layout: 'classic' } for try-it on the same page).
236
+ * @param {string|null} [options.password] - Optional password to protect docs behind a login form. When set, unauthenticated visitors see a password prompt.
150
237
  * @param {boolean} [options.mutateRegistry=true] - If true, push endpoints to registry. If false, return [docsEndpoint, specEndpoint] without mutating.
151
238
  * @param {object} [registry] - Target registry to register routes on when mutateRegistry is true. Defaults to the module's targetRegistry.
152
239
  * @returns {undefined | [Endpoint, Endpoint]} When mutateRegistry is false, returns the two endpoints for the caller to register.
@@ -157,6 +244,7 @@ export function registerDocRoutes(options = {}, registry = targetRegistry) {
157
244
  docsPath = '/docs',
158
245
  specUrl: specUrlOption,
159
246
  scalarConfig,
247
+ password = null,
160
248
  mutateRegistry = true,
161
249
  } = options;
162
250
 
@@ -171,8 +259,8 @@ export function registerDocRoutes(options = {}, registry = targetRegistry) {
171
259
  const specPath = `${basePath}/openapi.json`;
172
260
  const htmlContent = buildDocsPage(specUrl, scalarConfig);
173
261
 
174
- const docsEndpoint = createDocsHtmlEndpoint(docsPath, htmlContent);
175
- const specEndpoint = createSpecJsonEndpoint(specPath, getSpec);
262
+ const docsEndpoint = createDocsHtmlEndpoint(docsPath, htmlContent, password);
263
+ const specEndpoint = createSpecJsonEndpoint(specPath, getSpec, password);
176
264
 
177
265
  if (mutateRegistry) {
178
266
  registry.targets.push(docsEndpoint);
@@ -70,7 +70,7 @@ app.withRateLimit({
70
70
 
71
71
  #### serveDocs(config)
72
72
 
73
- Serve an interactive API documentation UI (Scalar) from a pre-generated OpenAPI spec.
73
+ Serve an interactive API documentation UI (Scalar) from a pre-generated OpenAPI spec. Optionally password-protected.
74
74
 
75
75
  ```javascript
76
76
  app.serveDocs({
@@ -79,12 +79,13 @@ app.serveDocs({
79
79
  });
80
80
  ```
81
81
 
82
- | Option | Type | Default | Description |
83
- | -------------- | ------ | ------------------ | ------------------------------- |
84
- | `specPath` | string | `'./openapi.json'` | Path to the OpenAPI spec file |
85
- | `scalarConfig` | object | _(defaults)_ | Scalar UI configuration options |
82
+ | Option | Type | Default | Description |
83
+ | -------------- | ------ | ------------------- | ----------------------------------------------------------------------------------- |
84
+ | `specPath` | string | `'./openapi.json'` | Path to the OpenAPI spec file |
85
+ | `password` | string | `DOCS_PASSWORD` env | Password to protect docs behind a login form. Falls back to `DOCS_PASSWORD` env var |
86
+ | `scalarConfig` | object | _(defaults)_ | Scalar UI configuration options |
86
87
 
87
- Registers `GET /docs` (HTML UI) and `GET /docs/openapi.json` (spec JSON).
88
+ Registers `GET /docs` (HTML UI) and `GET /docs/openapi.json` (spec JSON). In production (or when `NODE_ENV` is unset), docs are **disabled** unless a password is configured. In development, docs are open by default. When a password is set, unauthenticated visitors see a login form.
88
89
 
89
90
  **Returns:** `Tejas` (for chaining)
90
91
 
package/docs/auto-docs.md CHANGED
@@ -32,11 +32,11 @@ Target files → Handler analysis → LLM enhancement → OpenAPI 3.0 spec → S
32
32
 
33
33
  The `level` option controls how much context the LLM receives and how much work it does:
34
34
 
35
- | Level | Name | Context Sent to LLM | Output |
36
- |-------|------|---------------------|--------|
37
- | **1** | Moderate | Handler source code only (~hundreds of tokens per endpoint) | Summaries, schemas, tags |
38
- | **2** | High | Handler + full dependency chain from imports (~thousands of tokens per endpoint) | More accurate schemas and descriptions |
39
- | **3** | Comprehensive | Same as level 2, plus post-processing | Everything from level 2, plus: reordered tags by importance, `API_OVERVIEW.md` page |
35
+ | Level | Name | Context Sent to LLM | Output |
36
+ | ----- | ------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
37
+ | **1** | Moderate | Handler source code only (~hundreds of tokens per endpoint) | Summaries, schemas, tags |
38
+ | **2** | High | Handler + full dependency chain from imports (~thousands of tokens per endpoint) | More accurate schemas and descriptions |
39
+ | **3** | Comprehensive | Same as level 2, plus post-processing | Everything from level 2, plus: reordered tags by importance, `API_OVERVIEW.md` page |
40
40
 
41
41
  Higher levels produce better documentation but use more LLM tokens.
42
42
 
@@ -47,24 +47,28 @@ You can provide explicit metadata when registering endpoints. This metadata is u
47
47
  ```javascript
48
48
  const users = new Target('/users');
49
49
 
50
- users.register('/', {
51
- summary: 'User operations',
52
- description: 'Create and list users',
53
- methods: ['GET', 'POST'],
54
- request: {
55
- name: { type: 'string', required: true },
56
- email: { type: 'string', required: true }
50
+ users.register(
51
+ '/',
52
+ {
53
+ summary: 'User operations',
54
+ description: 'Create and list users',
55
+ methods: ['GET', 'POST'],
56
+ request: {
57
+ name: { type: 'string', required: true },
58
+ email: { type: 'string', required: true },
59
+ },
60
+ response: {
61
+ 200: { description: 'Success' },
62
+ 201: { description: 'User created' },
63
+ 400: { description: 'Validation error' },
64
+ },
57
65
  },
58
- response: {
59
- 200: { description: 'Success' },
60
- 201: { description: 'User created' },
61
- 400: { description: 'Validation error' }
62
- }
63
- }, (ammo) => {
64
- if (ammo.GET) return ammo.fire(userService.list());
65
- if (ammo.POST) return ammo.fire(201, userService.create(ammo.payload));
66
- ammo.notAllowed();
67
- });
66
+ (ammo) => {
67
+ if (ammo.GET) return ammo.fire(userService.list());
68
+ if (ammo.POST) return ammo.fire(201, userService.create(ammo.payload));
69
+ ammo.notAllowed();
70
+ },
71
+ );
68
72
  ```
69
73
 
70
74
  The metadata object is optional. When omitted, the LLM infers everything from the handler source.
@@ -134,17 +138,17 @@ All options live under the `docs` key in `tejas.config.json`:
134
138
  }
135
139
  ```
136
140
 
137
- | Key | Type | Default | Description |
138
- |-----|------|---------|-------------|
139
- | `dirTargets` | string | `"targets"` | Directory containing `.target.js` files |
140
- | `output` | string | `"./openapi.json"` | Output file path for the generated spec |
141
- | `title` | string | `"API"` | API title in the OpenAPI `info` block |
142
- | `version` | string | `"1.0.0"` | API version in the OpenAPI `info` block |
143
- | `description` | string | `""` | API description |
144
- | `level` | number | `1` | Enhancement level (1–3) |
145
- | `llm` | object | — | LLM provider configuration (see above) |
146
- | `overviewPath` | string | `"./API_OVERVIEW.md"` | Path for the generated overview page (level 3 only) |
147
- | `productionBranch` | string | `"main"` | Branch that triggers `docs:on-push` |
141
+ | Key | Type | Default | Description |
142
+ | ------------------ | ------ | --------------------- | --------------------------------------------------- |
143
+ | `dirTargets` | string | `"targets"` | Directory containing `.target.js` files |
144
+ | `output` | string | `"./openapi.json"` | Output file path for the generated spec |
145
+ | `title` | string | `"API"` | API title in the OpenAPI `info` block |
146
+ | `version` | string | `"1.0.0"` | API version in the OpenAPI `info` block |
147
+ | `description` | string | `""` | API description |
148
+ | `level` | number | `1` | Enhancement level (1–3) |
149
+ | `llm` | object | — | LLM provider configuration (see above) |
150
+ | `overviewPath` | string | `"./API_OVERVIEW.md"` | Path for the generated overview page (level 3 only) |
151
+ | `productionBranch` | string | `"main"` | Branch that triggers `docs:on-push` |
148
152
 
149
153
  ## Serving API Docs
150
154
 
@@ -162,34 +166,71 @@ app.takeoff();
162
166
 
163
167
  This registers two routes:
164
168
 
165
- | Route | Description |
166
- |-------|-------------|
167
- | `GET /docs` | Interactive Scalar API reference UI |
168
- | `GET /docs/openapi.json` | Raw OpenAPI spec JSON |
169
+ | Route | Description |
170
+ | ------------------------ | ----------------------------------- |
171
+ | `GET /docs` | Interactive Scalar API reference UI |
172
+ | `GET /docs/openapi.json` | Raw OpenAPI spec JSON |
169
173
 
170
174
  ### serveDocs Options
171
175
 
172
176
  ```javascript
173
177
  app.serveDocs({
174
- specPath: './openapi.json', // Path to the spec file (relative to cwd)
175
- scalarConfig: { // Scalar UI configuration
176
- layout: 'modern', // 'modern' or 'classic'
178
+ specPath: './openapi.json', // Path to the spec file (relative to cwd)
179
+ password: 'my-secret', // Optional: protect docs with a password
180
+ scalarConfig: {
181
+ // Scalar UI configuration
182
+ layout: 'modern', // 'modern' or 'classic'
177
183
  theme: 'default',
178
184
  showSidebar: true,
179
- hideTestRequestButton: false
180
- }
185
+ hideTestRequestButton: false,
186
+ },
181
187
  });
182
188
  ```
183
189
 
190
+ | Option | Type | Default | Description |
191
+ | -------------- | ------ | ------------------- | ------------------------------------------------------------------------------------- |
192
+ | `specPath` | string | `'./openapi.json'` | Path to the OpenAPI spec file (relative to cwd) |
193
+ | `password` | string | `DOCS_PASSWORD` env | Password to protect docs. Falls back to `DOCS_PASSWORD` env var. Omit for open access |
194
+ | `scalarConfig` | object | _(defaults)_ | Scalar UI configuration options |
195
+
184
196
  See the [Scalar configuration reference](https://scalar.com/products/api-references/configuration) for all available UI options.
185
197
 
198
+ ### Password Protection
199
+
200
+ API docs reveal your entire endpoint surface — every route, parameter, and response schema. Tejas protects you by default:
201
+
202
+ **In production** (or when `NODE_ENV` is not set), docs are **disabled** unless a password is configured. Visitors see a setup page directing them to set `DOCS_PASSWORD`.
203
+
204
+ **In development** (`NODE_ENV=development`), docs are open without a password for convenience.
205
+
206
+ To enable docs in production, set the `DOCS_PASSWORD` environment variable:
207
+
208
+ ```bash
209
+ DOCS_PASSWORD=my-secret
210
+ ```
211
+
212
+ That's it — no code changes required. When the env var is set, visitors to `/docs` see a password form. After entering the correct password, a cookie-based session grants access for 7 days. Both `/docs` and `/docs/openapi.json` are protected.
213
+
214
+ You can also pass the password explicitly:
215
+
216
+ ```javascript
217
+ app.serveDocs({ password: process.env.DOCS_PASSWORD });
218
+ ```
219
+
220
+ | Environment | Password Set | Behavior |
221
+ | ------------------ | ------------ | ------------------------------------ |
222
+ | development | No | Docs open (no auth) |
223
+ | development | Yes | Login form required |
224
+ | production / unset | No | **Docs disabled** (setup page shown) |
225
+ | production / unset | Yes | Login form required |
226
+
186
227
  ## CLI Commands
187
228
 
188
- | Command | Description |
189
- |---------|-------------|
190
- | `tejas generate:docs` | Interactive OpenAPI generation |
191
- | `tejas generate:docs --ci` | Non-interactive mode (for CI/CD) |
192
- | `tejas docs:on-push` | Generate docs when pushing to production branch |
229
+ | Command | Description |
230
+ | -------------------------- | ----------------------------------------------- |
231
+ | `tejas generate:docs` | Interactive OpenAPI generation |
232
+ | `tejas generate:docs --ci` | Non-interactive mode (for CI/CD) |
233
+ | `tejas docs:on-push` | Generate docs when pushing to production branch |
193
234
 
194
235
  See the [CLI Reference](./cli.md) for full details.
195
236
 
@@ -213,4 +254,3 @@ npx tejas generate:docs
213
254
  - [CLI Reference](./cli.md) — Detailed CLI command documentation
214
255
  - [Configuration](./configuration.md) — Full framework configuration reference
215
256
  - [Routing](./routing.md) — Learn about endpoint metadata
216
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "AI Native Node.js Framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
package/te.js CHANGED
@@ -468,11 +468,13 @@ class Tejas {
468
468
  * @param {Object} [config] - Configuration
469
469
  * @param {string} [config.specPath='./openapi.json'] - Path to the OpenAPI spec JSON file (relative to process.cwd())
470
470
  * @param {object} [config.scalarConfig] - Optional Scalar API Reference config (e.g. { layout: 'modern' } for dialog try-it)
471
+ * @param {string} [config.password] - Optional password to protect docs. Falls back to DOCS_PASSWORD env var. When set, visitors must authenticate via a login form.
471
472
  * @returns {Tejas} The Tejas instance for chaining
472
473
  *
473
474
  * @example
474
475
  * app.serveDocs({ specPath: './openapi.json' });
475
476
  * app.serveDocs({ specPath: './openapi.json', scalarConfig: { layout: 'modern' } });
477
+ * app.serveDocs({ password: process.env.DOCS_PASSWORD });
476
478
  * app.takeoff();
477
479
  */
478
480
  serveDocs(config = {}) {
@@ -481,12 +483,13 @@ class Tejas {
481
483
  config.specPath || './openapi.json',
482
484
  );
483
485
  const { scalarConfig } = config;
486
+ const password = config.password ?? process.env.DOCS_PASSWORD ?? null;
484
487
  const getSpec = async () => {
485
488
  const content = await readFile(specPath, 'utf8');
486
489
  return JSON.parse(content);
487
490
  };
488
491
  registerDocRoutes(
489
- { getSpec, specUrl: '/docs/openapi.json', scalarConfig },
492
+ { getSpec, specUrl: '/docs/openapi.json', scalarConfig, password },
490
493
  targetRegistry,
491
494
  );
492
495
  return this;