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 +6 -0
- package/auto-docs/ui/docs-auth.js +291 -0
- package/auto-docs/ui/docs-ui.js +95 -7
- package/docs/api-reference.md +7 -6
- package/docs/auto-docs.md +88 -48
- package/package.json +1 -1
- package/te.js +4 -1
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
|
+
};
|
package/auto-docs/ui/docs-ui.js
CHANGED
|
@@ -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.
|
|
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.
|
|
216
|
+
sendJson(ammo.res, 200, spec);
|
|
131
217
|
} catch (err) {
|
|
132
|
-
ammo.
|
|
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);
|
package/docs/api-reference.md
CHANGED
|
@@ -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
|
|
83
|
-
| -------------- | ------ |
|
|
84
|
-
| `specPath` | string | `'./openapi.json'`
|
|
85
|
-
| `
|
|
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
|
|
36
|
-
|
|
37
|
-
| **1** | Moderate
|
|
38
|
-
| **2** | High
|
|
39
|
-
| **3** | Comprehensive | Same as level 2, plus post-processing
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
| `dirTargets`
|
|
140
|
-
| `output`
|
|
141
|
-
| `title`
|
|
142
|
-
| `version`
|
|
143
|
-
| `description`
|
|
144
|
-
| `level`
|
|
145
|
-
| `llm`
|
|
146
|
-
| `overviewPath`
|
|
147
|
-
| `productionBranch` | string | `"main"`
|
|
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
|
|
166
|
-
|
|
167
|
-
| `GET /docs`
|
|
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',
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
| `tejas generate:docs`
|
|
191
|
-
| `tejas generate:docs --ci` | Non-interactive mode (for CI/CD)
|
|
192
|
-
| `tejas docs:on-push`
|
|
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
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;
|