@jimhoyd/urlcode 0.3.0
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/CONTRIBUTING.md +80 -0
- package/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +342 -0
- package/ROADMAP.md +248 -0
- package/SECURITY.md +40 -0
- package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
- package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
- package/data/agents/ai-crawlers.json +1240 -0
- package/data/agents/crawlers.json +10515 -0
- package/data/agents/index.d.ts +3 -0
- package/data/agents/index.js +2453 -0
- package/data/agents/monitoring.json +1520 -0
- package/data/agents/seo.json +3865 -0
- package/dist/BUILD-MANIFEST.json +60 -0
- package/dist/adapters.js +59 -0
- package/dist/agent-lists.js +59 -0
- package/dist/assets.js +129 -0
- package/dist/authoring.js +68 -0
- package/dist/aws.js +110 -0
- package/dist/build-cloudflare.js +164 -0
- package/dist/cli.js +185 -0
- package/dist/client-address.js +85 -0
- package/dist/cloudflare.js +165 -0
- package/dist/compliance-rules/baseline.js +98 -0
- package/dist/compliance-rules/privacy.js +38 -0
- package/dist/compliance-rules/shared.js +62 -0
- package/dist/compliance-rules/strict.js +61 -0
- package/dist/compliance.js +219 -0
- package/dist/config-worker.js +9 -0
- package/dist/config.js +172 -0
- package/dist/errors.js +8 -0
- package/dist/function-sources.js +54 -0
- package/dist/function-worker.js +105 -0
- package/dist/functions.js +174 -0
- package/dist/guest-api.js +114 -0
- package/dist/header-validation.js +18 -0
- package/dist/http-policy.js +74 -0
- package/dist/http-response.js +84 -0
- package/dist/index.js +11 -0
- package/dist/link-api.js +136 -0
- package/dist/link-cli.js +141 -0
- package/dist/link-events.js +76 -0
- package/dist/link-records.js +31 -0
- package/dist/link-store-worker.js +150 -0
- package/dist/link-store.js +250 -0
- package/dist/logging.js +22 -0
- package/dist/management-policy.js +41 -0
- package/dist/match.js +124 -0
- package/dist/observability.js +242 -0
- package/dist/plugins.js +74 -0
- package/dist/policies/agents.js +248 -0
- package/dist/policies/cache.js +297 -0
- package/dist/policies/compression.js +187 -0
- package/dist/policies/security.js +0 -0
- package/dist/policies/throttle.js +131 -0
- package/dist/policies.js +142 -0
- package/dist/policy.js +55 -0
- package/dist/prerender.js +165 -0
- package/dist/project-tests.js +23 -0
- package/dist/readiness.js +216 -0
- package/dist/router.js +166 -0
- package/dist/runtime.js +244 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +75 -0
- package/dist/server.js +270 -0
- package/dist/site.js +0 -0
- package/dist/sqlite-version.js +6 -0
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/agent-lists.d.ts +5 -0
- package/dist/types/assets.d.ts +13 -0
- package/dist/types/authoring.d.ts +2 -0
- package/dist/types/aws.d.ts +35 -0
- package/dist/types/build-cloudflare.d.ts +14 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client-address.d.ts +9 -0
- package/dist/types/cloudflare.d.ts +75 -0
- package/dist/types/compliance-rules/baseline.d.ts +12 -0
- package/dist/types/compliance-rules/privacy.d.ts +6 -0
- package/dist/types/compliance-rules/shared.d.ts +24 -0
- package/dist/types/compliance-rules/strict.d.ts +9 -0
- package/dist/types/compliance.d.ts +119 -0
- package/dist/types/config-worker.d.ts +1 -0
- package/dist/types/config.d.ts +21 -0
- package/dist/types/errors.d.ts +7 -0
- package/dist/types/function-sources.d.ts +21 -0
- package/dist/types/function-worker.d.ts +1 -0
- package/dist/types/functions.d.ts +92 -0
- package/dist/types/guest-api.d.ts +16 -0
- package/dist/types/header-validation.d.ts +2 -0
- package/dist/types/http-policy.d.ts +39 -0
- package/dist/types/http-response.d.ts +43 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/link-api.d.ts +30 -0
- package/dist/types/link-cli.d.ts +36 -0
- package/dist/types/link-events.d.ts +27 -0
- package/dist/types/link-records.d.ts +11 -0
- package/dist/types/link-store-worker.d.ts +1 -0
- package/dist/types/link-store.d.ts +130 -0
- package/dist/types/logging.d.ts +9 -0
- package/dist/types/management-policy.d.ts +9 -0
- package/dist/types/match.d.ts +81 -0
- package/dist/types/observability.d.ts +104 -0
- package/dist/types/plugins.d.ts +27 -0
- package/dist/types/policies/agents.d.ts +86 -0
- package/dist/types/policies/cache.d.ts +82 -0
- package/dist/types/policies/compression.d.ts +41 -0
- package/dist/types/policies/security.d.ts +37 -0
- package/dist/types/policies/throttle.d.ts +55 -0
- package/dist/types/policies.d.ts +36 -0
- package/dist/types/policy.d.ts +21 -0
- package/dist/types/prerender.d.ts +31 -0
- package/dist/types/project-tests.d.ts +13 -0
- package/dist/types/readiness.d.ts +121 -0
- package/dist/types/router.d.ts +11 -0
- package/dist/types/runtime.d.ts +91 -0
- package/dist/types/scaffold.d.ts +17 -0
- package/dist/types/server.d.ts +37 -0
- package/dist/types/site.d.ts +12 -0
- package/dist/types/sqlite-version.d.ts +1 -0
- package/dist/types/types.d.ts +332 -0
- package/dist/types/vercel.d.ts +12 -0
- package/dist/types.js +177 -0
- package/dist/vercel.js +65 -0
- package/docs/AI-AUTHORING.md +113 -0
- package/docs/ASSETS.md +106 -0
- package/docs/AWS.md +77 -0
- package/docs/BEST-PRACTICES.md +267 -0
- package/docs/CAPACITY.md +208 -0
- package/docs/CLOUDFLARE.md +110 -0
- package/docs/COMPLIANCE.md +242 -0
- package/docs/DYNAMIC-LINKS.md +561 -0
- package/docs/FUNCTION-SECURITY.md +113 -0
- package/docs/HTTP.md +129 -0
- package/docs/INSTALL.md +90 -0
- package/docs/LOAD-TESTING.md +91 -0
- package/docs/LOCAL-DEVELOPMENT.md +99 -0
- package/docs/MANAGEMENT-SECURITY.md +82 -0
- package/docs/MIDDLEWARE.md +83 -0
- package/docs/MONITORING.md +132 -0
- package/docs/OBSERVABILITY.md +229 -0
- package/docs/OPERATIONAL-PROOF.md +42 -0
- package/docs/OPERATIONS.md +207 -0
- package/docs/ORGANIZATION.md +135 -0
- package/docs/PERFORMANCE.md +72 -0
- package/docs/PLUGINS.md +234 -0
- package/docs/POLICIES.md +489 -0
- package/docs/PRERENDER.md +193 -0
- package/docs/PROJECT-DIRECTION.md +51 -0
- package/docs/READINESS.md +125 -0
- package/docs/README.md +58 -0
- package/docs/RELEASE-READINESS.md +95 -0
- package/docs/RELEASE-SECURITY.md +72 -0
- package/docs/RESILIENCE.md +160 -0
- package/docs/ROUTING.md +95 -0
- package/docs/SANDBOX-REVIEW.md +59 -0
- package/docs/SCAFFOLDING.md +72 -0
- package/docs/SECURITY-AUDIT.md +124 -0
- package/docs/SITE.md +150 -0
- package/docs/SPECIFICATION.md +232 -0
- package/docs/SPIKE-EXTENSIONS.md +475 -0
- package/docs/STANDARDS.md +303 -0
- package/docs/STARTERS.md +51 -0
- package/docs/TUNNELS.md +75 -0
- package/docs/TYPESCRIPT.md +102 -0
- package/docs/VERCEL.md +107 -0
- package/docs/YAML-GUIDE.md +521 -0
- package/docs/YAML-REFERENCE.md +328 -0
- package/docs/policies/agents.md +182 -0
- package/docs/policies/cache.md +139 -0
- package/docs/policies/compression.md +168 -0
- package/docs/policies/security.md +160 -0
- package/docs/policies/throttle.md +103 -0
- package/examples/assets/.env.example +1 -0
- package/examples/assets/.gitattributes +2 -0
- package/examples/assets/Makefile +30 -0
- package/examples/assets/README.md +18 -0
- package/examples/assets/functions/hello.mjs +3 -0
- package/examples/assets/gitignore.template +7 -0
- package/examples/assets/public/about.html +2 -0
- package/examples/assets/public/assets/example.txt +1 -0
- package/examples/assets/public/guide.txt +1 -0
- package/examples/assets/tests/requests.json +44 -0
- package/examples/assets/urlcode.yaml +27 -0
- package/examples/aws/README.md +13 -0
- package/examples/aws/handler.mjs +5 -0
- package/examples/aws/public/index.html +3 -0
- package/examples/aws/public/notes.txt +1 -0
- package/examples/aws/template.yaml +33 -0
- package/examples/aws/tests/requests.json +9 -0
- package/examples/aws/urlcode.yaml +19 -0
- package/examples/cloudflare/README.md +18 -0
- package/examples/cloudflare/package.json +14 -0
- package/examples/cloudflare/tests/requests.json +9 -0
- package/examples/cloudflare/urlcode.yaml +24 -0
- package/examples/cloudflare/wrangler.toml +5 -0
- package/examples/compliance/README.md +20 -0
- package/examples/compliance/rules.mjs +65 -0
- package/examples/cookbook/README.md +21 -0
- package/examples/cookbook/functions/choice.mjs +4 -0
- package/examples/cookbook/functions/echo.mjs +3 -0
- package/examples/cookbook/functions/hello.mjs +3 -0
- package/examples/cookbook/functions/text.mjs +3 -0
- package/examples/cookbook/middleware/headers.mjs +6 -0
- package/examples/cookbook/public/about.html +1 -0
- package/examples/cookbook/public/assets/index.html +1 -0
- package/examples/cookbook/public/assets/site.css +1 -0
- package/examples/cookbook/public/favicon.svg +1 -0
- package/examples/cookbook/public/guide.txt +1 -0
- package/examples/cookbook/public/llms.txt +6 -0
- package/examples/cookbook/routes/code.yaml +55 -0
- package/examples/cookbook/routes/files.yaml +17 -0
- package/examples/cookbook/routes/policies.yaml +28 -0
- package/examples/cookbook/routes/redirects.yaml +38 -0
- package/examples/cookbook/routes/responses.yaml +26 -0
- package/examples/cookbook/tests/requests.json +254 -0
- package/examples/cookbook/urlcode.yaml +22 -0
- package/examples/live-links/README.md +11 -0
- package/examples/live-links/tests/requests.json +6 -0
- package/examples/live-links/urlcode.yaml +16 -0
- package/examples/monitoring/blackbox-jobs.yaml +37 -0
- package/examples/monitoring/prometheus-rules.yaml +88 -0
- package/examples/monitoring/prometheus-scrape.yaml +20 -0
- package/examples/monitoring/vector.toml +65 -0
- package/examples/prerender/README.md +48 -0
- package/examples/prerender/functions/page.mjs +6 -0
- package/examples/prerender/middleware/template.mjs +29 -0
- package/examples/prerender/prerender.d.mts +4 -0
- package/examples/prerender/prerender.mjs +63 -0
- package/examples/prerender/tests/requests.json +39 -0
- package/examples/prerender/urlcode.yaml +50 -0
- package/examples/tunnel/dev-with-ngrok.sh +61 -0
- package/examples/vercel/README.md +13 -0
- package/examples/vercel/api/index.js +5 -0
- package/examples/vercel/package.json +8 -0
- package/examples/vercel/public/index.html +3 -0
- package/examples/vercel/public/notes.txt +1 -0
- package/examples/vercel/tests/requests.json +48 -0
- package/examples/vercel/urlcode.yaml +19 -0
- package/examples/vercel/vercel.json +10 -0
- package/llms.txt +46 -0
- package/package.json +123 -0
- package/schemas/urlcode.schema.json +1230 -0
- package/starters/default/.gitattributes +1 -0
- package/starters/default/Makefile +30 -0
- package/starters/default/README.md +34 -0
- package/starters/default/functions/hello.mjs +3 -0
- package/starters/default/gitignore.template +8 -0
- package/starters/default/middleware/headers.mjs +6 -0
- package/starters/default/routes/functions.yaml +20 -0
- package/starters/default/routes/marketing/links.yaml +7 -0
- package/starters/default/starter.json +5 -0
- package/starters/default/tests/requests.json +56 -0
- package/starters/default/urlcode.yaml +8 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
5
|
+
import { readdir, lstat } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { createRuntime } from './runtime.js';
|
|
8
|
+
|
|
9
|
+
import { createJsonLogger } from './logging.js';
|
|
10
|
+
import { createLinkObserver } from './link-events.js';
|
|
11
|
+
|
|
12
|
+
import { createObserverSink, renderPrometheus } from './observability.js';
|
|
13
|
+
|
|
14
|
+
import { assert, HttpError } from './errors.js';
|
|
15
|
+
import { writeResponse, writeError } from './http-response.js';
|
|
16
|
+
|
|
17
|
+
import { compileTrustedProxies, resolveClient } from './client-address.js';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const excluded = new Set(['node_modules', '.git', 'coverage', 'dist', '.urlcode']);
|
|
32
|
+
async function fingerprint(root , local , assets = []) {
|
|
33
|
+
const hash = createHash('sha256'); let count = 0;
|
|
34
|
+
async function walk(dir , depth = 0) {
|
|
35
|
+
if (depth > 20) throw new Error('Project watch depth exceeded');
|
|
36
|
+
for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a,b) => a.name.localeCompare(b.name))) {
|
|
37
|
+
if (excluded.has(entry.name) || (entry.name.startsWith('.') && !(local && entry.name === '.env.local'))) continue;
|
|
38
|
+
const file = join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) await walk(file, depth + 1);
|
|
40
|
+
else if (entry.isFile() && (/\.(?:yaml|yml|mjs|js|json)$/.test(entry.name) || entry.name === '.env.local')) {
|
|
41
|
+
if (++count > 10000) throw new Error('Project watch file limit exceeded');
|
|
42
|
+
const info=await lstat(file);
|
|
43
|
+
hash.update(file+':'+info.size+':'+info.mtimeMs+':'+info.ctimeMs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await walk(root);
|
|
48
|
+
async function assetWalk(file , depth = 0) {
|
|
49
|
+
if (++count > 20000 || depth > 20) throw new Error('Asset watch limit exceeded');
|
|
50
|
+
let stat; try { stat = await lstat(file); } catch { hash.update(file + ':missing'); return; }
|
|
51
|
+
hash.update(file + ':' + stat.size + ':' + stat.mtimeMs + ':' + stat.ctimeMs);
|
|
52
|
+
if (stat.isDirectory()) for (const name of (await readdir(file)).sort()) await assetWalk(join(file,name),depth+1);
|
|
53
|
+
}
|
|
54
|
+
for (const file of assets) await assetWalk(file);
|
|
55
|
+
return hash.digest('hex');
|
|
56
|
+
}
|
|
57
|
+
async function readBody(req , limit ) {
|
|
58
|
+
if (req.headers['content-length'] && Number(req.headers['content-length']) > limit) throw new HttpError(413, 'Request body too large');
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
let size = 0; const chunks = [];
|
|
61
|
+
const cleanup = () => { req.off('data', data); req.off('end', end); req.off('error', error); req.off('aborted', aborted); };
|
|
62
|
+
const error = (cause ) => { cleanup(); reject(cause); };
|
|
63
|
+
const aborted = () => error(new HttpError(400, 'Request aborted'));
|
|
64
|
+
const data = (chunk ) => {
|
|
65
|
+
size += chunk.length;
|
|
66
|
+
if (size > limit) { req.pause(); error(new HttpError(413, 'Request body too large')); }
|
|
67
|
+
else chunks.push(chunk);
|
|
68
|
+
};
|
|
69
|
+
const end = () => { cleanup(); resolve(Buffer.concat(chunks)); };
|
|
70
|
+
req.on('data',data); req.once('end',end); req.once('error',error); req.once('aborted',aborted);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const safeRequestId = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
74
|
+
// A server must accept absolute-form targets (RFC 9112 §3.2.2). The scheme
|
|
75
|
+
// and authority are removed textually, never re-encoded, so the path keeps
|
|
76
|
+
// the exact bytes the runtime's encoding checks inspect.
|
|
77
|
+
function originForm(target ) {
|
|
78
|
+
const match = /^https?:\/\/[^/?#]*(.*)$/i.exec(target);
|
|
79
|
+
const rest = match?.[1];
|
|
80
|
+
if (rest === undefined) return target;
|
|
81
|
+
return rest === '' || rest.startsWith('?') ? '/' + rest : rest;
|
|
82
|
+
}
|
|
83
|
+
export async function startServer({ project = '.', host = '127.0.0.1', port = 3000, watch = false,
|
|
84
|
+
local = false, log = createJsonLogger(),
|
|
85
|
+
maxBodyBytes = 1048576, maxInFlightRequests = 64, maxInFlightHealthRequests = 16,
|
|
86
|
+
requestLog = 'minimal', trustRequestId = false, origin, linkEvents, trustedProxies = [],
|
|
87
|
+
observers = [], metrics = false, metricsIntervalMs = 0, ...runtimeOptions } = {}) {
|
|
88
|
+
// Which peers may set X-Forwarded-For. Empty means the socket peer is the
|
|
89
|
+
// client for every policy; a forwarded header from anyone else is ignored.
|
|
90
|
+
const proxies = compileTrustedProxies(trustedProxies);
|
|
91
|
+
assert(Number.isInteger(maxBodyBytes) && maxBodyBytes >= 1 && maxBodyBytes <= 16777216, 'Request limit must be 1–16777216 bytes');
|
|
92
|
+
assert(Number.isInteger(maxInFlightRequests) && maxInFlightRequests >= 1 && maxInFlightRequests <= 1024, 'In-flight request limit must be 1–1024');
|
|
93
|
+
assert(Number.isInteger(maxInFlightHealthRequests) && maxInFlightHealthRequests >= 1 && maxInFlightHealthRequests <= 1024, 'Health admission limit must be 1–1024');
|
|
94
|
+
assert(['minimal','detailed'].includes(requestLog), 'Request log detail must be minimal or detailed');
|
|
95
|
+
assert(typeof trustRequestId === 'boolean', 'Request ID trust must be a boolean');
|
|
96
|
+
assert(typeof metrics === 'boolean', 'Metrics exposition must be a boolean');
|
|
97
|
+
assert(metricsIntervalMs === 0 || (Number.isInteger(metricsIntervalMs) && metricsIntervalMs >= 1000 && metricsIntervalMs <= 3600000), 'Metrics interval must be 0 or 1000–3600000 ms');
|
|
98
|
+
if (origin) {
|
|
99
|
+
let u ;
|
|
100
|
+
try { u = new URL(origin); } catch { assert(false, 'Invalid public origin'); }
|
|
101
|
+
assert(['http:', 'https:'].includes(u.protocol) && u.origin === origin, 'Origin must be HTTP(S) without path or credentials');
|
|
102
|
+
}
|
|
103
|
+
// The JSON logger stays the default sink; observers see the same records
|
|
104
|
+
// after it, in array order. Counters are derived from what passes through.
|
|
105
|
+
const sink = createObserverSink(observers, log);
|
|
106
|
+
const counters = sink.metrics;
|
|
107
|
+
const emit = (event , context ) => { try { sink(event, context); } catch { /* Logging cannot fail requests. */ } };
|
|
108
|
+
// Operator-supplied and explicitly enabled; route YAML cannot reach it and no
|
|
109
|
+
// callback is ever loaded from the project. Undefined leaves it off.
|
|
110
|
+
const observer = createLinkObserver(linkEvents, emit);
|
|
111
|
+
let current = await createRuntime(project, { local, log: emit, origin, ...runtimeOptions });
|
|
112
|
+
let shuttingDown = false, reloading = false, watching = false, interval , lastFingerprint , inFlight = 0, healthInFlight = 0;
|
|
113
|
+
const retired = new Set ();
|
|
114
|
+
const server = http.createServer({ maxHeaderSize: 16384, headersTimeout: 10000, requestTimeout: 15000, keepAliveTimeout: 5000 }, async (req, res) => {
|
|
115
|
+
const started = performance.now();
|
|
116
|
+
const url = req.url ?? '', method = req.method ?? 'GET';
|
|
117
|
+
// Upstream correlation is opt-in: an untrusted client must not choose the ID
|
|
118
|
+
// that ties together this deployment's operational records.
|
|
119
|
+
let inbound ;
|
|
120
|
+
if (trustRequestId) {
|
|
121
|
+
const values = [];
|
|
122
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) if (req.rawHeaders[i]?.toLowerCase() === 'x-request-id') values.push(req.rawHeaders[i + 1] ?? '');
|
|
123
|
+
const only = values[0];
|
|
124
|
+
if (values.length === 1 && only !== undefined && safeRequestId.test(only)) inbound = only;
|
|
125
|
+
}
|
|
126
|
+
const requestId = inbound || randomUUID();
|
|
127
|
+
const trace = {};
|
|
128
|
+
let status = 500;
|
|
129
|
+
res.on('error', () => {});
|
|
130
|
+
req.on('error', () => {});
|
|
131
|
+
{
|
|
132
|
+
// Enqueued after the response is over, so an observer can neither delay a
|
|
133
|
+
// redirect nor turn its own failure into one. A response that never
|
|
134
|
+
// finished is reported as aborted rather than counted as a click. The
|
|
135
|
+
// outcome is counted whether or not an operator collects link events.
|
|
136
|
+
let observed = false;
|
|
137
|
+
const settle = () => {
|
|
138
|
+
if (observed || !trace.link) return;
|
|
139
|
+
observed = true;
|
|
140
|
+
const event = { event: 'link_request', requestId, collection: trace.link.collection, route: trace.route ?? null,
|
|
141
|
+
code: trace.link.code, method, status,
|
|
142
|
+
outcome: trace.link.result === 'redirect' ? (res.writableFinished ? 'completed' : 'aborted') : trace.link.result,
|
|
143
|
+
durationMs: Math.round((performance.now() - started) * 100) / 100 };
|
|
144
|
+
counters.record(event);
|
|
145
|
+
if (observer) observer.emit(event);
|
|
146
|
+
};
|
|
147
|
+
res.once('finish', settle); res.once('close', settle);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
if (shuttingDown) throw new HttpError(503, 'Runtime shutting down');
|
|
151
|
+
let result ;
|
|
152
|
+
if (url === '/_urlcode/health' || url === '/_urlcode/ready' || (metrics && url === '/_urlcode/metrics')) {
|
|
153
|
+
// Probes keep their own budget so they stay answerable while the
|
|
154
|
+
// application is saturated, without being an unmetered amplifier.
|
|
155
|
+
// Metrics exposition, when enabled, shares that budget and bind host.
|
|
156
|
+
trace.route = url; trace.probe = true;
|
|
157
|
+
if (healthInFlight >= maxInFlightHealthRequests) { counters.shed('health'); throw new HttpError(503, 'Health probe capacity unavailable'); }
|
|
158
|
+
healthInFlight++; counters.inFlight('health', 1);
|
|
159
|
+
let releasedProbe = false;
|
|
160
|
+
const releaseProbe = () => { if (!releasedProbe) { releasedProbe = true; healthInFlight--; counters.inFlight('health', -1); } };
|
|
161
|
+
res.once('finish', releaseProbe); res.once('close', releaseProbe);
|
|
162
|
+
if (!['GET','HEAD'].includes(method)) result = { status: 405, headers: [['allow','GET, HEAD']], body: Buffer.alloc(0) };
|
|
163
|
+
else if (url === '/_urlcode/metrics') result = { status: 200, headers: [['content-type','text/plain; version=0.0.4; charset=utf-8'],['cache-control','no-store']], body: Buffer.from(renderPrometheus(snapshot())) };
|
|
164
|
+
else {
|
|
165
|
+
const ready = url === '/_urlcode/health' || current.healthy;
|
|
166
|
+
result = { status: ready ? 200 : 503, headers: [['content-type','application/json']], body: Buffer.from(JSON.stringify({ status: ready ? 'ok' : 'degraded', version: current.version, routes: current.count })) };
|
|
167
|
+
}
|
|
168
|
+
req.resume();
|
|
169
|
+
} else {
|
|
170
|
+
if (inFlight >= maxInFlightRequests) { counters.shed('requests'); throw new HttpError(503, 'HTTP request capacity unavailable'); }
|
|
171
|
+
inFlight++; counters.inFlight('requests', 1);
|
|
172
|
+
// Keep admission until the response finishes or the peer disconnects.
|
|
173
|
+
let released = false;
|
|
174
|
+
const release = () => { if (!released) { released = true; inFlight--; counters.inFlight('requests', -1); } };
|
|
175
|
+
res.once('finish', release); res.once('close', release);
|
|
176
|
+
const headers = new Headers(), headerCounts = Object.create(null) ;
|
|
177
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
178
|
+
const key = (req.rawHeaders[i] ?? '').toLowerCase();
|
|
179
|
+
headers.append(key, req.rawHeaders[i + 1] ?? ''); headerCounts[key] = (headerCounts[key] || 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
const target = originForm(url);
|
|
182
|
+
const body = await readBody(req, Math.min(maxBodyBytes, current.requestLimit(target) ?? maxBodyBytes));
|
|
183
|
+
result = await current.handle({ target, method, headers, headerCounts, body, trace,
|
|
184
|
+
origin: publicOrigin(), client: resolveClient(req.socket.remoteAddress, headerCounts['x-forwarded-for'] === 1 ? headers.get('x-forwarded-for') ?? undefined : undefined, proxies) });
|
|
185
|
+
}
|
|
186
|
+
status = writeResponse(res, result, { requestId, method });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
status = writeError(res, error, { requestId, method, headers: current.errorHeaders(error, publicOrigin()) });
|
|
189
|
+
} finally {
|
|
190
|
+
// No request URL, query, headers, body, bindings or thrown operator errors.
|
|
191
|
+
// Detailed adds the method and the matched route pattern: both come from the
|
|
192
|
+
// reviewed configuration, never from request-supplied path or query text.
|
|
193
|
+
// Counters always see the matched pattern and whether this was a probe;
|
|
194
|
+
// observers see exactly the record the logger does.
|
|
195
|
+
emit({ event: 'request', requestId, status, durationMs: Math.round((performance.now() - started) * 100) / 100,
|
|
196
|
+
...(requestLog === 'detailed' ? { method, route: trace.route ?? null } : {}) }, { ...(trace.probe || trace.route === undefined ? {} : { route: trace.route }), probe: trace.probe === true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const publicOrigin = () => origin || `http://${host.includes(':') ? `[${host}]` : host}:${address.port}`;
|
|
200
|
+
// Counters live with the server, so a reload does not reset them; the slot
|
|
201
|
+
// gauges belong to whichever runtime is serving now.
|
|
202
|
+
const snapshot = () => { const { healthy, slots } = current.workers; const out = counters.snapshot(); out.functionWorkers.healthySlots = healthy; out.functionWorkers.slots = slots; return out; };
|
|
203
|
+
let metricsTimer ;
|
|
204
|
+
if (metricsIntervalMs) { metricsTimer = setInterval(() => sink.publish(snapshot()), metricsIntervalMs); metricsTimer.unref(); }
|
|
205
|
+
server.setTimeout(15000, socket => socket.destroy());
|
|
206
|
+
server.maxRequestsPerSocket = 1000;
|
|
207
|
+
server.maxConnections = 1024;
|
|
208
|
+
server.on('clientError', (error, socket) => {
|
|
209
|
+
// Header fields past the parser's budget are 431 (RFC 6585 §5); every
|
|
210
|
+
// other parse failure is a malformed message, 400.
|
|
211
|
+
const status = 'code' in error && error.code === 'HPE_HEADER_OVERFLOW' ? '431 Request Header Fields Too Large' : '400 Bad Request';
|
|
212
|
+
if (socket.writable) socket.end(`HTTP/1.1 ${status}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
await new Promise ((resolve, reject) => { server.once('error', reject); server.listen(port,host, () => { server.off('error',reject); resolve(); }); });
|
|
216
|
+
} catch (error) { await current.close(); throw error; }
|
|
217
|
+
const listening = server.address();
|
|
218
|
+
assert(listening !== null && typeof listening === 'object', 'Server has no address');
|
|
219
|
+
const address = listening;
|
|
220
|
+
async function reload() {
|
|
221
|
+
if (shuttingDown || reloading) return false;
|
|
222
|
+
reloading = true;
|
|
223
|
+
try {
|
|
224
|
+
const next = await createRuntime(project, { local, log: emit, origin, ...runtimeOptions });
|
|
225
|
+
if (shuttingDown) { await next.close(); return false; }
|
|
226
|
+
const old = current; current = next;
|
|
227
|
+
const cleanup = old.close(); retired.add(cleanup); void cleanup.finally(() => retired.delete(cleanup));
|
|
228
|
+
emit({ event: 'reload', status: 'ok', version: current.version, routes: current.count });
|
|
229
|
+
return true;
|
|
230
|
+
} catch { emit({ event: 'reload', status: 'rejected' }); return false; }
|
|
231
|
+
finally { reloading = false; }
|
|
232
|
+
}
|
|
233
|
+
if (watch) {
|
|
234
|
+
try { lastFingerprint = await fingerprint(current.root, local, current.assetWatch); }
|
|
235
|
+
catch { await new Promise (resolve => server.close(() => resolve())); await current.close(); throw new Error('Unable to watch project'); }
|
|
236
|
+
interval = setInterval(async () => {
|
|
237
|
+
if (reloading || shuttingDown || watching) return;
|
|
238
|
+
watching = true;
|
|
239
|
+
try {
|
|
240
|
+
const next = await fingerprint(current.root, local, current.assetWatch);
|
|
241
|
+
if (next !== lastFingerprint) { lastFingerprint = next; await reload(); }
|
|
242
|
+
} catch { emit({ event: 'watch', status: 'failed' }); }
|
|
243
|
+
finally { watching = false; }
|
|
244
|
+
}, 500);
|
|
245
|
+
interval.unref();
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
server, reload, address, root: current.root, testPlan: () => current.testPlan(),
|
|
249
|
+
linkEventStats: () => observer?.stats(),
|
|
250
|
+
metrics: snapshot,
|
|
251
|
+
get observers() { return observers.map(observer => ({ name: observer.name, version: observer.version })); },
|
|
252
|
+
// What a request sees as its own origin: behind a tunnel or proxy this is
|
|
253
|
+
// the operator's --origin, never a forwarded header.
|
|
254
|
+
origin: publicOrigin(),
|
|
255
|
+
async close() {
|
|
256
|
+
shuttingDown = true; clearInterval(interval); clearInterval(metricsTimer);
|
|
257
|
+
const deadline = setTimeout(() => server.closeAllConnections(), 10000);
|
|
258
|
+
deadline.unref();
|
|
259
|
+
await new Promise (resolve => server.close(() => resolve()));
|
|
260
|
+
clearTimeout(deadline);
|
|
261
|
+
while (reloading) await new Promise(resolve => setTimeout(resolve,10));
|
|
262
|
+
await current.close(); await Promise.all(retired);
|
|
263
|
+
// Observers drain after the connections they describe are gone.
|
|
264
|
+
if (observer) emit({ event: 'link_observer', status: 'closed', ...await observer.close() });
|
|
265
|
+
// A final snapshot, then observers release in reverse order.
|
|
266
|
+
sink.publish(snapshot());
|
|
267
|
+
await sink.close();
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
package/dist/site.js
ADDED
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// https://www.sqlite.org/wal.html#the_wal_reset_bug
|
|
2
|
+
export function supportsConcurrentWal(version ) {
|
|
3
|
+
const [major=NaN,minor=NaN,patch=NaN]=String(version).split('.').map(Number);
|
|
4
|
+
if(![major,minor,patch].every(Number.isInteger))return false;
|
|
5
|
+
return major>3 || major===3 && (minor>51 || minor===51&&patch>=3 || minor===50&&patch>=7 || minor===44&&patch>=6);
|
|
6
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HostPlugin, OperatorPolicy, Runtime } from './runtime.ts';
|
|
2
|
+
import type { TargetName } from './types.ts';
|
|
3
|
+
/** The subset of process.env a hosted adapter reads. */
|
|
4
|
+
export type Environment = Record<string, string | undefined>;
|
|
5
|
+
export interface NativeOnlyOptions {
|
|
6
|
+
target?: TargetName | undefined;
|
|
7
|
+
plugins?: HostPlugin[] | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function readPolicyFromEnvironment(environment: Environment): OperatorPolicy | undefined;
|
|
10
|
+
export declare function activateNativeOnly(project: string, environment: Environment, { target, plugins }?: NativeOnlyOptions): Promise<Runtime>;
|
|
11
|
+
export declare function lazyRuntime<T>(activate: () => Promise<T>): () => Promise<T>;
|
|
12
|
+
export declare function resolveOrigin(origin: string | undefined, environment: Environment, platformVariables: string[]): string | undefined;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentEntry, AgentsConfig } from './policies/agents.ts';
|
|
2
|
+
export declare const MAX_LIST_ENTRIES = 4096;
|
|
3
|
+
export declare function loadListFile(root: string, reference: string, routePattern?: string): Promise<AgentEntry[]>;
|
|
4
|
+
export declare function loadListFiles(references: string[], root: string | undefined, routePattern: string): Promise<Record<string, AgentEntry[]>>;
|
|
5
|
+
export declare function resolveLists(config: AgentsConfig | undefined, root: string, routePattern?: string): Promise<AgentsConfig | undefined>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AssetResult, CompiledRoute } from './types.ts';
|
|
2
|
+
/** The header surface assetResponse reads for conditional and range requests. */
|
|
3
|
+
export interface AssetRequestHeaders {
|
|
4
|
+
has(name: string): boolean;
|
|
5
|
+
get(name: string): string | null | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface AssetSnapshot {
|
|
8
|
+
watch: string[];
|
|
9
|
+
digest: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const publishableAssetName: (name: string) => boolean;
|
|
12
|
+
export declare function compileAssets(root: string, routes: CompiledRoute[]): Promise<AssetSnapshot>;
|
|
13
|
+
export declare function assetResponse(route: CompiledRoute, path: string, method: string, request: AssetRequestHeaders): AssetResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Environment } from './adapters.ts';
|
|
2
|
+
import type { HostPlugin } from './runtime.ts';
|
|
3
|
+
export interface LambdaHandlerOptions {
|
|
4
|
+
project?: string | undefined;
|
|
5
|
+
origin?: string | undefined;
|
|
6
|
+
environment?: Environment | undefined;
|
|
7
|
+
maxBodyBytes?: number | undefined;
|
|
8
|
+
plugins?: HostPlugin[] | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** A Lambda payload format 2.0 event, as far as this adapter reads it. */
|
|
11
|
+
export interface LambdaEvent {
|
|
12
|
+
version?: string;
|
|
13
|
+
httpMethod?: string;
|
|
14
|
+
rawPath?: string;
|
|
15
|
+
rawQueryString?: string;
|
|
16
|
+
headers?: Record<string, string | undefined>;
|
|
17
|
+
cookies?: string[];
|
|
18
|
+
body?: string | null;
|
|
19
|
+
isBase64Encoded?: boolean;
|
|
20
|
+
requestContext?: {
|
|
21
|
+
http?: {
|
|
22
|
+
method?: string;
|
|
23
|
+
sourceIp?: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface LambdaResponse {
|
|
28
|
+
statusCode: number;
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
cookies: string[];
|
|
31
|
+
body: string;
|
|
32
|
+
isBase64Encoded: true;
|
|
33
|
+
}
|
|
34
|
+
export type LambdaHandler = (event: unknown) => Promise<LambdaResponse>;
|
|
35
|
+
export declare function createLambdaHandler({ project, origin, environment, maxBodyBytes, plugins }?: LambdaHandlerOptions): LambdaHandler;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LogFn } from './types.ts';
|
|
2
|
+
export interface BuildOptions {
|
|
3
|
+
out?: string | undefined;
|
|
4
|
+
origin?: string | undefined;
|
|
5
|
+
log?: LogFn | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildReport {
|
|
8
|
+
out: string;
|
|
9
|
+
format: number;
|
|
10
|
+
version: string;
|
|
11
|
+
routes: number;
|
|
12
|
+
validators: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildCloudflare(project: string, { out, origin, log }?: BuildOptions): Promise<BuildReport>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Cidr {
|
|
2
|
+
bytes: Uint8Array;
|
|
3
|
+
prefix: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function parseCidr(text: string): Cidr;
|
|
6
|
+
export declare function compileTrustedProxies(list?: string | string[]): Cidr[];
|
|
7
|
+
export declare function isTrustedProxy(address: string, trusted: Cidr[]): boolean;
|
|
8
|
+
export declare function resolveClient(peer: string | undefined, forwarded: string | undefined, trusted: Cidr[]): string | undefined;
|
|
9
|
+
export declare function normalizeAddress(address: unknown): string | undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CompiledRoutes, MatchableRoute, ParameterLocation, ParameterSchema } from './match.ts';
|
|
2
|
+
import type { RequestBodyPolicy } from './http-policy.ts';
|
|
3
|
+
import type { HeaderPair } from './http-response.ts';
|
|
4
|
+
import type { AgentsState } from './policies/agents.ts';
|
|
5
|
+
import type { SecurityConfig, SecurityState } from './policies/security.ts';
|
|
6
|
+
import type { CompiledRedirect, PolicyModule } from './types.ts';
|
|
7
|
+
export interface ArtifactParameter {
|
|
8
|
+
name: string;
|
|
9
|
+
in: ParameterLocation;
|
|
10
|
+
required: boolean;
|
|
11
|
+
schema: ParameterSchema;
|
|
12
|
+
validator: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ArtifactReply {
|
|
15
|
+
status: number;
|
|
16
|
+
headers: HeaderPair[];
|
|
17
|
+
body: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ArtifactRoute {
|
|
20
|
+
pattern: string;
|
|
21
|
+
parts: string[];
|
|
22
|
+
names: string[];
|
|
23
|
+
methods: string[];
|
|
24
|
+
parameters: ArtifactParameter[];
|
|
25
|
+
responseHeaders: HeaderPair[];
|
|
26
|
+
request?: {
|
|
27
|
+
body?: RequestBodyPolicy;
|
|
28
|
+
};
|
|
29
|
+
redirect?: CompiledRedirect;
|
|
30
|
+
reply?: ArtifactReply;
|
|
31
|
+
enabled?: false;
|
|
32
|
+
expiresAt?: number;
|
|
33
|
+
policies?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export interface Artifact {
|
|
36
|
+
format: number;
|
|
37
|
+
version: string;
|
|
38
|
+
routes: ArtifactRoute[];
|
|
39
|
+
policies?: {
|
|
40
|
+
security: SecurityConfig;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export type Validator = (value: unknown) => boolean;
|
|
44
|
+
export type Validators = Record<string, Validator | undefined>;
|
|
45
|
+
/** A route's compiled policy chain on this target: the same hook pairs the Node runtime holds. */
|
|
46
|
+
export interface WorkerPolicy {
|
|
47
|
+
request: [PolicyModule, unknown][];
|
|
48
|
+
response: [PolicyModule, unknown][];
|
|
49
|
+
security?: SecurityState;
|
|
50
|
+
agents?: AgentsState;
|
|
51
|
+
}
|
|
52
|
+
/** A rehydrated route: MatchableRoute plus what the request path reads. The reply body is bytes, not a Buffer. */
|
|
53
|
+
export interface WorkerRoute extends MatchableRoute {
|
|
54
|
+
names: string[];
|
|
55
|
+
methods: string[];
|
|
56
|
+
responseHeaders: HeaderPair[];
|
|
57
|
+
request?: {
|
|
58
|
+
body?: RequestBodyPolicy;
|
|
59
|
+
};
|
|
60
|
+
redirect?: CompiledRedirect;
|
|
61
|
+
reply: {
|
|
62
|
+
status: number;
|
|
63
|
+
headers: HeaderPair[];
|
|
64
|
+
body: Uint8Array<ArrayBuffer>;
|
|
65
|
+
} | undefined;
|
|
66
|
+
enabled?: false;
|
|
67
|
+
expiresAt?: number;
|
|
68
|
+
middleware: never[];
|
|
69
|
+
policy: WorkerPolicy | null;
|
|
70
|
+
}
|
|
71
|
+
export interface RehydratedArtifact extends CompiledRoutes<WorkerRoute> {
|
|
72
|
+
errorPolicy: SecurityState | null;
|
|
73
|
+
}
|
|
74
|
+
export declare function rehydrate(artifact: Artifact, validators?: Validators): RehydratedArtifact;
|
|
75
|
+
export declare function createFetchHandler(artifact: Artifact, validators?: Validators): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComplianceRule, ProjectRule, RouteRule } from '../compliance.ts';
|
|
2
|
+
export declare const profile = "baseline";
|
|
3
|
+
export declare const securityHeaders: RouteRule;
|
|
4
|
+
export declare const hstsOrigin: ProjectRule;
|
|
5
|
+
export declare const secretsCompression: RouteRule;
|
|
6
|
+
export declare const secretsNoStore: RouteRule;
|
|
7
|
+
export declare const cacheControlDeclared: RouteRule;
|
|
8
|
+
export declare const throttleFunctions: RouteRule;
|
|
9
|
+
export declare const robots: ProjectRule;
|
|
10
|
+
export declare const expiredRoutes: ProjectRule;
|
|
11
|
+
export declare const managementPrivate: ProjectRule;
|
|
12
|
+
export declare const rules: readonly ComplianceRule[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComplianceRule, ProjectRule, RouteRule } from '../compliance.ts';
|
|
2
|
+
export declare const profile = "privacy";
|
|
3
|
+
export declare const requestLogMinimal: ProjectRule;
|
|
4
|
+
export declare const linkEventsOff: ProjectRule;
|
|
5
|
+
export declare const detailedParameters: RouteRule;
|
|
6
|
+
export declare const rules: readonly ComplianceRule[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Standard } from '../compliance.ts';
|
|
2
|
+
import type { EffectivePolicies, PlanInventoryEntry, PolicyInventory, RouteConfig } from '../types.ts';
|
|
3
|
+
export declare const oshp: Standard;
|
|
4
|
+
export declare const rfc9111: Standard;
|
|
5
|
+
export declare const rfc9110: Standard;
|
|
6
|
+
export declare const rfc6585: Standard;
|
|
7
|
+
export declare const rfc9309: Standard;
|
|
8
|
+
export declare const breach: Standard;
|
|
9
|
+
export declare const monitoring: Standard;
|
|
10
|
+
export declare const linkChannel: Standard;
|
|
11
|
+
export declare const management: Standard;
|
|
12
|
+
export declare const agentLists: Standard;
|
|
13
|
+
export declare const active: (route: PlanInventoryEntry) => boolean;
|
|
14
|
+
export declare const functionLike: (route: PlanInventoryEntry) => boolean;
|
|
15
|
+
export declare const hasSecrets: (config: RouteConfig | undefined) => boolean;
|
|
16
|
+
export declare function yamlHeader(config: RouteConfig | undefined, name: string): string | string[] | undefined;
|
|
17
|
+
export declare function yamlHeaderBytes(config: RouteConfig | undefined): number;
|
|
18
|
+
export declare function handlerCacheControl(config: RouteConfig | undefined): {
|
|
19
|
+
declared: boolean;
|
|
20
|
+
value: string;
|
|
21
|
+
} | null;
|
|
22
|
+
export declare const directive: (value: unknown, token: string) => boolean;
|
|
23
|
+
export declare function emittedSecurityHeaders(effective: EffectivePolicies | undefined, policy: PolicyInventory | undefined): Set<string>;
|
|
24
|
+
export declare function securityHeaderBytes(effective: EffectivePolicies | undefined, policy: PolicyInventory | undefined): number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComplianceRule, RouteRule } from '../compliance.ts';
|
|
2
|
+
export declare const profile = "strict";
|
|
3
|
+
export declare const headerBudgetBytes = 8192;
|
|
4
|
+
export declare const csp: RouteRule;
|
|
5
|
+
export declare const throttleAll: RouteRule;
|
|
6
|
+
export declare const listsPinned: RouteRule;
|
|
7
|
+
export declare const redirectHttps: RouteRule;
|
|
8
|
+
export declare const headerBudget: RouteRule;
|
|
9
|
+
export declare const rules: readonly ComplianceRule[];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as baseline from './compliance-rules/baseline.ts';
|
|
2
|
+
import * as strict from './compliance-rules/strict.ts';
|
|
3
|
+
import * as privacy from './compliance-rules/privacy.ts';
|
|
4
|
+
import type { EffectivePolicies, PlanInventoryEntry, PolicyInventory, ProjectDocument, RouteConfig, TestPlan } from './types.ts';
|
|
5
|
+
export type Severity = 'high' | 'medium' | 'low' | 'info';
|
|
6
|
+
export interface Standard {
|
|
7
|
+
name: string;
|
|
8
|
+
reference: string;
|
|
9
|
+
section?: string;
|
|
10
|
+
}
|
|
11
|
+
/** What a rule's check returns per finding; severity and route default to the rule's and the context's. */
|
|
12
|
+
export interface RawFinding {
|
|
13
|
+
message: string;
|
|
14
|
+
remediation: string;
|
|
15
|
+
severity?: Severity;
|
|
16
|
+
route?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Finding {
|
|
19
|
+
rule: string;
|
|
20
|
+
severity: Severity;
|
|
21
|
+
route?: string;
|
|
22
|
+
message: string;
|
|
23
|
+
remediation: string;
|
|
24
|
+
standard: Standard;
|
|
25
|
+
}
|
|
26
|
+
export type RuleResult = RawFinding[] | RawFinding | null | undefined | false;
|
|
27
|
+
/** The host settings under review, null where the operator declared nothing. */
|
|
28
|
+
export interface HostSettings {
|
|
29
|
+
requestLog: 'minimal' | 'detailed' | null;
|
|
30
|
+
linkEvents: boolean | null;
|
|
31
|
+
includeCode: boolean | null;
|
|
32
|
+
}
|
|
33
|
+
export interface ProjectContext {
|
|
34
|
+
document: ProjectDocument;
|
|
35
|
+
routes: Record<string, RouteConfig>;
|
|
36
|
+
plan: TestPlan;
|
|
37
|
+
policies: Record<string, EffectivePolicies>;
|
|
38
|
+
origin: string | null;
|
|
39
|
+
target: string;
|
|
40
|
+
host: HostSettings;
|
|
41
|
+
}
|
|
42
|
+
export interface RouteContext extends ProjectContext {
|
|
43
|
+
route: PlanInventoryEntry;
|
|
44
|
+
config: RouteConfig;
|
|
45
|
+
policy: PolicyInventory;
|
|
46
|
+
effective: EffectivePolicies;
|
|
47
|
+
}
|
|
48
|
+
interface RuleBase {
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
standard: Standard;
|
|
52
|
+
severity: Severity;
|
|
53
|
+
}
|
|
54
|
+
export interface ProjectRule extends RuleBase {
|
|
55
|
+
appliesTo: 'project';
|
|
56
|
+
check(context: ProjectContext): RuleResult | Promise<RuleResult>;
|
|
57
|
+
}
|
|
58
|
+
export interface RouteRule extends RuleBase {
|
|
59
|
+
appliesTo: 'route';
|
|
60
|
+
check(context: RouteContext): RuleResult | Promise<RuleResult>;
|
|
61
|
+
}
|
|
62
|
+
export type ComplianceRule = ProjectRule | RouteRule;
|
|
63
|
+
/** What an operator rules module exports, after loadComplianceRules checked it. */
|
|
64
|
+
export interface ComplianceRules {
|
|
65
|
+
rules: ComplianceRule[];
|
|
66
|
+
disable: string[];
|
|
67
|
+
override: Record<string, Partial<ComplianceRule>>;
|
|
68
|
+
}
|
|
69
|
+
export interface ComplianceOptions {
|
|
70
|
+
rules?: ComplianceRule[];
|
|
71
|
+
profile?: string;
|
|
72
|
+
ignore?: string[];
|
|
73
|
+
origin?: string | null | undefined;
|
|
74
|
+
target?: string;
|
|
75
|
+
host?: unknown;
|
|
76
|
+
override?: Record<string, Partial<ComplianceRule>> | undefined;
|
|
77
|
+
disable?: string[] | undefined;
|
|
78
|
+
}
|
|
79
|
+
/** The started server or runtime under review: its project root and test plan. */
|
|
80
|
+
export interface ComplianceApp {
|
|
81
|
+
root: string;
|
|
82
|
+
testPlan(): TestPlan;
|
|
83
|
+
}
|
|
84
|
+
export interface ComplianceReport {
|
|
85
|
+
profile: string;
|
|
86
|
+
rules: number;
|
|
87
|
+
ruleIds: string[];
|
|
88
|
+
ignored: string[];
|
|
89
|
+
findings: Finding[];
|
|
90
|
+
counts: Record<Severity, number>;
|
|
91
|
+
pass: boolean;
|
|
92
|
+
evidence: {
|
|
93
|
+
routes: number;
|
|
94
|
+
active: number;
|
|
95
|
+
dynamicLinks: boolean;
|
|
96
|
+
policies: string[];
|
|
97
|
+
files: string[];
|
|
98
|
+
origin: string | null;
|
|
99
|
+
target: string;
|
|
100
|
+
host: HostSettings;
|
|
101
|
+
scope: string;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export declare const severities: readonly Severity[];
|
|
105
|
+
export declare const idPattern: RegExp;
|
|
106
|
+
export type ComplianceProfileName = typeof baseline.profile | typeof strict.profile | typeof privacy.profile;
|
|
107
|
+
export declare const builtinProfiles: Readonly<Record<ComplianceProfileName, readonly ComplianceRule[]>>;
|
|
108
|
+
export declare const profileNames: readonly string[];
|
|
109
|
+
export declare function validateRule(rule: unknown): ComplianceRule;
|
|
110
|
+
export declare function validateRules(rules: unknown): ComplianceRule[];
|
|
111
|
+
export declare function resolveRules({ profile, rules, override, disable }?: {
|
|
112
|
+
profile?: string;
|
|
113
|
+
rules?: unknown;
|
|
114
|
+
override?: unknown;
|
|
115
|
+
disable?: unknown;
|
|
116
|
+
}): ComplianceRule[];
|
|
117
|
+
export declare function loadComplianceRules(file: string | undefined, project: string): Promise<ComplianceRules | undefined>;
|
|
118
|
+
export declare function runCompliance(app: ComplianceApp, { rules, profile, ignore, origin, target, host, override, disable }?: ComplianceOptions): Promise<ComplianceReport>;
|
|
119
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|