@oscharko-dev/test-intelligence 0.0.1-beta.0 → 0.1.0-beta.1
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/CODE_OF_CONDUCT.md +22 -0
- package/CONTRIBUTING.md +38 -0
- package/GOVERNANCE.md +36 -0
- package/LICENSE +199 -0
- package/NOTICE +12 -0
- package/README.md +71 -10
- package/SECURITY.md +64 -0
- package/SUPPORT.md +18 -0
- package/dist/cli.cjs +66559 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +31 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +66532 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts/index.cjs +2087 -0
- package/dist/contracts/index.cjs.map +1 -0
- package/dist/contracts/index.d.cts +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1665 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/index.cjs +1255 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +252 -0
- package/dist/index.d.ts +252 -0
- package/dist/index.js +1250 -0
- package/dist/index.js.map +1 -0
- package/dist/server-entrypoint.cjs +1645 -0
- package/dist/server-entrypoint.cjs.map +1 -0
- package/dist/server-entrypoint.d.cts +102 -0
- package/dist/server-entrypoint.d.ts +102 -0
- package/dist/server-entrypoint.js +1637 -0
- package/dist/server-entrypoint.js.map +1 -0
- package/package.json +156 -9
- package/index.js +0 -8
package/dist/index.js
ADDED
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { createHash, timingSafeEqual, randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
createRequire(import.meta.url);
|
|
6
|
+
|
|
7
|
+
// packages/server/src/package-identity.ts
|
|
8
|
+
var PACKAGE_NAME = "@oscharko-dev/test-intelligence";
|
|
9
|
+
var PACKAGE_VERSION = "0.0.1-beta.0";
|
|
10
|
+
function resolveReleaseStage(version) {
|
|
11
|
+
const separatorIndex = version.indexOf("-");
|
|
12
|
+
if (separatorIndex === -1) {
|
|
13
|
+
return "stable";
|
|
14
|
+
}
|
|
15
|
+
const preRelease = version.slice(separatorIndex + 1);
|
|
16
|
+
return preRelease.startsWith("beta") ? "beta" : "pre-beta";
|
|
17
|
+
}
|
|
18
|
+
function getPackageIdentity() {
|
|
19
|
+
return {
|
|
20
|
+
name: PACKAGE_NAME,
|
|
21
|
+
version: PACKAGE_VERSION,
|
|
22
|
+
stage: resolveReleaseStage(PACKAGE_VERSION)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// packages/contracts/dist/index.js
|
|
27
|
+
var TEST_INTELLIGENCE_ENV = "TEST_INTELLIGENCE_ENABLED";
|
|
28
|
+
|
|
29
|
+
// packages/server/src/constants.ts
|
|
30
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
31
|
+
var DEFAULT_PORT = 1983;
|
|
32
|
+
var DEFAULT_RATE_LIMIT_PER_MINUTE = 60;
|
|
33
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
34
|
+
var MAX_REQUEST_BODY_BYTES = 1048576;
|
|
35
|
+
var MAX_SUBMIT_BODY_BYTES = 8388608;
|
|
36
|
+
var ENABLE_HSTS_ENV = "TEST_INTELLIGENCE_ENABLE_HSTS";
|
|
37
|
+
var DEFAULT_STRICT_TRANSPORT_SECURITY = "max-age=31536000";
|
|
38
|
+
var DEFAULT_CONTENT_SECURITY_POLICY = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
|
39
|
+
var API_ROUTE_PREFIX = "/api/v1";
|
|
40
|
+
var resolveTestIntelligenceEnabled = (env = process.env) => {
|
|
41
|
+
return parseBooleanEnv(env[TEST_INTELLIGENCE_ENV]);
|
|
42
|
+
};
|
|
43
|
+
var resolveStrictTransportSecurity = (env = process.env) => {
|
|
44
|
+
const raw = env[ENABLE_HSTS_ENV];
|
|
45
|
+
if (raw === void 0) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
const normalized = raw.trim().toLowerCase();
|
|
49
|
+
if (normalized === "" || normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
return DEFAULT_STRICT_TRANSPORT_SECURITY;
|
|
53
|
+
};
|
|
54
|
+
var parseBooleanEnv = (raw) => {
|
|
55
|
+
if (raw === void 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const normalized = raw.trim().toLowerCase();
|
|
59
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// packages/server/src/error-codes.ts
|
|
63
|
+
var statusForErrorCode = (code) => {
|
|
64
|
+
switch (code) {
|
|
65
|
+
case "BAD_REQUEST":
|
|
66
|
+
return 400;
|
|
67
|
+
case "UNAUTHORIZED":
|
|
68
|
+
case "BEARER_TOKEN_MISSING":
|
|
69
|
+
return 401;
|
|
70
|
+
case "FORBIDDEN_REQUEST_ORIGIN":
|
|
71
|
+
case "FEATURE_GATE_DISABLED":
|
|
72
|
+
return 403;
|
|
73
|
+
case "NOT_FOUND":
|
|
74
|
+
return 404;
|
|
75
|
+
case "METHOD_NOT_ALLOWED":
|
|
76
|
+
return 405;
|
|
77
|
+
case "PAYLOAD_TOO_LARGE":
|
|
78
|
+
return 413;
|
|
79
|
+
case "UNSUPPORTED_MEDIA_TYPE":
|
|
80
|
+
return 415;
|
|
81
|
+
case "RATE_LIMITED":
|
|
82
|
+
return 429;
|
|
83
|
+
case "INTERNAL_ERROR":
|
|
84
|
+
case "VERIFICATION_FAILED":
|
|
85
|
+
return 500;
|
|
86
|
+
case "AUTHENTICATION_UNAVAILABLE":
|
|
87
|
+
case "LLM_GATEWAY_UNCONFIGURED":
|
|
88
|
+
case "LLM_GATEWAY_FAILED":
|
|
89
|
+
return 503;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// packages/server/src/http-helpers.ts
|
|
94
|
+
var applySecurityHeaders = (response, options = {}) => {
|
|
95
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
96
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
97
|
+
response.setHeader(
|
|
98
|
+
"Content-Security-Policy",
|
|
99
|
+
options.contentSecurityPolicy ?? DEFAULT_CONTENT_SECURITY_POLICY
|
|
100
|
+
);
|
|
101
|
+
const hsts = options.strictTransportSecurity ?? resolveStrictTransportSecurity();
|
|
102
|
+
if (hsts !== void 0) {
|
|
103
|
+
response.setHeader("Strict-Transport-Security", hsts);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var writeJsonResponse = ({
|
|
107
|
+
response,
|
|
108
|
+
statusCode,
|
|
109
|
+
payload,
|
|
110
|
+
extraHeaders
|
|
111
|
+
}) => {
|
|
112
|
+
applySecurityHeaders(response);
|
|
113
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
114
|
+
if (extraHeaders !== void 0) {
|
|
115
|
+
for (const [name, value] of Object.entries(extraHeaders)) {
|
|
116
|
+
response.setHeader(name, value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
response.statusCode = statusCode;
|
|
120
|
+
response.end(JSON.stringify(payload));
|
|
121
|
+
};
|
|
122
|
+
var writeErrorResponse = ({
|
|
123
|
+
response,
|
|
124
|
+
code,
|
|
125
|
+
message,
|
|
126
|
+
extraHeaders,
|
|
127
|
+
statusCode
|
|
128
|
+
}) => {
|
|
129
|
+
const envelope = { error: code, message };
|
|
130
|
+
writeJsonResponse({
|
|
131
|
+
response,
|
|
132
|
+
statusCode: statusCode ?? statusForErrorCode(code),
|
|
133
|
+
payload: envelope,
|
|
134
|
+
...extraHeaders !== void 0 ? { extraHeaders } : {}
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
var readJsonBody = async ({
|
|
138
|
+
request,
|
|
139
|
+
maxBytes
|
|
140
|
+
}) => {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
let received = 0;
|
|
143
|
+
for await (const chunk of request) {
|
|
144
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
145
|
+
received += buf.length;
|
|
146
|
+
if (received > maxBytes) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
150
|
+
message: `Request body exceeds ${maxBytes} bytes.`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
chunks.push(buf);
|
|
154
|
+
}
|
|
155
|
+
if (chunks.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
code: "BAD_REQUEST",
|
|
159
|
+
message: "Request body is empty."
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
163
|
+
try {
|
|
164
|
+
return { ok: true, value: JSON.parse(text) };
|
|
165
|
+
} catch {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
code: "BAD_REQUEST",
|
|
169
|
+
message: "Request body is not valid JSON."
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var beginSseResponse = (response, options = {}) => {
|
|
174
|
+
applySecurityHeaders(response, options);
|
|
175
|
+
response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
176
|
+
response.setHeader("Cache-Control", "no-cache, no-transform");
|
|
177
|
+
response.setHeader("Connection", "keep-alive");
|
|
178
|
+
response.statusCode = 200;
|
|
179
|
+
response.flushHeaders();
|
|
180
|
+
return {
|
|
181
|
+
writeEvent({ event, data, id }) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
if (id !== void 0) {
|
|
184
|
+
lines.push(`id: ${id}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push(`event: ${event}`);
|
|
187
|
+
const serialized = JSON.stringify(data);
|
|
188
|
+
for (const line of serialized.split("\n")) {
|
|
189
|
+
lines.push(`data: ${line}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push("", "");
|
|
192
|
+
response.write(lines.join("\n"));
|
|
193
|
+
},
|
|
194
|
+
writeRetry(retryMs) {
|
|
195
|
+
response.write(`retry: ${String(retryMs)}
|
|
196
|
+
|
|
197
|
+
`);
|
|
198
|
+
},
|
|
199
|
+
end() {
|
|
200
|
+
response.end();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
var FORWARDED_FOR_HEADER = "x-forwarded-for";
|
|
205
|
+
var resolveClientKey = (request) => {
|
|
206
|
+
const forwarded = request.headers[FORWARDED_FOR_HEADER];
|
|
207
|
+
const raw = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
208
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
209
|
+
const first = raw.split(",")[0]?.trim();
|
|
210
|
+
if (first !== void 0 && first.length > 0) {
|
|
211
|
+
return first;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return request.socket.remoteAddress ?? "unknown";
|
|
215
|
+
};
|
|
216
|
+
var createRequestLogger = (logger) => {
|
|
217
|
+
return {
|
|
218
|
+
log({ requestId, method, path, statusCode, durationMs, clientKey, route }) {
|
|
219
|
+
const segments = [
|
|
220
|
+
`method=${method}`,
|
|
221
|
+
`path=${path}`,
|
|
222
|
+
`status=${String(statusCode)}`,
|
|
223
|
+
`durationMs=${String(durationMs)}`,
|
|
224
|
+
`client=${clientKey}`
|
|
225
|
+
];
|
|
226
|
+
if (route !== void 0) {
|
|
227
|
+
segments.push(`route=${route}`);
|
|
228
|
+
}
|
|
229
|
+
logger.log({
|
|
230
|
+
level: statusCode >= 500 ? "error" : "info",
|
|
231
|
+
message: segments.join(" "),
|
|
232
|
+
requestId,
|
|
233
|
+
method,
|
|
234
|
+
path,
|
|
235
|
+
statusCode
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
newRequestId() {
|
|
239
|
+
return randomUUID();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
var createAuditLogger = ({
|
|
244
|
+
write,
|
|
245
|
+
now = () => (/* @__PURE__ */ new Date()).toISOString()
|
|
246
|
+
}) => {
|
|
247
|
+
return {
|
|
248
|
+
record(event) {
|
|
249
|
+
const line = JSON.stringify({
|
|
250
|
+
ts: now(),
|
|
251
|
+
action: event.action,
|
|
252
|
+
subject: event.subject,
|
|
253
|
+
outcome: event.outcome,
|
|
254
|
+
...event.principal !== void 0 ? { principal: event.principal } : {},
|
|
255
|
+
...event.requestId !== void 0 ? { requestId: event.requestId } : {},
|
|
256
|
+
...event.details !== void 0 ? { details: event.details } : {}
|
|
257
|
+
});
|
|
258
|
+
write(`${line}
|
|
259
|
+
`);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// packages/server/src/rate-limit-store.ts
|
|
265
|
+
var RateLimitStore = class {
|
|
266
|
+
buckets = /* @__PURE__ */ new Map();
|
|
267
|
+
get(key) {
|
|
268
|
+
return this.buckets.get(key);
|
|
269
|
+
}
|
|
270
|
+
set(key, bucket) {
|
|
271
|
+
this.buckets.set(key, bucket);
|
|
272
|
+
}
|
|
273
|
+
delete(key) {
|
|
274
|
+
this.buckets.delete(key);
|
|
275
|
+
}
|
|
276
|
+
clear() {
|
|
277
|
+
this.buckets.clear();
|
|
278
|
+
}
|
|
279
|
+
size() {
|
|
280
|
+
return this.buckets.size;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// packages/server/src/rate-limit.ts
|
|
285
|
+
var createRateLimiter = ({
|
|
286
|
+
requestsPerMinute,
|
|
287
|
+
store = new RateLimitStore()
|
|
288
|
+
}) => {
|
|
289
|
+
if (!Number.isFinite(requestsPerMinute) || requestsPerMinute < 1) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"createRateLimiter: requestsPerMinute must be a finite integer >= 1."
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const limit = Math.floor(requestsPerMinute);
|
|
295
|
+
return {
|
|
296
|
+
check({ clientKey, routeKey, nowMs }) {
|
|
297
|
+
const key = `${clientKey}\0${routeKey}`;
|
|
298
|
+
const existing = store.get(key);
|
|
299
|
+
if (existing === void 0 || nowMs - existing.windowStartedAtMs >= RATE_LIMIT_WINDOW_MS) {
|
|
300
|
+
store.set(key, { count: 1, windowStartedAtMs: nowMs });
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
remaining: limit - 1,
|
|
304
|
+
resetAtMs: nowMs + RATE_LIMIT_WINDOW_MS
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (existing.count >= limit) {
|
|
308
|
+
const resetAtMs = existing.windowStartedAtMs + RATE_LIMIT_WINDOW_MS;
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
retryAfterSeconds: Math.max(1, Math.ceil((resetAtMs - nowMs) / 1e3)),
|
|
312
|
+
resetAtMs
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
existing.count += 1;
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
remaining: Math.max(0, limit - existing.count),
|
|
319
|
+
resetAtMs: existing.windowStartedAtMs + RATE_LIMIT_WINDOW_MS
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
reset() {
|
|
323
|
+
store.clear();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
var JSON_CONTENT_TYPE_PATTERN = /^application\/json(?:\s*(?:;|$))/i;
|
|
328
|
+
var ALLOWED_SEC_FETCH_SITE_VALUES = /* @__PURE__ */ new Set(["same-origin", "same-site"]);
|
|
329
|
+
var TEST_INTELLIGENCE_BEARER_REALM = "test-intelligence";
|
|
330
|
+
var getHeaderValue = (value) => {
|
|
331
|
+
if (typeof value === "string") {
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
if (Array.isArray(value)) {
|
|
335
|
+
return value[0];
|
|
336
|
+
}
|
|
337
|
+
return void 0;
|
|
338
|
+
};
|
|
339
|
+
var normalizeOrigin = (value) => {
|
|
340
|
+
if (value === void 0 || value.trim().length === 0) {
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
return new URL(value).origin;
|
|
345
|
+
} catch {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var normalizeOriginHost = (host) => {
|
|
350
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
351
|
+
return `[${host}]`;
|
|
352
|
+
}
|
|
353
|
+
return host;
|
|
354
|
+
};
|
|
355
|
+
var isLoopbackLikeHost = (host) => {
|
|
356
|
+
const normalized = host.trim().toLowerCase();
|
|
357
|
+
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "[::1]" || normalized === "0.0.0.0" || normalized === "::" || normalized === "[::]";
|
|
358
|
+
};
|
|
359
|
+
var getAllowedWriteOrigins = ({
|
|
360
|
+
host,
|
|
361
|
+
port
|
|
362
|
+
}) => {
|
|
363
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
364
|
+
`http://${normalizeOriginHost(host)}:${port}`
|
|
365
|
+
]);
|
|
366
|
+
if (isLoopbackLikeHost(host)) {
|
|
367
|
+
allowed.add(`http://127.0.0.1:${port}`);
|
|
368
|
+
allowed.add(`http://localhost:${port}`);
|
|
369
|
+
allowed.add(`http://[::1]:${port}`);
|
|
370
|
+
}
|
|
371
|
+
return allowed;
|
|
372
|
+
};
|
|
373
|
+
var validateWriteRequest = ({
|
|
374
|
+
request,
|
|
375
|
+
host,
|
|
376
|
+
port
|
|
377
|
+
}) => {
|
|
378
|
+
const contentType = getHeaderValue(request.headers["content-type"]);
|
|
379
|
+
if (contentType === void 0 || !JSON_CONTENT_TYPE_PATTERN.test(contentType)) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
statusCode: 415,
|
|
383
|
+
payload: {
|
|
384
|
+
error: "UNSUPPORTED_MEDIA_TYPE",
|
|
385
|
+
message: "Write routes require 'Content-Type: application/json'."
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const originHeader = getHeaderValue(request.headers.origin);
|
|
390
|
+
const refererHeader = getHeaderValue(request.headers.referer);
|
|
391
|
+
const secFetchSite = getHeaderValue(request.headers["sec-fetch-site"])?.trim().toLowerCase();
|
|
392
|
+
const allowedOrigins = getAllowedWriteOrigins({ host, port });
|
|
393
|
+
if (secFetchSite !== void 0 && !ALLOWED_SEC_FETCH_SITE_VALUES.has(secFetchSite)) {
|
|
394
|
+
return forbiddenOrigin(
|
|
395
|
+
"Cross-site browser requests to test-intelligence write routes are blocked."
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (originHeader !== void 0) {
|
|
399
|
+
const origin = normalizeOrigin(originHeader);
|
|
400
|
+
if (origin === void 0 || !allowedOrigins.has(origin)) {
|
|
401
|
+
return forbiddenOrigin(
|
|
402
|
+
"Only same-origin browser requests may access test-intelligence write routes."
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (refererHeader !== void 0) {
|
|
407
|
+
const refererOrigin = normalizeOrigin(refererHeader);
|
|
408
|
+
if (refererOrigin === void 0 || !allowedOrigins.has(refererOrigin)) {
|
|
409
|
+
return forbiddenOrigin(
|
|
410
|
+
"Only same-origin browser requests may access test-intelligence write routes."
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { ok: true };
|
|
415
|
+
};
|
|
416
|
+
var forbiddenOrigin = (message) => ({
|
|
417
|
+
ok: false,
|
|
418
|
+
statusCode: 403,
|
|
419
|
+
payload: { error: "FORBIDDEN_REQUEST_ORIGIN", message }
|
|
420
|
+
});
|
|
421
|
+
var normalizeConfiguredBearerToken = (value) => {
|
|
422
|
+
if (typeof value !== "string") {
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
425
|
+
const trimmed = value.trim();
|
|
426
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
427
|
+
};
|
|
428
|
+
var readBearerToken = (request) => {
|
|
429
|
+
const authorization = getHeaderValue(request.headers.authorization);
|
|
430
|
+
if (authorization === void 0) {
|
|
431
|
+
return void 0;
|
|
432
|
+
}
|
|
433
|
+
const expectedScheme = "bearer";
|
|
434
|
+
if (authorization.length <= expectedScheme.length) {
|
|
435
|
+
return void 0;
|
|
436
|
+
}
|
|
437
|
+
for (let index = 0; index < expectedScheme.length; index += 1) {
|
|
438
|
+
const code = authorization.charCodeAt(index);
|
|
439
|
+
const lowered = code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : authorization[index];
|
|
440
|
+
if (lowered !== expectedScheme[index]) {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
let tokenStart = expectedScheme.length;
|
|
445
|
+
while (tokenStart < authorization.length) {
|
|
446
|
+
const code = authorization.charCodeAt(tokenStart);
|
|
447
|
+
if (code !== 32 && code !== 9) {
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
tokenStart += 1;
|
|
451
|
+
}
|
|
452
|
+
if (tokenStart === expectedScheme.length || tokenStart >= authorization.length) {
|
|
453
|
+
return void 0;
|
|
454
|
+
}
|
|
455
|
+
let tokenEnd = authorization.length;
|
|
456
|
+
while (tokenEnd > tokenStart) {
|
|
457
|
+
const code = authorization.charCodeAt(tokenEnd - 1);
|
|
458
|
+
if (code !== 32 && code !== 9) {
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
tokenEnd -= 1;
|
|
462
|
+
}
|
|
463
|
+
return tokenEnd > tokenStart ? authorization.slice(tokenStart, tokenEnd) : void 0;
|
|
464
|
+
};
|
|
465
|
+
var tokensMatch = (expected, candidate) => {
|
|
466
|
+
const expectedDigest = createHash("sha256").update(expected, "utf8").digest();
|
|
467
|
+
const candidateDigest = createHash("sha256").update(candidate, "utf8").digest();
|
|
468
|
+
return timingSafeEqual(expectedDigest, candidateDigest);
|
|
469
|
+
};
|
|
470
|
+
var validateBearerToken = ({
|
|
471
|
+
request,
|
|
472
|
+
bearerToken,
|
|
473
|
+
routeLabel
|
|
474
|
+
}) => {
|
|
475
|
+
const configuredToken = normalizeConfiguredBearerToken(bearerToken);
|
|
476
|
+
if (configuredToken === void 0) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
statusCode: 503,
|
|
480
|
+
payload: {
|
|
481
|
+
error: "AUTHENTICATION_UNAVAILABLE",
|
|
482
|
+
message: `${routeLabel} writes are disabled until server bearer authentication is configured.`
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const receivedToken = readBearerToken(request);
|
|
487
|
+
if (receivedToken !== void 0 && tokensMatch(configuredToken, receivedToken)) {
|
|
488
|
+
return { ok: true, principal: { scheme: "bearer" } };
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
statusCode: 401,
|
|
493
|
+
payload: {
|
|
494
|
+
error: "UNAUTHORIZED",
|
|
495
|
+
message: `${routeLabel} writes require a valid Bearer token.`
|
|
496
|
+
},
|
|
497
|
+
wwwAuthenticate: `Bearer realm="${TEST_INTELLIGENCE_BEARER_REALM}"`
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
var STABLE_SEGMENT_RE = /^[A-Za-z0-9_.-]{1,128}$/u;
|
|
501
|
+
var isSafeIdSegment = (segment) => {
|
|
502
|
+
if (!STABLE_SEGMENT_RE.test(segment)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
if (segment === "." || segment === "..") {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
};
|
|
510
|
+
var stripTrailingSlash = (pathname) => {
|
|
511
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
512
|
+
return pathname.slice(0, -1);
|
|
513
|
+
}
|
|
514
|
+
return pathname;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// packages/server/src/test-intelligence-routes.ts
|
|
518
|
+
var parseTestIntelligenceRoute = ({
|
|
519
|
+
method,
|
|
520
|
+
pathname
|
|
521
|
+
}) => {
|
|
522
|
+
const upper = method.toUpperCase();
|
|
523
|
+
if (upper === "OPTIONS") {
|
|
524
|
+
return { ok: true, route: { kind: "cors_preflight" } };
|
|
525
|
+
}
|
|
526
|
+
const normalized = stripTrailingSlash(pathname);
|
|
527
|
+
if (normalized === "/healthz") {
|
|
528
|
+
return methodGuard(upper, "GET", { kind: "healthz" });
|
|
529
|
+
}
|
|
530
|
+
if (normalized === "/readyz") {
|
|
531
|
+
return methodGuard(upper, "GET", { kind: "readyz" });
|
|
532
|
+
}
|
|
533
|
+
if (normalized === "/openapi.json") {
|
|
534
|
+
return methodGuard(upper, "GET", { kind: "openapi" });
|
|
535
|
+
}
|
|
536
|
+
if (!normalized.startsWith(`${API_ROUTE_PREFIX}/`)) {
|
|
537
|
+
return { ok: false, reason: "unknown_route" };
|
|
538
|
+
}
|
|
539
|
+
const rest = normalized.slice(API_ROUTE_PREFIX.length + 1);
|
|
540
|
+
const segments = rest.split("/");
|
|
541
|
+
return dispatchSegments(upper, segments);
|
|
542
|
+
};
|
|
543
|
+
var methodGuard = (method, allowed, route) => {
|
|
544
|
+
if (method !== allowed) {
|
|
545
|
+
return {
|
|
546
|
+
ok: false,
|
|
547
|
+
reason: "method_not_allowed",
|
|
548
|
+
allowedMethods: [allowed]
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return { ok: true, route };
|
|
552
|
+
};
|
|
553
|
+
var dispatchSegments = (method, segments) => {
|
|
554
|
+
if (segments.length === 0 || segments[0] === "") {
|
|
555
|
+
return { ok: false, reason: "unknown_route" };
|
|
556
|
+
}
|
|
557
|
+
const head = segments[0];
|
|
558
|
+
const tail = segments.slice(1);
|
|
559
|
+
switch (head) {
|
|
560
|
+
case "jobs":
|
|
561
|
+
return dispatchJobs(method, tail);
|
|
562
|
+
case "review":
|
|
563
|
+
return dispatchReview(method, tail);
|
|
564
|
+
case "tms":
|
|
565
|
+
return dispatchTms(method, tail);
|
|
566
|
+
case "execution":
|
|
567
|
+
return dispatchExecution(method, tail);
|
|
568
|
+
case "onboard":
|
|
569
|
+
return dispatchSingleton(method, tail, "POST", { kind: "onboard" });
|
|
570
|
+
case "figma-export":
|
|
571
|
+
return dispatchSingleton(method, tail, "POST", { kind: "figma_export" });
|
|
572
|
+
default:
|
|
573
|
+
return { ok: false, reason: "unknown_route" };
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
var dispatchJobs = (method, tail) => {
|
|
577
|
+
if (tail.length === 0) {
|
|
578
|
+
return methodGuard(method, "POST", { kind: "submit_job" });
|
|
579
|
+
}
|
|
580
|
+
const jobId = tail[0];
|
|
581
|
+
if (jobId === "") {
|
|
582
|
+
return { ok: false, reason: "empty_segment" };
|
|
583
|
+
}
|
|
584
|
+
if (!isSafeIdSegment(jobId)) {
|
|
585
|
+
return { ok: false, reason: "unsafe_id_segment" };
|
|
586
|
+
}
|
|
587
|
+
if (tail.length === 1) {
|
|
588
|
+
return methodGuard(method, "GET", { kind: "job_status", jobId });
|
|
589
|
+
}
|
|
590
|
+
if (tail.length === 2 && tail[1] === "events") {
|
|
591
|
+
return methodGuard(method, "GET", { kind: "job_events", jobId });
|
|
592
|
+
}
|
|
593
|
+
if (tail.length === 3 && tail[2] === "verify") {
|
|
594
|
+
return jobsVerifySubroute(method, jobId, tail[1]);
|
|
595
|
+
}
|
|
596
|
+
return { ok: false, reason: "unknown_route" };
|
|
597
|
+
};
|
|
598
|
+
var jobsVerifySubroute = (method, jobId, subject) => {
|
|
599
|
+
switch (subject) {
|
|
600
|
+
case "evidence":
|
|
601
|
+
return methodGuard(method, "POST", { kind: "verify_evidence", jobId });
|
|
602
|
+
case "audit-dossier":
|
|
603
|
+
return methodGuard(method, "POST", {
|
|
604
|
+
kind: "verify_audit_dossier",
|
|
605
|
+
jobId
|
|
606
|
+
});
|
|
607
|
+
case "provenance":
|
|
608
|
+
return methodGuard(method, "POST", { kind: "verify_provenance", jobId });
|
|
609
|
+
case "seal":
|
|
610
|
+
return methodGuard(method, "POST", { kind: "verify_seal", jobId });
|
|
611
|
+
default:
|
|
612
|
+
return { ok: false, reason: "unknown_route" };
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
var dispatchReview = (method, tail) => {
|
|
616
|
+
if (tail.length === 0) {
|
|
617
|
+
return { ok: false, reason: "unknown_route" };
|
|
618
|
+
}
|
|
619
|
+
const jobId = tail[0];
|
|
620
|
+
if (jobId === "") {
|
|
621
|
+
return { ok: false, reason: "empty_segment" };
|
|
622
|
+
}
|
|
623
|
+
if (!isSafeIdSegment(jobId)) {
|
|
624
|
+
return { ok: false, reason: "unsafe_id_segment" };
|
|
625
|
+
}
|
|
626
|
+
if (tail.length === 1) {
|
|
627
|
+
return methodGuard(method, "GET", { kind: "review_snapshot", jobId });
|
|
628
|
+
}
|
|
629
|
+
if (tail.length === 2 && tail[1] === "decision") {
|
|
630
|
+
return methodGuard(method, "POST", { kind: "review_decision", jobId });
|
|
631
|
+
}
|
|
632
|
+
return { ok: false, reason: "unknown_route" };
|
|
633
|
+
};
|
|
634
|
+
var dispatchTms = (method, tail) => {
|
|
635
|
+
return dispatchSingleton(method, tail, "POST", { kind: "tms_push" }, [
|
|
636
|
+
"push"
|
|
637
|
+
]);
|
|
638
|
+
};
|
|
639
|
+
var dispatchExecution = (method, tail) => {
|
|
640
|
+
return dispatchSingleton(method, tail, "POST", { kind: "execution_pull" }, [
|
|
641
|
+
"pull"
|
|
642
|
+
]);
|
|
643
|
+
};
|
|
644
|
+
var dispatchSingleton = (method, tail, allowedMethod, route, expectedTail = []) => {
|
|
645
|
+
if (tail.length !== expectedTail.length) {
|
|
646
|
+
return { ok: false, reason: "unknown_route" };
|
|
647
|
+
}
|
|
648
|
+
for (let i = 0; i < expectedTail.length; i += 1) {
|
|
649
|
+
if (tail[i] !== expectedTail[i]) {
|
|
650
|
+
return { ok: false, reason: "unknown_route" };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return methodGuard(method, allowedMethod, route);
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// packages/server/src/route-handlers.ts
|
|
657
|
+
var buildHandlerTable = (options) => {
|
|
658
|
+
const defaults = {
|
|
659
|
+
healthz: handleHealthz,
|
|
660
|
+
readyz: ({ response }) => handleReadyz(response, options),
|
|
661
|
+
openapi: handleOpenapi,
|
|
662
|
+
cors_preflight: ({ response }) => handleCorsPreflight(response, options),
|
|
663
|
+
submit_job: handleNotImplemented("submit_job"),
|
|
664
|
+
job_status: handleNotImplemented("job_status"),
|
|
665
|
+
job_events: handleSseStub,
|
|
666
|
+
verify_evidence: handleNotImplemented("verify_evidence"),
|
|
667
|
+
verify_audit_dossier: handleNotImplemented("verify_audit_dossier"),
|
|
668
|
+
verify_provenance: handleNotImplemented("verify_provenance"),
|
|
669
|
+
verify_seal: handleNotImplemented("verify_seal"),
|
|
670
|
+
review_snapshot: handleNotImplemented("review_snapshot"),
|
|
671
|
+
review_decision: handleNotImplemented("review_decision"),
|
|
672
|
+
tms_push: handleNotImplemented("tms_push"),
|
|
673
|
+
execution_pull: handleNotImplemented("execution_pull"),
|
|
674
|
+
onboard: handleNotImplemented("onboard"),
|
|
675
|
+
figma_export: handleNotImplemented("figma_export")
|
|
676
|
+
};
|
|
677
|
+
const overrides = options.routeHandlers ?? {};
|
|
678
|
+
for (const [kind, handler] of Object.entries(overrides)) {
|
|
679
|
+
defaults[kind] = handler;
|
|
680
|
+
}
|
|
681
|
+
return defaults;
|
|
682
|
+
};
|
|
683
|
+
var handleHealthz = ({ response }) => {
|
|
684
|
+
writeJsonResponse({
|
|
685
|
+
response,
|
|
686
|
+
statusCode: 200,
|
|
687
|
+
payload: { status: "ok", checkedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
688
|
+
});
|
|
689
|
+
};
|
|
690
|
+
var handleReadyz = (response, options) => {
|
|
691
|
+
writeJsonResponse({
|
|
692
|
+
response,
|
|
693
|
+
statusCode: 200,
|
|
694
|
+
payload: {
|
|
695
|
+
status: "ready",
|
|
696
|
+
featureGate: options.testIntelligenceEnabled ? "enabled" : "disabled",
|
|
697
|
+
authConfigured: options.bearerToken !== void 0,
|
|
698
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
var handleOpenapi = ({ response }) => {
|
|
703
|
+
writeJsonResponse({
|
|
704
|
+
response,
|
|
705
|
+
statusCode: 200,
|
|
706
|
+
payload: {
|
|
707
|
+
openapi: "3.1.0",
|
|
708
|
+
info: { title: "Test Intelligence API", version: "0.0.1" },
|
|
709
|
+
paths: {}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
var handleCorsPreflight = (response, options) => {
|
|
714
|
+
const allowedOrigin = options.allowedCorsOrigins?.[0];
|
|
715
|
+
if (allowedOrigin !== void 0) {
|
|
716
|
+
response.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
717
|
+
}
|
|
718
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
719
|
+
response.setHeader(
|
|
720
|
+
"Access-Control-Allow-Headers",
|
|
721
|
+
"Content-Type, Authorization"
|
|
722
|
+
);
|
|
723
|
+
response.setHeader("Access-Control-Max-Age", "600");
|
|
724
|
+
response.statusCode = 204;
|
|
725
|
+
response.end();
|
|
726
|
+
};
|
|
727
|
+
var handleNotImplemented = (routeKind) => {
|
|
728
|
+
return ({
|
|
729
|
+
request,
|
|
730
|
+
response,
|
|
731
|
+
route,
|
|
732
|
+
audit,
|
|
733
|
+
requestId
|
|
734
|
+
}) => {
|
|
735
|
+
void readJsonBodyForRoute(request, route).then((body) => {
|
|
736
|
+
audit.record({
|
|
737
|
+
action: `${routeKind}.received`,
|
|
738
|
+
subject: routeKind,
|
|
739
|
+
outcome: body.ok ? "ok" : "denied",
|
|
740
|
+
requestId
|
|
741
|
+
});
|
|
742
|
+
writeErrorResponse({
|
|
743
|
+
response,
|
|
744
|
+
code: "LLM_GATEWAY_UNCONFIGURED",
|
|
745
|
+
message: `Route '${routeKind}' is reachable but no operator-supplied handler is wired.`
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
var handleSseStub = ({ response }) => {
|
|
751
|
+
const sse = beginSseResponse(response);
|
|
752
|
+
sse.writeRetry(15e3);
|
|
753
|
+
sse.writeEvent({
|
|
754
|
+
event: "noop",
|
|
755
|
+
data: { message: "No event source is wired for this job." },
|
|
756
|
+
id: "0"
|
|
757
|
+
});
|
|
758
|
+
sse.end();
|
|
759
|
+
};
|
|
760
|
+
var readJsonBodyForRoute = async (request, route) => {
|
|
761
|
+
const maxBytes = route.kind === "submit_job" ? MAX_SUBMIT_BODY_BYTES : MAX_REQUEST_BODY_BYTES;
|
|
762
|
+
const result = await readJsonBody({ request, maxBytes });
|
|
763
|
+
return { ok: result.ok };
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// packages/server/src/request-handler.ts
|
|
767
|
+
var createTestIntelligenceRequestHandler = (options) => {
|
|
768
|
+
const rateLimiter = createRateLimiter({
|
|
769
|
+
requestsPerMinute: options.requestsPerMinute ?? DEFAULT_RATE_LIMIT_PER_MINUTE
|
|
770
|
+
});
|
|
771
|
+
const requestLog = createRequestLogger(options.logger);
|
|
772
|
+
const audit = createAuditLogger({
|
|
773
|
+
write: options.auditWrite ?? defaultAuditWrite(options.logger)
|
|
774
|
+
});
|
|
775
|
+
const handlers = buildHandlerTable(options);
|
|
776
|
+
const nowMs = options.nowMs ?? (() => Date.now());
|
|
777
|
+
return (request, response) => {
|
|
778
|
+
void runHandler({
|
|
779
|
+
request,
|
|
780
|
+
response,
|
|
781
|
+
options,
|
|
782
|
+
rateLimiter,
|
|
783
|
+
requestLog,
|
|
784
|
+
audit,
|
|
785
|
+
handlers,
|
|
786
|
+
nowMs
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
};
|
|
790
|
+
var runHandler = async (input) => {
|
|
791
|
+
const { request, response, options, requestLog } = input;
|
|
792
|
+
const start = Date.now();
|
|
793
|
+
const requestId = requestLog.newRequestId();
|
|
794
|
+
const clientKey = resolveClientKey(request);
|
|
795
|
+
const method = request.method ?? "GET";
|
|
796
|
+
const pathname = extractPathname(request.url ?? "/");
|
|
797
|
+
try {
|
|
798
|
+
const dispatched = await dispatch({
|
|
799
|
+
...input,
|
|
800
|
+
method,
|
|
801
|
+
pathname,
|
|
802
|
+
requestId,
|
|
803
|
+
clientKey
|
|
804
|
+
});
|
|
805
|
+
requestLog.log({
|
|
806
|
+
requestId,
|
|
807
|
+
method,
|
|
808
|
+
path: pathname,
|
|
809
|
+
statusCode: response.statusCode,
|
|
810
|
+
durationMs: Date.now() - start,
|
|
811
|
+
clientKey,
|
|
812
|
+
route: dispatched
|
|
813
|
+
});
|
|
814
|
+
} catch (error) {
|
|
815
|
+
handleUnexpectedError({ response, error, options, requestId });
|
|
816
|
+
requestLog.log({
|
|
817
|
+
requestId,
|
|
818
|
+
method,
|
|
819
|
+
path: pathname,
|
|
820
|
+
statusCode: response.statusCode,
|
|
821
|
+
durationMs: Date.now() - start,
|
|
822
|
+
clientKey
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var dispatch = async (input) => {
|
|
827
|
+
const {
|
|
828
|
+
request,
|
|
829
|
+
response,
|
|
830
|
+
method,
|
|
831
|
+
pathname,
|
|
832
|
+
options,
|
|
833
|
+
rateLimiter,
|
|
834
|
+
handlers,
|
|
835
|
+
requestId,
|
|
836
|
+
clientKey,
|
|
837
|
+
audit,
|
|
838
|
+
nowMs
|
|
839
|
+
} = input;
|
|
840
|
+
const parse = parseTestIntelligenceRoute({ method, pathname });
|
|
841
|
+
if (!parse.ok) {
|
|
842
|
+
emitParseFailure({
|
|
843
|
+
response,
|
|
844
|
+
reason: parse.reason,
|
|
845
|
+
allowed: parse.allowedMethods
|
|
846
|
+
});
|
|
847
|
+
return `parse:${parse.reason}`;
|
|
848
|
+
}
|
|
849
|
+
const route = parse.route;
|
|
850
|
+
const routeKey = `${method.toUpperCase()} ${route.kind}`;
|
|
851
|
+
const limit = rateLimiter.check({
|
|
852
|
+
clientKey,
|
|
853
|
+
routeKey,
|
|
854
|
+
nowMs: nowMs()
|
|
855
|
+
});
|
|
856
|
+
if (!limit.ok) {
|
|
857
|
+
writeErrorResponse({
|
|
858
|
+
response,
|
|
859
|
+
code: "RATE_LIMITED",
|
|
860
|
+
message: "Too many requests; retry after the indicated interval.",
|
|
861
|
+
extraHeaders: { "Retry-After": String(limit.retryAfterSeconds) }
|
|
862
|
+
});
|
|
863
|
+
return route.kind;
|
|
864
|
+
}
|
|
865
|
+
if (requiresWriteGate(route)) {
|
|
866
|
+
const gate = enforceWriteGate({ request, response, options, route });
|
|
867
|
+
if (!gate.ok) {
|
|
868
|
+
return route.kind;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
await handlers[route.kind]({
|
|
872
|
+
request,
|
|
873
|
+
response,
|
|
874
|
+
route,
|
|
875
|
+
requestId,
|
|
876
|
+
clientKey,
|
|
877
|
+
audit,
|
|
878
|
+
logger: options.logger
|
|
879
|
+
});
|
|
880
|
+
return route.kind;
|
|
881
|
+
};
|
|
882
|
+
var requiresWriteGate = (route) => {
|
|
883
|
+
switch (route.kind) {
|
|
884
|
+
case "healthz":
|
|
885
|
+
case "readyz":
|
|
886
|
+
case "openapi":
|
|
887
|
+
case "cors_preflight":
|
|
888
|
+
case "job_status":
|
|
889
|
+
case "job_events":
|
|
890
|
+
case "review_snapshot":
|
|
891
|
+
return false;
|
|
892
|
+
default:
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
var enforceWriteGate = ({
|
|
897
|
+
request,
|
|
898
|
+
response,
|
|
899
|
+
options,
|
|
900
|
+
route
|
|
901
|
+
}) => {
|
|
902
|
+
if (!options.testIntelligenceEnabled) {
|
|
903
|
+
writeErrorResponse({
|
|
904
|
+
response,
|
|
905
|
+
code: "FEATURE_GATE_DISABLED",
|
|
906
|
+
message: "The test-intelligence feature gate is disabled on this server."
|
|
907
|
+
});
|
|
908
|
+
return { ok: false };
|
|
909
|
+
}
|
|
910
|
+
const writeCheck = validateWriteRequest({
|
|
911
|
+
request,
|
|
912
|
+
host: options.host,
|
|
913
|
+
port: options.port
|
|
914
|
+
});
|
|
915
|
+
if (!writeCheck.ok) {
|
|
916
|
+
writeErrorResponse({
|
|
917
|
+
response,
|
|
918
|
+
code: writeCheck.payload.error,
|
|
919
|
+
message: writeCheck.payload.message,
|
|
920
|
+
statusCode: writeCheck.statusCode
|
|
921
|
+
});
|
|
922
|
+
return { ok: false };
|
|
923
|
+
}
|
|
924
|
+
const auth = validateBearerToken({
|
|
925
|
+
request,
|
|
926
|
+
bearerToken: options.bearerToken,
|
|
927
|
+
routeLabel: route.kind
|
|
928
|
+
});
|
|
929
|
+
if (!auth.ok) {
|
|
930
|
+
writeErrorResponse({
|
|
931
|
+
response,
|
|
932
|
+
code: auth.payload.error,
|
|
933
|
+
message: auth.payload.message,
|
|
934
|
+
statusCode: auth.statusCode,
|
|
935
|
+
...auth.wwwAuthenticate !== void 0 ? { extraHeaders: { "WWW-Authenticate": auth.wwwAuthenticate } } : {}
|
|
936
|
+
});
|
|
937
|
+
return { ok: false };
|
|
938
|
+
}
|
|
939
|
+
return { ok: true };
|
|
940
|
+
};
|
|
941
|
+
var emitParseFailure = ({
|
|
942
|
+
response,
|
|
943
|
+
reason,
|
|
944
|
+
allowed
|
|
945
|
+
}) => {
|
|
946
|
+
if (reason === "method_not_allowed") {
|
|
947
|
+
writeErrorResponse({
|
|
948
|
+
response,
|
|
949
|
+
code: "METHOD_NOT_ALLOWED",
|
|
950
|
+
message: "The requested method is not allowed on this route.",
|
|
951
|
+
...allowed !== void 0 ? { extraHeaders: { Allow: allowed.join(", ") } } : {}
|
|
952
|
+
});
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const code = reason === "unsafe_id_segment" || reason === "empty_segment" ? "BAD_REQUEST" : "NOT_FOUND";
|
|
956
|
+
const message = code === "BAD_REQUEST" ? "The route contains an unsafe or empty path segment." : "The requested route does not exist.";
|
|
957
|
+
writeErrorResponse({ response, code, message });
|
|
958
|
+
};
|
|
959
|
+
var extractPathname = (url) => {
|
|
960
|
+
const queryIndex = url.indexOf("?");
|
|
961
|
+
return queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
962
|
+
};
|
|
963
|
+
var defaultAuditWrite = (logger) => (line) => {
|
|
964
|
+
logger.log({ level: "info", message: `audit ${line.trimEnd()}` });
|
|
965
|
+
};
|
|
966
|
+
var handleUnexpectedError = ({
|
|
967
|
+
response,
|
|
968
|
+
error,
|
|
969
|
+
options,
|
|
970
|
+
requestId
|
|
971
|
+
}) => {
|
|
972
|
+
const message = error instanceof Error ? error.message : "Unknown error.";
|
|
973
|
+
options.logger.log({
|
|
974
|
+
level: "error",
|
|
975
|
+
message: `request handler failure: ${message}`,
|
|
976
|
+
requestId
|
|
977
|
+
});
|
|
978
|
+
if (!response.headersSent) {
|
|
979
|
+
writeErrorResponse({
|
|
980
|
+
response,
|
|
981
|
+
code: "INTERNAL_ERROR",
|
|
982
|
+
message: "An unexpected error occurred handling the request."
|
|
983
|
+
});
|
|
984
|
+
} else if (!response.writableEnded) {
|
|
985
|
+
response.end();
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// packages/server/src/openapi.ts
|
|
990
|
+
var ERROR_ENVELOPE_SCHEMA = {
|
|
991
|
+
type: "object",
|
|
992
|
+
required: ["error", "message"],
|
|
993
|
+
properties: {
|
|
994
|
+
error: { type: "string" },
|
|
995
|
+
message: { type: "string" }
|
|
996
|
+
},
|
|
997
|
+
additionalProperties: false
|
|
998
|
+
};
|
|
999
|
+
var READYZ_SCHEMA = {
|
|
1000
|
+
type: "object",
|
|
1001
|
+
required: ["status", "featureGate", "authConfigured", "checkedAt"],
|
|
1002
|
+
properties: {
|
|
1003
|
+
status: { type: "string", enum: ["ready"] },
|
|
1004
|
+
featureGate: { type: "string", enum: ["enabled", "disabled"] },
|
|
1005
|
+
authConfigured: { type: "boolean" },
|
|
1006
|
+
checkedAt: { type: "string", format: "date-time" }
|
|
1007
|
+
},
|
|
1008
|
+
additionalProperties: false
|
|
1009
|
+
};
|
|
1010
|
+
var HEALTHZ_SCHEMA = {
|
|
1011
|
+
type: "object",
|
|
1012
|
+
required: ["status", "checkedAt"],
|
|
1013
|
+
properties: {
|
|
1014
|
+
status: { type: "string", enum: ["ok"] },
|
|
1015
|
+
checkedAt: { type: "string", format: "date-time" }
|
|
1016
|
+
},
|
|
1017
|
+
additionalProperties: false
|
|
1018
|
+
};
|
|
1019
|
+
var errorResponse = (description) => ({
|
|
1020
|
+
description,
|
|
1021
|
+
content: {
|
|
1022
|
+
"application/json": {
|
|
1023
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
var writeRoute = (summary, operationId) => ({
|
|
1028
|
+
post: {
|
|
1029
|
+
summary,
|
|
1030
|
+
operationId,
|
|
1031
|
+
security: [{ bearerAuth: [] }],
|
|
1032
|
+
requestBody: {
|
|
1033
|
+
required: true,
|
|
1034
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
1035
|
+
},
|
|
1036
|
+
responses: {
|
|
1037
|
+
"200": { description: "Operation completed." },
|
|
1038
|
+
"401": errorResponse("Bearer token is missing or incorrect."),
|
|
1039
|
+
"403": errorResponse("Feature gate or origin policy denied the call."),
|
|
1040
|
+
"413": errorResponse("Request body exceeded the configured limit."),
|
|
1041
|
+
"415": errorResponse("Content-Type is not application/json."),
|
|
1042
|
+
"429": errorResponse("Per-client rate limit was exceeded."),
|
|
1043
|
+
"503": errorResponse("Server is not configured for this route.")
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
var readRoute = (summary, operationId, responseSchemaRef) => ({
|
|
1048
|
+
get: {
|
|
1049
|
+
summary,
|
|
1050
|
+
operationId,
|
|
1051
|
+
responses: {
|
|
1052
|
+
"200": {
|
|
1053
|
+
description: "Result.",
|
|
1054
|
+
content: {
|
|
1055
|
+
"application/json": { schema: { $ref: responseSchemaRef } }
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
"404": errorResponse("Resource not found."),
|
|
1059
|
+
"429": errorResponse("Per-client rate limit was exceeded.")
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
var jobScopedWriteRoute = (summary, operationId) => {
|
|
1064
|
+
const route = writeRoute(summary, operationId);
|
|
1065
|
+
route.post["parameters"] = [
|
|
1066
|
+
{
|
|
1067
|
+
name: "jobId",
|
|
1068
|
+
in: "path",
|
|
1069
|
+
required: true,
|
|
1070
|
+
schema: { type: "string", pattern: "^[A-Za-z0-9_.-]{1,128}$" }
|
|
1071
|
+
}
|
|
1072
|
+
];
|
|
1073
|
+
return route;
|
|
1074
|
+
};
|
|
1075
|
+
var buildPaths = () => ({
|
|
1076
|
+
"/healthz": readRoute(
|
|
1077
|
+
"Liveness probe.",
|
|
1078
|
+
"getHealthz",
|
|
1079
|
+
"#/components/schemas/Healthz"
|
|
1080
|
+
),
|
|
1081
|
+
"/readyz": readRoute(
|
|
1082
|
+
"Readiness probe.",
|
|
1083
|
+
"getReadyz",
|
|
1084
|
+
"#/components/schemas/Readyz"
|
|
1085
|
+
),
|
|
1086
|
+
"/openapi.json": readRoute(
|
|
1087
|
+
"Return this OpenAPI document.",
|
|
1088
|
+
"getOpenapi",
|
|
1089
|
+
"#/components/schemas/Error"
|
|
1090
|
+
),
|
|
1091
|
+
[`${API_ROUTE_PREFIX}/jobs`]: writeRoute(
|
|
1092
|
+
"Submit a Test Intelligence run.",
|
|
1093
|
+
"submitJob"
|
|
1094
|
+
),
|
|
1095
|
+
[`${API_ROUTE_PREFIX}/jobs/{jobId}`]: readRoute(
|
|
1096
|
+
"Read a job's status.",
|
|
1097
|
+
"getJobStatus",
|
|
1098
|
+
"#/components/schemas/Error"
|
|
1099
|
+
),
|
|
1100
|
+
[`${API_ROUTE_PREFIX}/jobs/{jobId}/events`]: readRoute(
|
|
1101
|
+
"Server-Sent Events stream of a job's phase events.",
|
|
1102
|
+
"streamJobEvents",
|
|
1103
|
+
"#/components/schemas/Error"
|
|
1104
|
+
),
|
|
1105
|
+
[`${API_ROUTE_PREFIX}/jobs/{jobId}/evidence/verify`]: jobScopedWriteRoute(
|
|
1106
|
+
"Verify a job's evidence manifest.",
|
|
1107
|
+
"verifyJobEvidence"
|
|
1108
|
+
),
|
|
1109
|
+
[`${API_ROUTE_PREFIX}/jobs/{jobId}/audit-dossier/verify`]: jobScopedWriteRoute(
|
|
1110
|
+
"Verify a job's audit dossier bundle.",
|
|
1111
|
+
"verifyJobAuditDossier"
|
|
1112
|
+
),
|
|
1113
|
+
[`${API_ROUTE_PREFIX}/jobs/{jobId}/provenance/verify`]: jobScopedWriteRoute(
|
|
1114
|
+
"Verify a job's provenance document.",
|
|
1115
|
+
"verifyJobProvenance"
|
|
1116
|
+
),
|
|
1117
|
+
[`${API_ROUTE_PREFIX}/jobs/{jobId}/seal/verify`]: jobScopedWriteRoute(
|
|
1118
|
+
"Verify a job's seal bundle.",
|
|
1119
|
+
"verifyJobSeal"
|
|
1120
|
+
),
|
|
1121
|
+
[`${API_ROUTE_PREFIX}/review/{jobId}`]: readRoute(
|
|
1122
|
+
"Read the review snapshot for a job.",
|
|
1123
|
+
"getReviewSnapshot",
|
|
1124
|
+
"#/components/schemas/Error"
|
|
1125
|
+
),
|
|
1126
|
+
[`${API_ROUTE_PREFIX}/review/{jobId}/decision`]: jobScopedWriteRoute(
|
|
1127
|
+
"Record a review decision.",
|
|
1128
|
+
"recordReviewDecision"
|
|
1129
|
+
),
|
|
1130
|
+
[`${API_ROUTE_PREFIX}/tms/push`]: writeRoute(
|
|
1131
|
+
"Push completed test cases to a configured TMS adapter.",
|
|
1132
|
+
"pushToTms"
|
|
1133
|
+
),
|
|
1134
|
+
[`${API_ROUTE_PREFIX}/execution/pull`]: writeRoute(
|
|
1135
|
+
"Pull execution evidence from a TMS adapter.",
|
|
1136
|
+
"pullExecutionEvidence"
|
|
1137
|
+
),
|
|
1138
|
+
[`${API_ROUTE_PREFIX}/onboard`]: writeRoute(
|
|
1139
|
+
"Run tenant onboarding.",
|
|
1140
|
+
"runTenantOnboarding"
|
|
1141
|
+
),
|
|
1142
|
+
[`${API_ROUTE_PREFIX}/figma-export`]: writeRoute(
|
|
1143
|
+
"Fetch a Figma file as a TI ingest payload.",
|
|
1144
|
+
"exportFigmaForTestIntelligence"
|
|
1145
|
+
)
|
|
1146
|
+
});
|
|
1147
|
+
var buildOpenApiDocument = () => ({
|
|
1148
|
+
openapi: "3.1.0",
|
|
1149
|
+
info: {
|
|
1150
|
+
title: "Test Intelligence API",
|
|
1151
|
+
version: PACKAGE_VERSION,
|
|
1152
|
+
description: "Standalone HTTP surface for the Test Intelligence runtime. Every write route is bearer-protected and fails closed when the test-intelligence feature gate is disabled."
|
|
1153
|
+
},
|
|
1154
|
+
servers: [{ url: "http://127.0.0.1:1983" }],
|
|
1155
|
+
components: {
|
|
1156
|
+
securitySchemes: {
|
|
1157
|
+
bearerAuth: {
|
|
1158
|
+
type: "http",
|
|
1159
|
+
scheme: "bearer"
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
schemas: {
|
|
1163
|
+
Error: ERROR_ENVELOPE_SCHEMA,
|
|
1164
|
+
Readyz: READYZ_SCHEMA,
|
|
1165
|
+
Healthz: HEALTHZ_SCHEMA
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
paths: buildPaths()
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// packages/server/src/server.ts
|
|
1172
|
+
var createTestIntelligenceServer = async (options) => {
|
|
1173
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
1174
|
+
const requestedPort = options.port ?? DEFAULT_PORT;
|
|
1175
|
+
const testIntelligenceEnabled = options.testIntelligenceEnabled ?? resolveTestIntelligenceEnabled(options.env);
|
|
1176
|
+
const openApiHandler = ({ response }) => {
|
|
1177
|
+
writeJsonResponse({
|
|
1178
|
+
response,
|
|
1179
|
+
statusCode: 200,
|
|
1180
|
+
payload: buildOpenApiDocument()
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
const mergedRouteHandlers = {
|
|
1184
|
+
openapi: openApiHandler,
|
|
1185
|
+
...options.routeHandlers ?? {}
|
|
1186
|
+
};
|
|
1187
|
+
const handler = createTestIntelligenceRequestHandler({
|
|
1188
|
+
host,
|
|
1189
|
+
port: requestedPort,
|
|
1190
|
+
logger: options.logger,
|
|
1191
|
+
testIntelligenceEnabled,
|
|
1192
|
+
requestsPerMinute: options.requestsPerMinute ?? DEFAULT_RATE_LIMIT_PER_MINUTE,
|
|
1193
|
+
routeHandlers: mergedRouteHandlers,
|
|
1194
|
+
...options.bearerToken !== void 0 ? { bearerToken: options.bearerToken } : {},
|
|
1195
|
+
...options.auditWrite !== void 0 ? { auditWrite: options.auditWrite } : {},
|
|
1196
|
+
...options.allowedCorsOrigins !== void 0 ? { allowedCorsOrigins: options.allowedCorsOrigins } : {}
|
|
1197
|
+
});
|
|
1198
|
+
const server = createServer((request, response) => {
|
|
1199
|
+
handler(request, response);
|
|
1200
|
+
});
|
|
1201
|
+
await listen(server, host, requestedPort);
|
|
1202
|
+
const address = server.address();
|
|
1203
|
+
const boundPort = address !== null && typeof address === "object" ? address.port : requestedPort;
|
|
1204
|
+
const url = `http://${formatHost(host)}:${String(boundPort)}`;
|
|
1205
|
+
const startedAt = Date.now();
|
|
1206
|
+
return {
|
|
1207
|
+
server,
|
|
1208
|
+
host,
|
|
1209
|
+
port: boundPort,
|
|
1210
|
+
url,
|
|
1211
|
+
startedAt,
|
|
1212
|
+
close: () => closeServer(server)
|
|
1213
|
+
};
|
|
1214
|
+
};
|
|
1215
|
+
var listen = (server, host, port) => {
|
|
1216
|
+
return new Promise((resolve, reject) => {
|
|
1217
|
+
const onError = (error) => {
|
|
1218
|
+
server.off("listening", onListening);
|
|
1219
|
+
reject(error);
|
|
1220
|
+
};
|
|
1221
|
+
const onListening = () => {
|
|
1222
|
+
server.off("error", onError);
|
|
1223
|
+
resolve();
|
|
1224
|
+
};
|
|
1225
|
+
server.once("error", onError);
|
|
1226
|
+
server.once("listening", onListening);
|
|
1227
|
+
server.listen(port, host);
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
var closeServer = (server) => {
|
|
1231
|
+
return new Promise((resolve, reject) => {
|
|
1232
|
+
server.close((error) => {
|
|
1233
|
+
if (error !== void 0) {
|
|
1234
|
+
reject(error);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
resolve();
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
var formatHost = (host) => {
|
|
1242
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
1243
|
+
return `[${host}]`;
|
|
1244
|
+
}
|
|
1245
|
+
return host;
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
export { PACKAGE_NAME, PACKAGE_VERSION, createTestIntelligenceServer, getPackageIdentity, resolveReleaseStage };
|
|
1249
|
+
//# sourceMappingURL=index.js.map
|
|
1250
|
+
//# sourceMappingURL=index.js.map
|