scooling-note-outline-smoke.mjs file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:4 fix(security): pin patched transitive deps to clear Dependabot moderate… · aaronrene · Jun 11, 2026
1 import express from 'express';
2
3 const ALLOWED_ENVIRONMENTS = new Set(['local', 'staging']);
4 const FORBIDDEN_REQUEST_HEADERS = ['authorization', 'cookie', 'x-api-key'];
5
6 function normalizeBooleanFlag(value) {
7 return value === true || value === '1' || value === 'true';
8 }
9
10 function configuredEnvironment() {
11 return String(
12 process.env.SCOOLING_NOTE_OUTLINE_SMOKE_ENV ||
13 process.env.KNOWTATION_ENV ||
14 process.env.HUB_ENV ||
15 '',
16 )
17 .trim()
18 .toLowerCase();
19 }
20
21 function smokeEnabled() {
22 return normalizeBooleanFlag(process.env.SCOOLING_NOTE_OUTLINE_SMOKE_ENABLED);
23 }
24
25 function defaultAuthorizationHeader() {
26 const token = String(process.env.SCOOLING_NOTE_OUTLINE_SMOKE_BEARER_TOKEN || '').trim();
27 return token ? `Bearer ${token}` : '';
28 }
29
30 function normalizeVaultRelativePath(rawPath) {
31 if (typeof rawPath !== 'string' || rawPath.trim() === '') {
32 throw new Error('Invalid path');
33 }
34 const forward = rawPath.trim().replace(/\\/g, '/');
35 if (forward.startsWith('/') || /^[A-Za-z]:\//.test(forward)) {
36 throw new Error('Invalid path');
37 }
38 const parts = forward.split('/').filter(Boolean);
39 if (parts.includes('..')) {
40 throw new Error('Invalid path');
41 }
42 return parts.join('/');
43 }
44
45 function normalizeEndpoint(rawEndpoint) {
46 const endpoint = String(rawEndpoint || '').trim();
47 if (!endpoint) {
48 throw new Error('Missing endpoint');
49 }
50 const url = new URL(endpoint);
51 if (url.protocol !== 'http:' && url.protocol !== 'https:') {
52 throw new Error('Invalid endpoint');
53 }
54 if (url.username || url.password || url.search || url.hash) {
55 throw new Error('Invalid endpoint');
56 }
57 return url.toString();
58 }
59
60 function hasForbiddenRequestCredentials(req) {
61 return FORBIDDEN_REQUEST_HEADERS.some((name) => {
62 const value = req.headers[name];
63 return Array.isArray(value) ? value.length > 0 : typeof value === 'string' && value.length > 0;
64 });
65 }
66
67 function sanitizeError(status, error, code) {
68 return {
69 status,
70 body: {
71 error,
72 code,
73 containsRawCredentials: false,
74 returnedBodyText: false,
75 performedWrite: false,
76 },
77 };
78 }
79
80 function safeString(value) {
81 return typeof value === 'string' ? value : '';
82 }
83
84 function isSafeHeading(value) {
85 if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
86 const keys = Object.keys(value);
87 return (
88 keys.length === 3 &&
89 Number.isInteger(value.level) &&
90 value.level >= 1 &&
91 value.level <= 6 &&
92 typeof value.text === 'string' &&
93 typeof value.id === 'string'
94 );
95 }
96
97 function sanitizeNoteOutline(payload, requestedPath) {
98 if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
99 throw new Error('Invalid NoteOutline');
100 }
101 if (payload.schema !== 'knowtation.note_outline/v1') {
102 throw new Error('Invalid NoteOutline');
103 }
104 if (payload.path !== requestedPath) {
105 throw new Error('Invalid NoteOutline');
106 }
107 if (!Array.isArray(payload.headings) || !payload.headings.every(isSafeHeading)) {
108 throw new Error('Invalid NoteOutline');
109 }
110 if (typeof payload.truncated !== 'boolean') {
111 throw new Error('Invalid NoteOutline');
112 }
113
114 return {
115 schema: 'knowtation.note_outline/v1',
116 path: requestedPath,
117 title: payload.title === null ? null : safeString(payload.title),
118 headings: payload.headings.map((heading) => ({
119 level: heading.level,
120 text: heading.text,
121 id: heading.id,
122 })),
123 truncated: payload.truncated,
124 };
125 }
126
127 function mapUpstreamStatus(status) {
128 if (status === 401 || status === 403) {
129 return sanitizeError(403, 'Forbidden', 'FORBIDDEN');
130 }
131 if (status === 404) {
132 return sanitizeError(404, 'Not found', 'NOT_FOUND');
133 }
134 return sanitizeError(502, 'Bad Gateway', 'BAD_GATEWAY');
135 }
136
137 async function readBridgeNoteOutline({ endpoint, authorizationHeader, fetchImpl, requestedPath }) {
138 const upstreamUrl = new URL(endpoint);
139 upstreamUrl.searchParams.set('path', requestedPath);
140
141 const response = await fetchImpl(upstreamUrl.toString(), {
142 method: 'GET',
143 headers: {
144 Accept: 'application/json',
145 Authorization: authorizationHeader,
146 },
147 });
148
149 if (!response.ok) {
150 throw mapUpstreamStatus(response.status);
151 }
152
153 let payload;
154 try {
155 payload = await response.json();
156 } catch (_) {
157 throw sanitizeError(502, 'Bad Gateway', 'BAD_GATEWAY');
158 }
159
160 try {
161 return sanitizeNoteOutline(payload, requestedPath);
162 } catch (_) {
163 throw sanitizeError(502, 'Bad Gateway', 'BAD_GATEWAY');
164 }
165 }
166
167 function createScoolingNoteOutlineSmokeRouter({
168 upstreamEndpoint = process.env.SCOOLING_NOTE_OUTLINE_SMOKE_UPSTREAM || '',
169 authorizationHeader = defaultAuthorizationHeader,
170 fetchImpl = globalThis.fetch,
171 isEnabled = smokeEnabled,
172 environment = configuredEnvironment,
173 } = {}) {
174 const router = express.Router();
175
176 router.get('/scooling/note-outline/smoke', async (req, res) => {
177 if (!isEnabled() || !ALLOWED_ENVIRONMENTS.has(environment())) {
178 return res.status(404).json({ error: 'Not found', code: 'NOT_FOUND' });
179 }
180 if (hasForbiddenRequestCredentials(req)) {
181 return res
182 .status(400)
183 .json(sanitizeError(400, 'Raw credentials are not accepted.', 'BAD_REQUEST').body);
184 }
185
186 let requestedPath;
187 let endpoint;
188 let authHeader;
189 try {
190 requestedPath = normalizeVaultRelativePath(req.query.path);
191 } catch (_) {
192 const err = sanitizeError(400, 'Invalid NoteOutline smoke request.', 'BAD_REQUEST');
193 return res.status(err.status).json(err.body);
194 }
195
196 try {
197 endpoint = normalizeEndpoint(upstreamEndpoint);
198 authHeader = authorizationHeader();
199 if (typeof authHeader !== 'string' || !authHeader.startsWith('Bearer ') || authHeader.length <= 7) {
200 throw new Error('Missing authorization');
201 }
202 } catch (_) {
203 const err = sanitizeError(503, 'NoteOutline smoke bridge is unavailable.', 'SERVICE_UNAVAILABLE');
204 return res.status(err.status).json(err.body);
205 }
206
207 try {
208 const outline = await readBridgeNoteOutline({
209 endpoint,
210 authorizationHeader: authHeader,
211 fetchImpl,
212 requestedPath,
213 });
214 return res.json(outline);
215 } catch (error) {
216 const sanitized = error && typeof error === 'object' && error.body ? error : sanitizeError(502, 'Bad Gateway', 'BAD_GATEWAY');
217 return res.status(sanitized.status).json(sanitized.body);
218 }
219 });
220
221 return router;
222 }
223
224 export {
225 createScoolingNoteOutlineSmokeRouter,
226 normalizeVaultRelativePath,
227 sanitizeNoteOutline,
228 };