@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3
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/.claude/skills/urlcode-authoring/SKILL.md +36 -22
- package/.claude/skills/urlcode-operations/SKILL.md +16 -22
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +20 -16
- package/README.md +83 -80
- package/ROADMAP.md +61 -331
- package/SECURITY.md +23 -8
- package/dist/BUILD-MANIFEST.json +42 -47
- package/dist/agents-guide.js +18 -18
- package/dist/build-static.js +136 -0
- package/dist/capabilities.js +85 -31
- package/dist/capability-query.js +0 -1
- package/dist/cli.js +24 -32
- package/dist/compliance-rules/baseline.js +2 -10
- package/dist/compliance-rules/privacy.js +5 -16
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance.js +6 -8
- package/dist/config.js +15 -8
- package/dist/context.js +9 -10
- package/dist/examples.js +2 -2
- package/dist/explain-cli.js +7 -5
- package/dist/explain.js +10 -5
- package/dist/extensions.js +61 -2
- package/dist/function-sources.js +34 -2
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +3 -6
- package/dist/manifest.js +11 -7
- package/dist/mcp-authoring.js +2 -2
- package/dist/mcp.js +1 -1
- package/dist/observability.js +1 -21
- package/dist/policies/cache.js +8 -3
- package/dist/policies.js +3 -1
- package/dist/policy.js +28 -9
- package/dist/prerender.js +4 -0
- package/dist/project-tests.js +3 -3
- package/dist/readiness.js +35 -9
- package/dist/route-diff.js +12 -5
- package/dist/router.js +5 -7
- package/dist/runtime.js +77 -58
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/tooling.js +1 -1
- package/dist/trusted-functions.js +210 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/capabilities.d.ts +14 -5
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/config.d.ts +2 -1
- package/dist/types/context.d.ts +2 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/explain.d.ts +6 -0
- package/dist/types/extensions.d.ts +55 -0
- package/dist/types/function-sources.d.ts +4 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +3 -6
- package/dist/types/manifest.d.ts +5 -3
- package/dist/types/observability.d.ts +1 -14
- package/dist/types/project-tests.d.ts +1 -2
- package/dist/types/readiness.d.ts +12 -3
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +0 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/server.d.ts +1 -4
- package/dist/types/tooling.d.ts +3 -3
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +22 -8
- package/dist/types/verify-deployment.d.ts +2 -2
- package/dist/types.js +25 -5
- package/dist/typescript-authoring.js +67 -18
- package/dist/verify-deployment.js +3 -3
- package/docs/AI-AUTHORING.md +109 -10
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +17 -8
- package/docs/CAPABILITIES.md +30 -17
- package/docs/CAPACITY.md +128 -31
- package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
- package/docs/CI.md +8 -3
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
- package/docs/COMPLIANCE.md +6 -9
- package/docs/DEPLOYMENT-CHECKS.md +1 -1
- package/docs/DEVELOPMENT-PIPELINE.md +181 -0
- package/docs/EXTENSIONS.md +193 -11
- package/docs/FRAMEWORK.md +53 -33
- package/docs/FUNCTION-SECURITY.md +173 -32
- package/docs/INSTALL.md +0 -5
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/MIDDLEWARE.md +27 -16
- package/docs/MONITORING.md +2 -19
- package/docs/OBSERVABILITY.md +7 -16
- package/docs/OPEN-DECISIONS.md +184 -0
- package/docs/OPERATIONAL-PROOF.md +26 -30
- package/docs/OPERATIONS.md +23 -32
- package/docs/POLICIES.md +27 -8
- package/docs/PRERENDER.md +25 -13
- package/docs/PROJECT-DIRECTION.md +35 -10
- package/docs/READINESS.md +6 -2
- package/docs/README.md +20 -41
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-READINESS.md +50 -30
- package/docs/RELEASE-SECURITY.md +72 -86
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +7 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +42 -2
- package/docs/SPECIFICATION.md +63 -27
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
- package/docs/SPIKE-CORE-LAYERING.md +368 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
- package/docs/STARTERS.md +1 -1
- package/docs/STATIC.md +105 -0
- package/docs/TOOLING.md +17 -12
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +32 -12
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +46 -0
- package/docs/YAML-GUIDE.md +0 -3
- package/docs/YAML-REFERENCE.md +16 -6
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
- package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
- package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
- package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
- package/docs/archive/2026-09-19/ROADMAP.md +386 -0
- package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
- package/docs/archive/README.md +27 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/examples/assets/example.yaml +2 -2
- package/examples/aws/example.yaml +1 -1
- package/examples/cloudflare/example.yaml +1 -1
- package/examples/conditions/example.yaml +1 -1
- package/examples/cookbook/example.yaml +1 -1
- package/examples/cookbook/middleware/bucket.mjs +12 -2
- package/examples/cookbook/middleware/locale.mjs +7 -3
- package/examples/cookbook/route-index.json +1 -1
- package/examples/cookbook/routes/middleware.yaml +1 -1
- package/examples/egress/example.yaml +1 -1
- package/examples/extensions/example.yaml +1 -1
- package/examples/prerender/README.md +14 -6
- package/examples/prerender/example.yaml +1 -1
- package/examples/prerender/functions/page.mjs +4 -2
- package/examples/prerender/middleware/template.mjs +1 -1
- package/examples/prerender/prerender.mjs +1 -1
- package/examples/prerender/urlcode.yaml +8 -4
- package/examples/provider-conformance/example.yaml +1 -1
- package/examples/vercel/example.yaml +1 -1
- package/llms-full.txt +686 -239
- package/llms.txt +27 -15
- package/package.json +32 -5
- package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
- package/recipes/authenticated-json-api/README.md +4 -3
- package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
- package/recipes/authenticated-json-api/recipe.yaml +2 -2
- package/recipes/contact-form/functions/contact.mjs +2 -1
- package/recipes/contact-form/recipe.yaml +3 -3
- package/recipes/cors-api/README.md +2 -2
- package/recipes/cors-api/recipe.yaml +2 -2
- package/recipes/health-page/README.md +1 -1
- package/recipes/health-page/recipe.yaml +1 -1
- package/recipes/json-api/README.md +1 -1
- package/recipes/json-api/recipe.yaml +4 -4
- package/recipes/middleware/README.md +8 -4
- package/recipes/middleware/middleware/bucket.mjs +12 -2
- package/recipes/middleware/middleware/locale.mjs +7 -3
- package/recipes/middleware/recipe.yaml +1 -1
- package/recipes/protected-download/README.md +1 -1
- package/recipes/protected-download/recipe.yaml +2 -2
- package/recipes/redirect/recipe.yaml +1 -1
- package/recipes/static-plus-api/README.md +2 -2
- package/recipes/static-plus-api/public/index.html +1 -1
- package/recipes/static-plus-api/recipe.yaml +2 -2
- package/recipes/static-plus-api/urlcode.yaml +1 -1
- package/recipes/typescript/README.md +2 -1
- package/recipes/typescript/recipe.yaml +5 -5
- package/recipes/webhook-receiver/README.md +5 -1
- package/recipes/webhook-receiver/recipe.yaml +2 -1
- package/recipes/webhook-receiver/urlcode.yaml +7 -0
- package/schemas/recipe.schema.json +4 -3
- package/schemas/urlcode.schema.json +14 -41
- package/skills/urlcode/SKILL.md +32 -11
- package/starters/default/AGENTS.md +18 -18
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -40
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -37
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -8
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -61
- package/docs/links/cli.md +0 -110
- package/docs/links/limits.md +0 -175
- package/docs/links/management-api.md +0 -80
- package/docs/links/pools.md +0 -75
- package/docs/links/setup.md +0 -135
- package/docs/yaml/links.md +0 -30
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/example.yaml +0 -21
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
package/dist/extensions.js
CHANGED
|
@@ -8,7 +8,18 @@ import { validateHeaderName, validateHeaderValue } from './header-validation.js'
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* `root` is the project's resolved absolute directory (the same value
|
|
13
|
+
* `loadDocument()` computes and `functionFile()`/router.ts resolves
|
|
14
|
+
* `function`/`middleware` `source` entries against). It is not the process's
|
|
15
|
+
* `cwd()`: `--project`/`--host-file` are independent paths, a server can be
|
|
16
|
+
* started from any working directory, and the JS API can load a project
|
|
17
|
+
* programmatically with no relationship to `cwd()` at all. An extension that
|
|
18
|
+
* resolves project-relative paths of its own (for example, an alternative
|
|
19
|
+
* middleware source it loads by convention rather than through core's native
|
|
20
|
+
* `middleware:` array) must resolve them against this field, never `cwd()`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
12
23
|
|
|
13
24
|
|
|
14
25
|
|
|
@@ -17,6 +28,20 @@ import { validateHeaderName, validateHeaderValue } from './header-validation.js'
|
|
|
17
28
|
|
|
18
29
|
|
|
19
30
|
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
20
45
|
|
|
21
46
|
|
|
22
47
|
/** Trusted operator code only. YAML declares names/configuration, never modules. */
|
|
@@ -30,6 +55,23 @@ import { validateHeaderName, validateHeaderValue } from './header-validation.js'
|
|
|
30
55
|
|
|
31
56
|
|
|
32
57
|
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
33
75
|
|
|
34
76
|
|
|
35
77
|
/** `urlcode init --with <name>` contract: what core hands `@jimhoyd/urlcode-<name>`'s `scaffold` export. Nothing is written by `scaffold`. */
|
|
@@ -85,6 +127,23 @@ export function effectiveExtensionPolicies(document ,route
|
|
|
85
127
|
return result;
|
|
86
128
|
}
|
|
87
129
|
export function hasExtensionPolicy(document ,route ) {return Object.keys(effectiveExtensionPolicies(document,route)).length>0;}
|
|
130
|
+
/**
|
|
131
|
+
* Whether a route naming these `policies.extensions` names must be treated as
|
|
132
|
+
* confidential (forced no-store, no compression, extension response caps).
|
|
133
|
+
* `names` with no entries is never confidential. Without a `registrations`
|
|
134
|
+
* list to consult (a build path with no operator host loaded), every name is
|
|
135
|
+
* treated as sensitive: the safe default this can only relax, never weaken.
|
|
136
|
+
* A name whose registration is missing, or declares no `cacheSensitive` (or
|
|
137
|
+
* `true`), counts as sensitive; only an explicit `cacheSensitive: false`
|
|
138
|
+
* excuses it, and one sensitive name among several makes the whole route
|
|
139
|
+
* confidential.
|
|
140
|
+
*/
|
|
141
|
+
export function isSensitiveExtensionPolicy(names ,registrations ) {
|
|
142
|
+
if(!names.length)return false;
|
|
143
|
+
if(!registrations)return true;
|
|
144
|
+
const byName=new Map(registrations.map(registration=>[registration.name,registration.cacheSensitive]));
|
|
145
|
+
return names.some(name=>byName.get(name)!==false);
|
|
146
|
+
}
|
|
88
147
|
export function prepareExtensions(document ,routes ,registrations ,context ) {
|
|
89
148
|
assert(registrations===undefined||Array.isArray(registrations)&®istrations.length<=16,'Extensions must be an array of at most 16 operator registrations');
|
|
90
149
|
const provided=new Map (),entries=new Map (),credentialHeaders=new Set ();
|
|
@@ -127,7 +186,7 @@ export function prepareExtensions(document ,routes
|
|
|
127
186
|
try{for(const {name,registration,config,policies,mounts,assetPrefixes}of preparations){
|
|
128
187
|
const instance=await registration.activate(config,frozen({...context,mounts}));
|
|
129
188
|
if(instance&&typeof instance==='object')entries.set(name,{instance,policies,assetPrefixes});
|
|
130
|
-
assert(instance&&typeof instance.handle==='function'&&(!policies.size||typeof instance.authorize==='function'),`Extension ${name} lacks a required handler or
|
|
189
|
+
assert(instance&&typeof instance.handle==='function'&&(!policies.size||typeof instance.authorize==='function'||typeof instance.middleware==='function'),`Extension ${name} lacks a required handler, authorization hook or middleware hook`);
|
|
131
190
|
entries.set(name,{instance,policies,assetPrefixes});
|
|
132
191
|
}}catch(error){for(const entry of [...entries.values()].reverse())try{await entry.instance.close?.();}catch{/* Keep the activation failure. */}throw error;}
|
|
133
192
|
return {entries,credentialHeaders:[...credentialHeaders],async close(){for(const entry of [...entries.values()].reverse())try{await entry.instance.close?.();}catch{/* Operators own extension lifecycle diagnostics. */}}};
|
package/dist/function-sources.js
CHANGED
|
@@ -25,8 +25,35 @@ export const TOTAL_BYTE_LIMIT = 4194304;
|
|
|
25
25
|
|
|
26
26
|
export function routeFunctions (route ) { return [...(route.middleware || []), ...(route.function ? [route.function] : [])]; }
|
|
27
27
|
|
|
28
|
+
// Trusted (unsandboxed) routes are never bundled into a QuickJS module snapshot:
|
|
29
|
+
// they run through Node's own module resolution, with no restriction on bare
|
|
30
|
+
// specifiers, dynamic import or dependency count/size. For revision pinning
|
|
31
|
+
// (docs/SPIKE-DEFAULT-TRUST-MODEL.md, "trust-declaration integrity"), the
|
|
32
|
+
// project hash must still change when a trusted function/middleware's own
|
|
33
|
+
// source changes, so an operator's env/secret grant is invalidated the moment
|
|
34
|
+
// the code that could use it is edited. This only hashes each entry file's own
|
|
35
|
+
// bytes, not its transitive import graph the way the sandboxed collector does:
|
|
36
|
+
// a change to a helper module a trusted entry imports, without touching the
|
|
37
|
+
// entry file itself, does not by itself change the project hash. Documented as
|
|
38
|
+
// a known limitation in docs/FUNCTION-SECURITY.md.
|
|
39
|
+
export async function collectTrustedSources(routes , root ) {
|
|
40
|
+
const sources = Object.create(null);
|
|
41
|
+
for (const definition of routes.flatMap(routeFunctions)) {
|
|
42
|
+
const name = '/' + relative(root, definition.source).split(sep).join('/');
|
|
43
|
+
if (Object.hasOwn(sources, name)) continue;
|
|
44
|
+
sources[name] = await readFile(definition.source, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
return sources;
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
// Parse and snapshot source without ever importing project code into Node.
|
|
29
|
-
|
|
50
|
+
// Route-shaped callers go through `collectFunctionSources` below; a
|
|
51
|
+
// route-independent caller (the public sandbox primitive, src/sandbox.ts)
|
|
52
|
+
// calls this directly with its own explicit `{source, export}` list, so the
|
|
53
|
+
// module-allowlist walk and per-module/total byte budgets apply identically
|
|
54
|
+
// either way — there is exactly one collector, not a route-shaped one and a
|
|
55
|
+
// second generic one.
|
|
56
|
+
export async function collectSourcesFor(definitions , root ) {
|
|
30
57
|
await init;
|
|
31
58
|
const sources = Object.create(null), dependencies = Object.create(null);
|
|
32
59
|
const entries = [], names = new Map (), seenEntries = new Set ();
|
|
@@ -56,7 +83,7 @@ export async function collectFunctionSources(routes , root
|
|
|
56
83
|
}
|
|
57
84
|
return name;
|
|
58
85
|
}
|
|
59
|
-
for (const definition of
|
|
86
|
+
for (const definition of definitions) {
|
|
60
87
|
const name = await collect(definition.source);
|
|
61
88
|
names.set(definition.source,name);
|
|
62
89
|
const key = name + ":" + definition.export;
|
|
@@ -64,3 +91,8 @@ export async function collectFunctionSources(routes , root
|
|
|
64
91
|
}
|
|
65
92
|
return { sources,dependencies,entries,names };
|
|
66
93
|
}
|
|
94
|
+
/** Route-shaped convenience over `collectSourcesFor`: every existing caller (policy.ts,
|
|
95
|
+
* prerender.ts, typescript-authoring.ts) keeps working unchanged from `FunctionRoute[]`. */
|
|
96
|
+
export async function collectFunctionSources(routes , root ) {
|
|
97
|
+
return collectSourcesFor(routes.flatMap(routeFunctions), root);
|
|
98
|
+
}
|
package/dist/function-worker.js
CHANGED
|
@@ -83,6 +83,7 @@ port.on('message', async ({id,source,name,request,context,maxBytes,timeoutMs,cha
|
|
|
83
83
|
if (output === undefined || Buffer.byteLength(output) > maxBytes * 6 + 65536) throw new Error('Output limit');
|
|
84
84
|
const value = JSON.parse(output) ; // trust boundary: guest JSON, checked below
|
|
85
85
|
if (!value || !Number.isInteger(value.status) || value.status < 200 || value.status > 599 || typeof value.body !== 'string' || !Array.isArray(value.headers) || value.headers.length > 256) throw new Error('Invalid response');
|
|
86
|
+
if (value.contentLength !== undefined && (!Number.isInteger(value.contentLength) || value.contentLength < 0)) throw new Error('Invalid response');
|
|
86
87
|
if (value.nativeBody) {
|
|
87
88
|
if (!native || value.status !== native.status || value.body !== '') throw new Error('Invalid native response');
|
|
88
89
|
// Preserve native status and metadata (validators, ranges, redirect Location).
|
|
@@ -100,6 +101,7 @@ port.on('message', async ({id,source,name,request,context,maxBytes,timeoutMs,cha
|
|
|
100
101
|
bytes += Buffer.byteLength(pair[0]) + Buffer.byteLength(pair[1]) + 4;
|
|
101
102
|
}
|
|
102
103
|
if (bytes > 16384) throw new Error('Header limit');
|
|
103
|
-
post({id,status:value.status,headers:value.headers,body,nativeBody:value.nativeBody === true
|
|
104
|
+
post({id,status:value.status,headers:value.headers,body,nativeBody:value.nativeBody === true,
|
|
105
|
+
...(typeof value.contentLength === 'number' ? {contentLength:value.contentLength} : {})});
|
|
104
106
|
} catch { post({id,error:true}); }
|
|
105
107
|
});
|
package/dist/functions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { collectSourcesFor, routeFunctions } from './function-sources.js';
|
|
4
|
+
|
|
5
5
|
import { assert, ConfigError, HttpError } from './errors.js';
|
|
6
6
|
|
|
7
7
|
|
|
@@ -9,10 +9,24 @@ import { assert, ConfigError, HttpError } from './errors.js';
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** A module entry point the sandbox must load: an absolute source file path
|
|
13
|
+
* (already resolved and validated, e.g. via `functionFile()`) plus which
|
|
14
|
+
* export of it needs to be reachable. The same shape a `FunctionDefinition`
|
|
15
|
+
* already uses for `function`/`middleware` routes. */
|
|
16
|
+
|
|
17
|
+
/** What one `SandboxPool.execute()` call invokes: the same `{source, export}`
|
|
18
|
+
* shape as a `SandboxEntry`, naming one of the pool's declared entries. */
|
|
19
|
+
|
|
20
|
+
/** `entry` runs last (the route's `function`/handler); `chain` runs first, in
|
|
21
|
+
* order, each with `(request, context, next)` — the same wrap/middleware
|
|
22
|
+
* semantics `__invokePipeline` already gives sandboxed routes. Neither is
|
|
23
|
+
* required: an empty invocation with a `native` reply just returns it. */
|
|
24
|
+
|
|
25
|
+
|
|
13
26
|
|
|
14
27
|
|
|
15
28
|
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
// The worker protocol. Only JSON-shaped data and byte buffers cross it.
|
|
18
32
|
|
|
@@ -26,27 +40,43 @@ import { assert, ConfigError, HttpError } from './errors.js';
|
|
|
26
40
|
|
|
27
41
|
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
|
|
30
44
|
|
|
31
45
|
|
|
32
46
|
|
|
33
47
|
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
// The generalized sandbox primitive: the pool of worker threads, each running
|
|
50
|
+
// the same function-worker.ts (QuickJS engine setup, dependency-closure module
|
|
51
|
+
// allowlisting, memory/stack limits, deadline enforcement via both the
|
|
52
|
+
// interrupt handler and outer worker termination, response-shape validation),
|
|
53
|
+
// driven by an explicit list of `{source, export}` entries rather than
|
|
54
|
+
// anything route/YAML-shaped. This is the ONE place that owns worker
|
|
55
|
+
// spawning/QuickJS setup/deadline enforcement; `FunctionPool` below is a thin
|
|
56
|
+
// route-shaped wrapper over it, not a second copy of the mechanics. Exported
|
|
57
|
+
// publicly (as `@jimhoyd/urlcode/sandbox`, see src/sandbox.ts) for an
|
|
58
|
+
// extension package that needs to run PROJECT code — a hook a project's own
|
|
59
|
+
// config names — through the same trusted/sandboxed dispatch selection route
|
|
60
|
+
// dispatch gets, when that hook declares `sandbox: true`
|
|
61
|
+
// (docs/EXTENSIONS.md#project-level-lifecycle-hooks). There is no "trusted"
|
|
62
|
+
// mode here: an extension wanting trusted execution just calls the project's
|
|
63
|
+
// function directly via `import()` (already possible via
|
|
64
|
+
// `ExtensionActivation.root`); this primitive is only ever the sandboxed path.
|
|
65
|
+
export class SandboxPool {
|
|
66
|
+
root ; entries ; preparedSnapshot ; snapshot ;
|
|
37
67
|
restarts ; restartTimers ; log ; timeoutMs ; maxBytes ;
|
|
38
68
|
modules ; size ; slots ; closed ;
|
|
39
|
-
constructor(
|
|
69
|
+
constructor(entries , { root, snapshot, workers = 2, timeoutMs = 5000, maxBytes = 1048576, log = () => {} } = {}) {
|
|
40
70
|
assert(Number.isInteger(workers) && workers >= 1 && workers <= 32, 'Workers must be 1–32');
|
|
41
71
|
assert(Number.isInteger(timeoutMs) && timeoutMs >= 10 && timeoutMs <= 60000, 'Function timeout must be 10–60000 ms');
|
|
42
72
|
assert(Number.isInteger(maxBytes) && maxBytes >= 1 && maxBytes <= 16777216, 'Response limit must be 1–16777216 bytes');
|
|
43
|
-
this.root = root; this.
|
|
73
|
+
this.root = root; this.entries = entries; this.preparedSnapshot = snapshot;
|
|
44
74
|
// Consecutive replacement attempts per slot; cleared by a completed invocation.
|
|
45
75
|
this.restarts = new Map(); this.restartTimers = new Set();
|
|
46
76
|
this.log = log;
|
|
47
77
|
this.timeoutMs = timeoutMs; this.maxBytes = maxBytes;
|
|
48
78
|
const modules = new Map ();
|
|
49
|
-
for (const definition of
|
|
79
|
+
for (const definition of entries) {
|
|
50
80
|
const { source, export: name } = definition;
|
|
51
81
|
let names = modules.get(source);
|
|
52
82
|
if (!names) { names = new Set(); modules.set(source, names); }
|
|
@@ -58,7 +88,7 @@ export class FunctionPool {
|
|
|
58
88
|
}
|
|
59
89
|
async start() {
|
|
60
90
|
let snapshot = this.preparedSnapshot;
|
|
61
|
-
if (!snapshot && this.size) { assert(this.root !== undefined, 'Function pool requires a project root'); snapshot = await
|
|
91
|
+
if (!snapshot && this.size) { assert(this.root !== undefined, 'Function pool requires a project root'); snapshot = await collectSourcesFor(this.entries,this.root); }
|
|
62
92
|
snapshot ??= {sources:{},dependencies:{},entries:[],names:new Map()};
|
|
63
93
|
this.snapshot = snapshot;
|
|
64
94
|
try { await Promise.all(Array.from({ length: this.size }, (_, i) => this.spawn(i))); }
|
|
@@ -138,11 +168,18 @@ export class FunctionPool {
|
|
|
138
168
|
timer.unref(); this.restartTimers.add(timer);
|
|
139
169
|
}
|
|
140
170
|
get healthy() { return !this.closed && this.slots.length === this.size && this.slots.every(slot => slot?.ready); }
|
|
141
|
-
|
|
171
|
+
/** `invocation.entry` runs last (chain-wrapped), `invocation.chain` first, in
|
|
172
|
+
* declared order — the same `__invokePipeline` wrap/middleware discipline
|
|
173
|
+
* (`next()` callable at most once) the sandboxed route path already
|
|
174
|
+
* enforces; there is no second dispatch mechanism for this. Every entry and
|
|
175
|
+
* chain source named here must already be one of this pool's declared
|
|
176
|
+
* `entries` (constructor), or the worker's own module allowlist denies it. */
|
|
177
|
+
execute(invocation , request , context , native ) {
|
|
142
178
|
const slot = this.slots.find(s => s?.ready && !s.pending);
|
|
143
179
|
const snapshot = this.snapshot;
|
|
144
180
|
if (this.closed || !slot || !snapshot) return Promise.reject(new HttpError(503, 'Function capacity unavailable'));
|
|
145
181
|
const id = randomUUID();
|
|
182
|
+
const { entry, chain = [] } = invocation;
|
|
146
183
|
return new Promise((resolve, reject) => {
|
|
147
184
|
const timer = setTimeout(() => {
|
|
148
185
|
slot.pending = null; slot.ready = false;
|
|
@@ -154,8 +191,8 @@ export class FunctionPool {
|
|
|
154
191
|
if (message.nativeBody && native) resolve({...message,body:native.body,...(native.contentLength === undefined ? {} : {contentLength:native.contentLength})});
|
|
155
192
|
else resolve(message);
|
|
156
193
|
}, reject };
|
|
157
|
-
const message = { id, source:
|
|
158
|
-
chain:
|
|
194
|
+
const message = { id, source: entry ? snapshot.names.get(entry.source) : undefined, name: entry?.export,
|
|
195
|
+
chain: chain.map(item => ({source:snapshot.names.get(item.source),name:item.export})),
|
|
159
196
|
native: native ? {status:native.status,headers:native.headers} : undefined,
|
|
160
197
|
request, context, maxBytes: this.maxBytes, timeoutMs:this.timeoutMs + 100 };
|
|
161
198
|
slot.worker.postMessage(message);
|
|
@@ -172,3 +209,37 @@ export class FunctionPool {
|
|
|
172
209
|
await Promise.all(this.slots.map(s => s?.worker.terminate()));
|
|
173
210
|
}
|
|
174
211
|
}
|
|
212
|
+
// Thin, route-shaped wrapper over `SandboxPool`: every existing internal
|
|
213
|
+
// caller (route-level `function`/`middleware` dispatch, `src/runtime.ts`)
|
|
214
|
+
// keeps working exactly as before, translating `FunctionRoute[]`/`FunctionRoute`
|
|
215
|
+
// into the generalized `{source, export}` entries/target shape at the edge,
|
|
216
|
+
// never duplicating worker spawning, module allowlisting or deadline
|
|
217
|
+
// enforcement — that all still lives in the wrapped `SandboxPool`. Composition
|
|
218
|
+
// rather than inheritance because `execute()`'s route-shaped and
|
|
219
|
+
// entries-shaped parameters are genuinely different, incompatible types; the
|
|
220
|
+
// fields/methods below the constructor exist only so the (unchanged) test
|
|
221
|
+
// suite and `src/runtime.ts` keep reaching the wrapped pool's own state
|
|
222
|
+
// exactly as they did before this file had two classes.
|
|
223
|
+
export class FunctionPool {
|
|
224
|
+
routes ; pool ;
|
|
225
|
+
constructor(routes , options = {}) {
|
|
226
|
+
this.routes = routes;
|
|
227
|
+
this.pool = new SandboxPool(routes.flatMap(routeFunctions), options);
|
|
228
|
+
}
|
|
229
|
+
async start() { await this.pool.start(); return this; }
|
|
230
|
+
execute(route , request , context , native ) {
|
|
231
|
+
return this.pool.execute({ entry: route.function, chain: route.middleware }, request, context, native);
|
|
232
|
+
}
|
|
233
|
+
scheduleRespawn(index ) { this.pool.scheduleRespawn(index); }
|
|
234
|
+
close() { return this.pool.close(); }
|
|
235
|
+
get healthy() { return this.pool.healthy; }
|
|
236
|
+
get restarts() { return this.pool.restarts; }
|
|
237
|
+
get restartTimers() { return this.pool.restartTimers; }
|
|
238
|
+
get slots() { return this.pool.slots; }
|
|
239
|
+
get size() { return this.pool.size; }
|
|
240
|
+
get closed() { return this.pool.closed; }
|
|
241
|
+
get snapshot() { return this.pool.snapshot; }
|
|
242
|
+
get log() { return this.pool.log; }
|
|
243
|
+
get timeoutMs() { return this.pool.timeoutMs; }
|
|
244
|
+
get maxBytes() { return this.pool.maxBytes; }
|
|
245
|
+
}
|
package/dist/guest-api.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/** The request the guest receives (stringified as JSON in the worker). */
|
|
6
6
|
|
|
7
7
|
/** What the guest returns as JSON text; the worker enforces this shape before trusting it. */
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
// Runs only inside QuickJS/WASM. No native host functions or objects are exposed.
|
|
10
10
|
// This is the documented text/JSON subset, not a complete Fetch implementation.
|
|
11
11
|
export const guestBootstrap = String.raw`
|
|
@@ -13,6 +13,24 @@ export const guestBootstrap = String.raw`
|
|
|
13
13
|
const NativeJSON = JSON;
|
|
14
14
|
const stringify = JSON.stringify.bind(JSON);
|
|
15
15
|
const now = Date.now.bind(Date);
|
|
16
|
+
// The guest has no TextEncoder/Buffer; this counts the UTF-8 bytes a
|
|
17
|
+
// Response's text would occupy on the wire (matching Buffer.byteLength on
|
|
18
|
+
// the host side, including its handling of lone surrogates as U+FFFD),
|
|
19
|
+
// without pulling any Node capability into the sandbox (#144).
|
|
20
|
+
function byteLength(text) {
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
for (let i = 0; i < text.length; i++) {
|
|
23
|
+
const code = text.charCodeAt(i);
|
|
24
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
25
|
+
const next = text.charCodeAt(i + 1);
|
|
26
|
+
if (next >= 0xdc00 && next <= 0xdfff) { bytes += 4; i++; continue; }
|
|
27
|
+
bytes += 3; continue;
|
|
28
|
+
}
|
|
29
|
+
if (code >= 0xdc00 && code <= 0xdfff) { bytes += 3; continue; }
|
|
30
|
+
bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : 3;
|
|
31
|
+
}
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
16
34
|
const timers = new Map(); let next = 1;
|
|
17
35
|
globalThis.setTimeout = (fn, delay = 0) => {
|
|
18
36
|
if (typeof fn !== 'function' || timers.size >= 128) throw new Error('Timer limit');
|
|
@@ -94,8 +112,14 @@ export const guestBootstrap = String.raw`
|
|
|
94
112
|
const response = await dispatch(0);
|
|
95
113
|
if (violated || !(response instanceof Response)) throw new TypeError('Invalid middleware response');
|
|
96
114
|
const nativeBody = response === nativeResponse;
|
|
115
|
+
const isHead = input.request.method === 'HEAD';
|
|
116
|
+
// The real length is already known "for free": _text is a fully
|
|
117
|
+
// materialized string at construction time, no stream read needed
|
|
118
|
+
// (#144, mirroring #139's fix for the trusted path). Only the
|
|
119
|
+
// transmitted bytes are suppressed for HEAD, never the length.
|
|
97
120
|
globalThis.__output = stringify({status:response.status,headers:response.headers._pairs,
|
|
98
|
-
body:nativeBody ||
|
|
121
|
+
body:nativeBody || isHead ? '' : response._text,nativeBody,
|
|
122
|
+
...(isHead && !nativeBody ? {contentLength:byteLength(response._text)} : {})});
|
|
99
123
|
globalThis.__state = 'done';
|
|
100
124
|
} catch { globalThis.__state = 'failed'; }
|
|
101
125
|
};
|
|
@@ -106,7 +130,9 @@ export const guestBootstrap = String.raw`
|
|
|
106
130
|
const response = await handler(new Request(input.request.url, input.request), input.context);
|
|
107
131
|
if (!(response instanceof Response)) throw new TypeError('Return a Response');
|
|
108
132
|
const headers = response.headers._pairs;
|
|
109
|
-
|
|
133
|
+
const isHead = input.request.method === 'HEAD';
|
|
134
|
+
globalThis.__output = stringify({status:response.status,headers,body: isHead ? '' : response._text,
|
|
135
|
+
...(isHead ? {contentLength:byteLength(response._text)} : {})});
|
|
110
136
|
globalThis.__state = 'done';
|
|
111
137
|
} catch { globalThis.__state = 'failed'; }
|
|
112
138
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
export { createRuntime } from './runtime.js';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
export { startServer } from './server.js';
|
|
4
4
|
|
|
5
5
|
export { loadDocument, validateDocument, parseYaml } from './config.js';
|
|
6
|
-
export {openLinkStore} from './link-store.js';
|
|
7
|
-
|
|
8
|
-
export {startLinkApi} from './link-api.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
6
|
export { events as observabilityEvents, validateObservers, createObserverSink, createMetrics, renderPrometheus } from './observability.js';
|
|
12
7
|
|
|
13
8
|
export { getCapabilities, routeCapabilities, analyzeProjectCapabilities, analyzeCompiledCapabilities, assertTargetCompatibility, normalizeCapabilityTarget } from './capabilities.js';
|
|
@@ -39,6 +34,8 @@ export {normalizeMatch, assertDisjointMatches, matchesRoute} from './conditions.
|
|
|
39
34
|
|
|
40
35
|
export {buildCloudflare} from './build-cloudflare.js';
|
|
41
36
|
|
|
37
|
+
export {buildStatic} from './build-static.js';
|
|
38
|
+
|
|
42
39
|
export {runProjectTests} from './project-tests.js';
|
|
43
40
|
|
|
44
41
|
export {scaffoldProject} from './scaffold.js';
|
package/dist/manifest.js
CHANGED
|
@@ -15,9 +15,14 @@ import {explainCompiledRoute} from './explain.js';
|
|
|
15
15
|
// checked in as a source of truth, and it is deterministic: the same project
|
|
16
16
|
// yields the same bytes. Nothing in it is a binding value or source text.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Bumped to 3 when `sandbox`/`sandboxReason` moved from the `function` handler
|
|
19
|
+
// record to the route, so a middleware-only route reports its execution mode too.
|
|
20
|
+
export const MANIFEST_SCHEMA_VERSION=3;
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
|
|
@@ -30,7 +35,7 @@ export const MANIFEST_SCHEMA_VERSION=1;
|
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
|
|
@@ -61,14 +66,13 @@ export async function buildManifest(project ,options ={})
|
|
|
61
66
|
const explanations=routes.map(route=>explainCompiledRoute(loaded,route,chains.get(route.pattern),{projectSha256,now:0})).sort((a,b)=>compare(a.path,b.path));
|
|
62
67
|
const functions=new Map (),middleware=new Map ();
|
|
63
68
|
const register=(table ,source ,name ,path )=>{const key=`${source}#${name}`;const entry=table.get(key)??{source,export:name,routes:[]};entry.routes.push(path);table.set(key,entry);};
|
|
64
|
-
const env=new Set (),secrets=new Set (),proxy=new Set (),signals=new Set ()
|
|
69
|
+
const env=new Set (),secrets=new Set (),proxy=new Set (),signals=new Set ();
|
|
65
70
|
const manifestRoutes =[];
|
|
66
71
|
for(const explanation of explanations){
|
|
67
72
|
const declared=loaded.routes[explanation.path];
|
|
68
73
|
const routeEnv=sorted(Object.values(declared?.env??{}).flatMap(ref=>ref.env?[ref.env]:[])),routeSecrets=sorted(Object.values(declared?.secrets??{}).map(ref=>ref.secret));
|
|
69
74
|
for(const name of routeEnv)env.add(name);for(const name of routeSecrets)secrets.add(name);
|
|
70
75
|
if(declared?.proxy)proxy.add(origin(declared.proxy.url));for(const signal of declared?.signals??[])signals.add(origin(signal.url));
|
|
71
|
-
if(declared?.link)linkStores.add(declared.link.collection);
|
|
72
76
|
if(explanation.handler.kind==='function')register(functions,explanation.handler.source ,explanation.handler.export ,explanation.path);
|
|
73
77
|
for(const item of explanation.middleware)register(middleware,item.source,item.export,explanation.path);
|
|
74
78
|
const extensions ={};
|
|
@@ -78,6 +82,7 @@ export async function buildManifest(project ,options ={})
|
|
|
78
82
|
manifestRoutes.push({
|
|
79
83
|
path:explanation.path,methods:explanation.methods,handler:explanation.handler,enabled:explanation.enabled,
|
|
80
84
|
...(explanation.expires?{expires:explanation.expires}:{}),...(explanation.generated?{generated:explanation.generated}:{}),...(explanation.description?{description:explanation.description}:{}),
|
|
85
|
+
sandbox:explanation.sandbox,...(explanation.sandboxReason?{sandboxReason:explanation.sandboxReason}:{}),
|
|
81
86
|
middleware:explanation.middleware,parameters:explanation.inputs.parameters,...(explanation.inputs.body?{body:explanation.inputs.body}:{}),
|
|
82
87
|
policies:explanation.policies.names,extensions,cache:explanation.cache,bindings:{env:routeEnv,secrets:routeSecrets},egress:explanation.egress,
|
|
83
88
|
capabilities:explanation.capabilities,targets,
|
|
@@ -91,17 +96,16 @@ export async function buildManifest(project ,options ={})
|
|
|
91
96
|
protectedRoutes:sorted(Object.entries(loaded.routes).filter(([,route])=>Object.hasOwn(effectiveExtensionPolicies(loaded.document,route),name)).map(([path])=>path))};
|
|
92
97
|
}
|
|
93
98
|
const targets={} ;
|
|
94
|
-
for(const target of capabilityTargets){const report=analyzeCompiledCapabilities(loaded.document,compiled,target);targets[target]={compatible:report.compatible,issues:report.issues.length};}
|
|
99
|
+
for(const target of capabilityTargets){const report=analyzeCompiledCapabilities(loaded.document,compiled,target,options.extensions);targets[target]={compatible:report.compatible,issues:report.issues.length};}
|
|
95
100
|
const capabilities=new Set ();
|
|
96
101
|
for(const route of routes)for(const capability of routeCapabilities(route,loaded.document))capabilities.add(capability);
|
|
97
|
-
if(loaded.document.dynamicLinks)capabilities.add('dynamicLinks');
|
|
98
102
|
const modules=(table )=>[...table.values()].map(entry=>({...entry,routes:sorted(entry.routes)})).sort((a,b)=>compare(a.source,b.source)||compare(a.export,b.export));
|
|
99
103
|
return {
|
|
100
104
|
schemaVersion:MANIFEST_SCHEMA_VERSION,urlcode:packageVersion,entry:'urlcode.yaml',
|
|
101
105
|
files:loaded.files.map(file=>relative(loaded.root,file).split('\\').join('/')),
|
|
102
106
|
revision:projectSha256,configVersion:loaded.version,routeCount:manifestRoutes.length,routes:manifestRoutes,
|
|
103
107
|
capabilities:[...capabilities].sort(compare),extensions:extensionDeclarations,recipes:await recipeProvenance(loaded.root),
|
|
104
|
-
external:{env:sorted(env),secrets:sorted(secrets),egress:{proxy:sorted(proxy),signals:sorted(signals)},extensions:Object.keys(extensionDeclarations)
|
|
108
|
+
external:{env:sorted(env),secrets:sorted(secrets),egress:{proxy:sorted(proxy),signals:sorted(signals)},extensions:Object.keys(extensionDeclarations)},
|
|
105
109
|
functions:modules(functions),middleware:modules(middleware),targets,
|
|
106
110
|
};
|
|
107
111
|
}
|
package/dist/mcp-authoring.js
CHANGED
|
@@ -17,8 +17,8 @@ import {assert} from './errors.js';
|
|
|
17
17
|
* Authoring tools for `urlcode mcp --allow-authoring`. Every write lands inside
|
|
18
18
|
* the operator-selected project root (after realpath), through the existing
|
|
19
19
|
* authoring, recipe and scaffold paths. Nothing here reads bindings, creates
|
|
20
|
-
* grants, deploys, or touches operator policy, compliance rules
|
|
21
|
-
*
|
|
20
|
+
* grants, deploys, or touches operator policy, compliance rules or host files.
|
|
21
|
+
* The runners spawn the CLI against the same root only.
|
|
22
22
|
*/
|
|
23
23
|
const text={type:'string',maxLength:1024};
|
|
24
24
|
const handler={anyOf:[{type:'string',maxLength:2048},{type:'object'}]};
|
package/dist/mcp.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function serveMcp(options ) {
|
|
|
78
78
|
if(message.method==='initialize') {
|
|
79
79
|
if(initialized){await error(id,-32600,'Already initialized');return;}
|
|
80
80
|
if(typeof params.protocolVersion!=='string'||!object(params.capabilities)||!object(params.clientInfo)||typeof params.clientInfo.name!=='string'||typeof params.clientInfo.version!=='string'){await error(id,-32602,'Invalid initialize params');return;}
|
|
81
|
-
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.0-alpha.
|
|
81
|
+
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.0-alpha.3'}}});return;
|
|
82
82
|
}
|
|
83
83
|
if(message.method==='ping'){await send({jsonrpc:'2.0',id,result:{}});return;}
|
|
84
84
|
if(!ready){await error(id,-32002,'Initialize first');return;}
|
package/dist/observability.js
CHANGED
|
@@ -17,12 +17,8 @@ export const events = Object.freeze({
|
|
|
17
17
|
reload: Object.freeze(['event', 'status', 'version', 'routes']),
|
|
18
18
|
watch: Object.freeze(['event', 'status']),
|
|
19
19
|
function_worker: Object.freeze(['event', 'status', 'slot', 'attempt', 'delayMs']),
|
|
20
|
-
link_store_worker: Object.freeze(['event', 'status', 'readOnly', 'attempt', 'delayMs']),
|
|
21
|
-
link_request: Object.freeze(['event', 'requestId', 'collection', 'route', 'code', 'method', 'status', 'outcome', 'durationMs']),
|
|
22
|
-
link_observer: Object.freeze(['event', 'status', 'reason', 'dropped', 'queued', 'delivered', 'failed', 'timedOut', 'closed']),
|
|
23
20
|
logs_dropped: Object.freeze(['event', 'count']),
|
|
24
21
|
observer: Object.freeze(['event', 'status', 'name']),
|
|
25
|
-
management_request: Object.freeze(['event', 'timestamp', 'requestId', 'collection', 'action', 'authenticated', 'principal', 'status', 'outcome', 'durationMs']),
|
|
26
22
|
throttle: Object.freeze(['event', 'route', 'outcome', 'remaining']),
|
|
27
23
|
agents: Object.freeze(['event', 'route', 'list', 'outcome']),
|
|
28
24
|
cache: Object.freeze(['event', 'route', 'outcome']),
|
|
@@ -47,10 +43,8 @@ export const events = Object.freeze({
|
|
|
47
43
|
|
|
48
44
|
|
|
49
45
|
|
|
50
|
-
|
|
51
46
|
|
|
52
47
|
|
|
53
|
-
|
|
54
48
|
|
|
55
49
|
|
|
56
50
|
|
|
@@ -85,14 +79,13 @@ export function validateObservers(observers = []) {
|
|
|
85
79
|
return observers ; // every entry was just checked
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
export const SNAPSHOT_VERSION =
|
|
82
|
+
export const SNAPSHOT_VERSION = 2;
|
|
89
83
|
const statusClasses = ['2xx', '3xx', '4xx', '5xx'];
|
|
90
84
|
const MAX_ROUTES = 10000;
|
|
91
85
|
const outcomes = {
|
|
92
86
|
throttle: ['allowed', 'exceeded'],
|
|
93
87
|
agents: ['denied', 'reported'],
|
|
94
88
|
cache: ['hit', 'stale', 'miss', 'store'],
|
|
95
|
-
link_request: ['completed', 'aborted', 'missing', 'disabled', 'expired', 'invalid_code', 'invalid_record', 'unavailable'],
|
|
96
89
|
};
|
|
97
90
|
const zeroed = (keys ) => Object.fromEntries(keys.map(key => [key, 0]));
|
|
98
91
|
|
|
@@ -108,11 +101,8 @@ export function createMetrics() {
|
|
|
108
101
|
const reloads = { ok: 0, rejected: 0 };
|
|
109
102
|
const watch = { failed: 0 };
|
|
110
103
|
const functionWorkers = { started: 0, restarts: 0 };
|
|
111
|
-
const linkStoreWorkers = { started: 0, restarts: 0 };
|
|
112
104
|
const policies = { throttle: zeroed(outcomes.throttle), agents: zeroed(outcomes.agents), cache: zeroed(outcomes.cache) };
|
|
113
|
-
const linkRequests = zeroed(outcomes.link_request);
|
|
114
105
|
const signals = zeroed(['accepted','delivered','failed','dropped']);
|
|
115
|
-
const linkObserver = { failed: 0, dropped: 0 };
|
|
116
106
|
let logsDropped = 0, observerErrors = 0;
|
|
117
107
|
const count = (table , key ) => { if (typeof key === 'string' && Object.hasOwn(table, key)) table[key] ++; };
|
|
118
108
|
function countRequest(target , status , route ) {
|
|
@@ -137,10 +127,7 @@ export function createMetrics() {
|
|
|
137
127
|
case 'reload': count(reloads, record.status); break;
|
|
138
128
|
case 'watch': if (record.status === 'failed') watch.failed++; break;
|
|
139
129
|
case 'function_worker': if (record.status === 'restarting') functionWorkers.restarts++; else if (record.status === 'started') functionWorkers.started++; break;
|
|
140
|
-
case 'link_store_worker': if (record.status === 'restarting') linkStoreWorkers.restarts++; else if (record.status === 'started') linkStoreWorkers.started++; break;
|
|
141
130
|
case 'throttle': case 'agents': case 'cache': count(policies[record.event], record.outcome); break;
|
|
142
|
-
case 'link_request': count(linkRequests, record.outcome); break;
|
|
143
|
-
case 'link_observer': if (record.status === 'failed') linkObserver.failed++; else if (record.status === 'dropped' || record.status === 'closed') linkObserver.dropped = Math.max(linkObserver.dropped, Number(record.dropped) || 0); break;
|
|
144
131
|
case 'logs_dropped': logsDropped += Number(record.count) || 0; break;
|
|
145
132
|
case 'observer': if (record.status === 'failed') observerErrors++; break;
|
|
146
133
|
default: break;
|
|
@@ -160,11 +147,8 @@ export function createMetrics() {
|
|
|
160
147
|
reloads: { ...reloads },
|
|
161
148
|
watch: { ...watch },
|
|
162
149
|
functionWorkers: { ...functionWorkers, healthySlots: 0, slots: 0 },
|
|
163
|
-
linkStoreWorkers: { ...linkStoreWorkers },
|
|
164
150
|
policies: { throttle: { ...policies.throttle }, agents: { ...policies.agents }, cache: { ...policies.cache } },
|
|
165
151
|
signals: {...signals},
|
|
166
|
-
linkRequests: { ...linkRequests },
|
|
167
|
-
linkObserver: { ...linkObserver },
|
|
168
152
|
logsDropped,
|
|
169
153
|
observers: { errors: observerErrors },
|
|
170
154
|
...extra,
|
|
@@ -231,14 +215,10 @@ export function renderPrometheus(snapshot ) {
|
|
|
231
215
|
metric('function_worker_restarts_total', 'counter', 'Function worker replacements scheduled.', [[{}, snapshot.functionWorkers?.restarts]]);
|
|
232
216
|
metric('function_worker_healthy_slots', 'gauge', 'Function worker slots ready to serve.', [[{}, snapshot.functionWorkers?.healthySlots]]);
|
|
233
217
|
metric('function_worker_slots', 'gauge', 'Function worker slots configured.', [[{}, snapshot.functionWorkers?.slots]]);
|
|
234
|
-
metric('link_store_worker_restarts_total', 'counter', 'Link-store worker replacements scheduled.', [[{}, snapshot.linkStoreWorkers?.restarts]]);
|
|
235
218
|
metric('throttle_total', 'counter', 'Throttle policy decisions.', byKey(snapshot.policies?.throttle, 'outcome'));
|
|
236
219
|
metric('agents_total', 'counter', 'Agents policy decisions.', byKey(snapshot.policies?.agents, 'outcome'));
|
|
237
220
|
metric('cache_total', 'counter', 'Cache policy outcomes.', byKey(snapshot.policies?.cache, 'outcome'));
|
|
238
|
-
metric('link_requests_total', 'counter', 'Dynamic link requests by outcome.', byKey(snapshot.linkRequests, 'outcome'));
|
|
239
221
|
metric('signals_total','counter','Best-effort webhook outcomes.',Object.entries(snapshot.signals||{}).map(([outcome,value])=>[{outcome},value]));
|
|
240
|
-
metric('link_observer_failures_total', 'counter', 'Link event collector failures.', [[{}, snapshot.linkObserver?.failed]]);
|
|
241
|
-
metric('link_observer_dropped_total', 'counter', 'Link events dropped under overload.', [[{}, snapshot.linkObserver?.dropped]]);
|
|
242
222
|
metric('logs_dropped_total', 'counter', 'Log records the JSON logger shed.', [[{}, snapshot.logsDropped]]);
|
|
243
223
|
metric('observer_errors_total', 'counter', 'Observer hooks that threw or rejected.', [[{}, snapshot.observers?.errors]]);
|
|
244
224
|
metric('uptime_seconds', 'gauge', 'Seconds since the process started serving.', [[{}, snapshot.uptimeSeconds]]);
|
package/dist/policies/cache.js
CHANGED
|
@@ -202,9 +202,14 @@ function revalidate(state , req , result
|
|
|
202
202
|
if (result.status !== 200 || (req.method !== 'GET' && req.method !== 'HEAD')) return result;
|
|
203
203
|
let headers = result.headers, etag = header(headers, 'etag');
|
|
204
204
|
if (!etag) {
|
|
205
|
-
// A strong validator over the representation. A
|
|
206
|
-
//
|
|
207
|
-
//
|
|
205
|
+
// A strong validator over the representation. A trusted function's HEAD
|
|
206
|
+
// answer is now measured the same way its GET answer is (the body is
|
|
207
|
+
// read to determine its real length even though HEAD never puts it on
|
|
208
|
+
// the wire — see #139), so it has real bytes to hash here and gets a
|
|
209
|
+
// validator that agrees with GET's. Only a HEAD answer that genuinely
|
|
210
|
+
// has no body to measure (e.g. one carrying a native reply with no
|
|
211
|
+
// body) skips computing one, rather than hashing zero bytes and
|
|
212
|
+
// asserting a validator for a representation it never measured.
|
|
208
213
|
const body = bodyOf(result);
|
|
209
214
|
if (req.method === 'HEAD' && !body.length) return result;
|
|
210
215
|
etag = '"' + createHash('sha256').update(body).digest('hex') + '"'; headers = [...headers, ['etag', etag]];
|