@jimhoyd/urlcode 0.4.0-alpha.2 → 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 +17 -19
- package/.claude/skills/urlcode-operations/SKILL.md +9 -9
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +20 -16
- package/README.md +44 -54
- package/ROADMAP.md +61 -376
- package/dist/BUILD-MANIFEST.json +15 -15
- package/dist/agents-guide.js +6 -6
- package/dist/build-static.js +2 -0
- package/dist/cli.js +3 -3
- package/dist/explain-cli.js +4 -2
- package/dist/explain.js +7 -1
- package/dist/extensions.js +1 -1
- package/dist/manifest.js +7 -1
- package/dist/mcp-authoring.js +2 -2
- package/dist/mcp.js +1 -1
- package/dist/policies.js +3 -1
- package/dist/prerender.js +4 -0
- package/dist/readiness.js +3 -0
- package/dist/route-diff.js +12 -5
- package/dist/types/explain.d.ts +6 -0
- package/dist/types/extensions.d.ts +1 -1
- package/dist/types/manifest.d.ts +5 -1
- package/dist/types/readiness.d.ts +2 -0
- package/dist/types/types.d.ts +8 -1
- package/dist/types.js +8 -1
- package/dist/typescript-authoring.js +18 -7
- package/docs/AI-AUTHORING.md +10 -6
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/BEST-PRACTICES.md +17 -9
- package/docs/CAPABILITIES.md +1 -1
- 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/CODEBASE-AUDIT-2026-09-20.md +278 -0
- package/docs/DEVELOPMENT-PIPELINE.md +181 -0
- package/docs/EXTENSIONS.md +15 -5
- package/docs/FRAMEWORK.md +22 -18
- package/docs/FUNCTION-SECURITY.md +44 -0
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/OPEN-DECISIONS.md +184 -212
- package/docs/OPERATIONAL-PROOF.md +3 -3
- package/docs/OPERATIONS.md +3 -3
- package/docs/POLICIES.md +13 -5
- package/docs/PRERENDER.md +23 -11
- package/docs/PROJECT-DIRECTION.md +3 -3
- package/docs/READINESS.md +6 -2
- package/docs/README.md +18 -41
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-READINESS.md +6 -6
- package/docs/RELEASE-SECURITY.md +68 -168
- package/docs/RESILIENCE.md +3 -3
- package/docs/ROUTING.md +3 -4
- package/docs/SECURITY-AUDIT.md +2 -2
- package/docs/SPECIFICATION.md +7 -5
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
- package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
- package/docs/SPIKE-CORE-LAYERING.md +48 -17
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
- package/docs/SPIKE-LAMBDA-COMPILE.md +8 -0
- package/docs/STATIC.md +14 -3
- package/docs/TOOLING.md +4 -3
- package/docs/TYPESCRIPT-AUTHORING.md +6 -1
- package/docs/VERSION-ALIGNMENT.md +46 -205
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
- package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
- package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
- 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} +10 -0
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
- package/docs/archive/README.md +27 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/examples/assets/example.yaml +1 -1
- package/examples/cookbook/route-index.json +1 -1
- package/examples/cookbook/routes/middleware.yaml +1 -1
- package/examples/prerender/README.md +14 -6
- 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/llms-full.txt +117 -45
- package/llms.txt +5 -4
- package/package.json +24 -4
- package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
- 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 +1 -1
- package/recipes/contact-form/functions/contact.mjs +2 -1
- package/recipes/contact-form/recipe.yaml +2 -2
- package/recipes/cors-api/README.md +2 -2
- package/recipes/cors-api/recipe.yaml +1 -1
- package/recipes/health-page/README.md +1 -1
- package/recipes/json-api/README.md +1 -1
- package/recipes/json-api/recipe.yaml +3 -3
- package/recipes/middleware/README.md +8 -4
- package/recipes/protected-download/README.md +1 -1
- package/recipes/protected-download/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 +1 -1
- package/recipes/static-plus-api/urlcode.yaml +1 -1
- package/recipes/typescript/recipe.yaml +4 -4
- package/skills/urlcode/SKILL.md +6 -6
- package/starters/default/AGENTS.md +6 -6
- package/docs/SPIKE-MONOREPO.md +0 -322
package/dist/explain.js
CHANGED
|
@@ -29,6 +29,11 @@ const handlerNames=['extension','proxy','conditional','redirect','function','pag
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
|
|
@@ -52,7 +57,7 @@ function handlerOf(route ,root ) {
|
|
|
52
57
|
return {kind,cases:(route.conditionalRoutes?.cases??[]).map(item=>({match:item.match,...branch(item.route)})),...(route.conditionalRoutes?.fallback?{fallback:branch(route.conditionalRoutes.fallback)}:{})};
|
|
53
58
|
}
|
|
54
59
|
case 'redirect':return {kind,url:route.redirect .url,status:route.redirect .status??302,...(route.redirect .query?{query:route.redirect .query}:{})};
|
|
55
|
-
case 'function':return {kind,source:relativeSource(root,route.function .source),export:route.function .export,...(route.function .args?{args:route.function .args}:{})
|
|
60
|
+
case 'function':return {kind,source:relativeSource(root,route.function .source),export:route.function .export,...(route.function .args?{args:route.function .args}:{})};
|
|
56
61
|
case 'page':return {kind,file:route.page .file,...(route.page .contentType?{contentType:route.page .contentType}:{})};
|
|
57
62
|
case 'static':return {kind,directory:route.static .directory,...(route.static .index?{index:route.static .index}:{})};
|
|
58
63
|
case 'download':return {kind,file:route.download .file,...(route.download .filename?{filename:route.download .filename}:{}),...(route.download .contentType?{contentType:route.download .contentType}:{})};
|
|
@@ -110,6 +115,7 @@ export function explainCompiledRoute(loaded ,route ,c
|
|
|
110
115
|
state:routeState(route,options.now??Date.now()),enabled:route.enabled!==false,...(route.expires?{expires:route.expires}:{}),
|
|
111
116
|
methods:[...route.methods],conditional:Boolean(route.match||route.conditional),handler,
|
|
112
117
|
middleware:route.middleware.map(item=>({source:relativeSource(root,item.source),export:item.export})),
|
|
118
|
+
sandbox:route.sandbox===true,...(route.sandboxReason?{sandboxReason:route.sandboxReason}:{}),
|
|
113
119
|
inputs:{parameters:route.parameters.map(({name,in:location,required,schema})=>({name,in:location,required,schema})),...(route.request?.body?{body:route.request.body}:{})},
|
|
114
120
|
policies:{names,inventory,extensions},
|
|
115
121
|
cache:cacheOf(route,chain,extensionNames),
|
package/dist/extensions.js
CHANGED
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
|
|
|
@@ -77,6 +82,7 @@ export async function buildManifest(project ,options ={})
|
|
|
77
82
|
manifestRoutes.push({
|
|
78
83
|
path:explanation.path,methods:explanation.methods,handler:explanation.handler,enabled:explanation.enabled,
|
|
79
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}:{}),
|
|
80
86
|
middleware:explanation.middleware,parameters:explanation.inputs.parameters,...(explanation.inputs.body?{body:explanation.inputs.body}:{}),
|
|
81
87
|
policies:explanation.policies.names,extensions,cache:explanation.cache,bindings:{env:routeEnv,secrets:routeSecrets},egress:explanation.egress,
|
|
82
88
|
capabilities:explanation.capabilities,targets,
|
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/policies.js
CHANGED
|
@@ -9,7 +9,9 @@ import { assert, ConfigError } from './errors.js';
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
// Host-side behavior declared in YAML and enforced
|
|
12
|
+
// Host-side behavior declared in YAML and enforced in the host process, outside
|
|
13
|
+
// function/middleware execution entirely -- trusted routes and sandbox: true
|
|
14
|
+
// routes alike. Every
|
|
13
15
|
// module here follows one contract so a first-party policy and an operator
|
|
14
16
|
// plugin share a code path (PolicyModule in src/types.ts):
|
|
15
17
|
//
|
package/dist/prerender.js
CHANGED
|
@@ -102,6 +102,10 @@ async function planPasses(source ) {
|
|
|
102
102
|
const cost = new Map ();
|
|
103
103
|
for (const pattern of patterns) {
|
|
104
104
|
const route = loaded.routes[pattern] ;
|
|
105
|
+
// Trusted routes use Node resolution, not the sandbox module collector.
|
|
106
|
+
// Applying guest import/size limits here would reject valid build-time Node
|
|
107
|
+
// code before the runtime can dispatch it through its declared trust mode.
|
|
108
|
+
if (route.sandbox !== true) { cost.set(pattern, []); continue; }
|
|
105
109
|
const definitions = [];
|
|
106
110
|
for (const definition of routeFunctions(route))
|
|
107
111
|
definitions.push({function: {source: await functionFile(loaded.root, definition.source), export: definition.export || 'default'}});
|
package/dist/readiness.js
CHANGED
|
@@ -17,6 +17,8 @@ import { runCompliance } from './compliance.js';
|
|
|
17
17
|
/** One configured route as the inventory reports it: a PlanInventoryEntry with the handler kind named. */
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
|
|
@@ -74,6 +76,7 @@ export function projectPlan(compiled )
|
|
|
74
76
|
const now = Date.now();
|
|
75
77
|
const inventory = routes.map(route => { const advisories = routeAdvisories(route); return { path:route.pattern, handler:handlers.find(key => route[key]), methods:route.methods, middleware:route.middleware?.length || 0,
|
|
76
78
|
policies:[...(route.policy ? Object.keys(route.policy.describe) : []),...(route.extensionPolicyNames??[]).map(name=>`extensions.${name}`)],
|
|
79
|
+
sandbox:route.sandbox === true, ...(route.sandboxReason ? { sandboxReason:route.sandboxReason } : {}),
|
|
77
80
|
...(route.generated ? { generated:route.generated } : {}),
|
|
78
81
|
...(advisories.length ? { advisories } : {}),
|
|
79
82
|
state:route.enabled === false ? 'disabled' : route.expiresAt && now >= route.expiresAt ? 'expired' : 'active' }; });
|
package/dist/route-diff.js
CHANGED
|
@@ -17,7 +17,7 @@ const isStrings = (value ) => Array.isArray(value) &&
|
|
|
17
17
|
function entry(value , index ) {
|
|
18
18
|
const fail = (what ) => { throw new ConfigError(`Route report inventory[${index}] ${what}`); };
|
|
19
19
|
if (!isRecord(value)) return fail('is not an object');
|
|
20
|
-
const { path, handler, methods, middleware, policies, generated, state } = value;
|
|
20
|
+
const { path, handler, methods, middleware, policies, generated, state, sandbox, sandboxReason } = value;
|
|
21
21
|
if (typeof path !== 'string' || !path) return fail('needs a path');
|
|
22
22
|
if (handler !== undefined && typeof handler !== 'string') return fail('has an invalid handler');
|
|
23
23
|
if (!isStrings(methods)) return fail('needs methods');
|
|
@@ -25,7 +25,13 @@ function entry(value , index ) {
|
|
|
25
25
|
if (!isStrings(policies)) return fail('needs policies');
|
|
26
26
|
if (generated !== undefined && typeof generated !== 'string') return fail('has an invalid generated marker');
|
|
27
27
|
if (typeof state !== 'string' || !states.includes(state )) return fail('needs a state');
|
|
28
|
-
|
|
28
|
+
// `sandbox`/`sandboxReason` postdate the report format, so an older report
|
|
29
|
+
// omits them: absent is carried through as absent rather than defaulted to
|
|
30
|
+
// `false`, which would read as a trust change that never happened.
|
|
31
|
+
if (sandbox !== undefined && typeof sandbox !== 'boolean') return fail('has an invalid sandbox flag');
|
|
32
|
+
if (sandboxReason !== undefined && typeof sandboxReason !== 'string') return fail('has an invalid sandboxReason');
|
|
33
|
+
return { path, handler, methods, middleware, policies, ...(generated !== undefined ? { generated } : {}), state:state ,
|
|
34
|
+
...(sandbox !== undefined ? { sandbox } : {}), ...(sandboxReason !== undefined ? { sandboxReason } : {}) };
|
|
29
35
|
}
|
|
30
36
|
/** Validates a parsed `urlcode routes` JSON report (a child-process or file boundary) into a snapshot. */
|
|
31
37
|
export function parseRouteSnapshot(value ) {
|
|
@@ -71,7 +77,7 @@ export function diffRoutes(before , after )
|
|
|
71
77
|
}
|
|
72
78
|
export const hasRouteChanges = (diff ) => diff.added.length + diff.removed.length + diff.changed.length > 0;
|
|
73
79
|
|
|
74
|
-
const fields = ['handler','methods','state','middleware','policies','generated','policy'] ;
|
|
80
|
+
const fields = ['handler','methods','state','sandbox','sandboxReason','middleware','policies','generated','policy'] ;
|
|
75
81
|
|
|
76
82
|
const cell = (value ) => {
|
|
77
83
|
const text = value === undefined ? '' : Array.isArray(value) && value.every(item => typeof item === 'string') ? value.join(', ') : typeof value === 'string' ? value : canonical(value);
|
|
@@ -82,12 +88,13 @@ const code = (value ) => `\`${value.replace(/\\/g, '\\\\').replac
|
|
|
82
88
|
function table(headers , rows ) {
|
|
83
89
|
return [`| ${headers.join(' | ')} |`, `|${headers.map(() => '---').join('|')}|`, ...rows.map(row => `| ${row.join(' | ')} |`)];
|
|
84
90
|
}
|
|
85
|
-
const routeRow = (record ) => [code(record.path), cell(record.handler), cell(record.methods), cell(record.state),
|
|
91
|
+
const routeRow = (record ) => [code(record.path), cell(record.handler), cell(record.methods), cell(record.state),
|
|
92
|
+
cell(record.sandbox), cell(record.sandboxReason), String(record.middleware), cell(record.policies), cell(record.generated)];
|
|
86
93
|
/** Renders a diff as Markdown: one table per nonempty section, or "No route changes". */
|
|
87
94
|
export function renderRouteDiff(diff ) {
|
|
88
95
|
if (!hasRouteChanges(diff)) return 'No route changes\n';
|
|
89
96
|
const lines = [];
|
|
90
|
-
const routeHeaders = ['Route','Handler','Methods','State','Middleware','Policies','Generated'];
|
|
97
|
+
const routeHeaders = ['Route','Handler','Methods','State','Sandbox','Sandbox reason','Middleware','Policies','Generated'];
|
|
91
98
|
const section = (title , list ) => {
|
|
92
99
|
if (!list.length) return;
|
|
93
100
|
lines.push(`### ${title} (${list.length})`, '', ...table(routeHeaders, list.map(routeRow)), '');
|
package/dist/types/explain.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface RouteExplanation {
|
|
|
55
55
|
source: string;
|
|
56
56
|
export: string;
|
|
57
57
|
}[];
|
|
58
|
+
/** Execution mode for this route's whole `function`/`middleware` chain: `true` for the
|
|
59
|
+
* QuickJS sandbox, `false` for trusted in-process execution. Route-level, not handler-level:
|
|
60
|
+
* a native handler with `middleware` runs project code too, and its mode has to be reviewable.
|
|
61
|
+
* Reported for every route, including those that run no project code at all. */
|
|
62
|
+
sandbox: boolean;
|
|
63
|
+
sandboxReason?: string;
|
|
58
64
|
inputs: {
|
|
59
65
|
parameters: ExplainedParameter[];
|
|
60
66
|
body?: RequestBodyPolicy;
|
|
@@ -83,7 +83,7 @@ export interface RuntimeExtension {
|
|
|
83
83
|
* extension response's header/size caps, exactly like `authorize`-gating
|
|
84
84
|
* auth/admin extensions. `false` is an explicit, reviewed opt-in a generic,
|
|
85
85
|
* cache-transparent extension (pure request/response middleware with no
|
|
86
|
-
* gating semantics of its own
|
|
86
|
+
* gating semantics of its own) makes to say its
|
|
87
87
|
* `middleware()` hook never depends on withholding the response from
|
|
88
88
|
* shared caches: the wrapped route's own declared cache headers pass
|
|
89
89
|
* through unchanged, exactly as the native `middleware:` array already
|
package/dist/types/manifest.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CapabilityName, CapabilityTarget } from './capabilities.ts';
|
|
2
2
|
import type { InspectOptions } from './tooling.ts';
|
|
3
3
|
import type { RouteExplanation } from './explain.ts';
|
|
4
|
-
export declare const MANIFEST_SCHEMA_VERSION =
|
|
4
|
+
export declare const MANIFEST_SCHEMA_VERSION = 3;
|
|
5
5
|
export interface ManifestRoute {
|
|
6
6
|
path: string;
|
|
7
7
|
methods: string[];
|
|
@@ -10,6 +10,10 @@ export interface ManifestRoute {
|
|
|
10
10
|
expires?: string;
|
|
11
11
|
generated?: string;
|
|
12
12
|
description?: string;
|
|
13
|
+
/** Execution mode for the route's whole `function`/`middleware` chain: `true` for the
|
|
14
|
+
* QuickJS sandbox, `false` for trusted in-process execution. */
|
|
15
|
+
sandbox: boolean;
|
|
16
|
+
sandboxReason?: string;
|
|
13
17
|
middleware: {
|
|
14
18
|
source: string;
|
|
15
19
|
export: string;
|
|
@@ -8,6 +8,8 @@ export type HandlerName = 'extension' | 'proxy' | 'conditional' | 'redirect' | '
|
|
|
8
8
|
/** One configured route as the inventory reports it: a PlanInventoryEntry with the handler kind named. */
|
|
9
9
|
export interface RouteInventory extends PlanInventoryEntry {
|
|
10
10
|
handler: HandlerName | undefined;
|
|
11
|
+
/** Always reported here, so a trust change is visible in `routes` and its diff. */
|
|
12
|
+
sandbox: boolean;
|
|
11
13
|
/** Non-blocking `audit` observations about this route (e.g. a webhook-shaped
|
|
12
14
|
* route with no declared `sandbox`/`sandboxReason`); never affects `ready`. */
|
|
13
15
|
advisories?: string[];
|
package/dist/types/types.d.ts
CHANGED
|
@@ -378,7 +378,7 @@ export type PolicyChain = {
|
|
|
378
378
|
describe: PolicyInventory;
|
|
379
379
|
} & Partial<PolicyStates>;
|
|
380
380
|
export type RouteState = 'active' | 'disabled' | 'expired';
|
|
381
|
-
/** One route in the audit inventory: its handler kind, methods and lifecycle state. */
|
|
381
|
+
/** One route in the audit inventory: its handler kind, methods, execution mode and lifecycle state. */
|
|
382
382
|
export interface PlanInventoryEntry {
|
|
383
383
|
path: string;
|
|
384
384
|
handler: string | undefined;
|
|
@@ -387,6 +387,13 @@ export interface PlanInventoryEntry {
|
|
|
387
387
|
policies: string[];
|
|
388
388
|
generated?: string;
|
|
389
389
|
state: RouteState;
|
|
390
|
+
/** The route's execution mode: `true` when its `function`/`middleware` chain runs in the
|
|
391
|
+
* QuickJS sandbox, `false` when it runs trusted in-process. Route-level, because the mode
|
|
392
|
+
* applies to the whole chain — a native handler with `middleware` has one too. Optional
|
|
393
|
+
* only at the report-parsing boundary: reports written before it existed omit it. */
|
|
394
|
+
sandbox?: boolean;
|
|
395
|
+
/** The route's declared `sandboxReason`, when it has one. */
|
|
396
|
+
sandboxReason?: string;
|
|
390
397
|
}
|
|
391
398
|
export interface TestPlan {
|
|
392
399
|
inventory: PlanInventoryEntry[];
|
package/dist/types.js
CHANGED
|
@@ -199,9 +199,16 @@
|
|
|
199
199
|
// consume it need not import the host module that produces it.
|
|
200
200
|
|
|
201
201
|
|
|
202
|
-
/** One route in the audit inventory: its handler kind, methods and lifecycle state. */
|
|
202
|
+
/** One route in the audit inventory: its handler kind, methods, execution mode and lifecycle state. */
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
205
212
|
|
|
206
213
|
|
|
207
214
|
|
|
@@ -23,16 +23,20 @@ const SANDBOX_MODULE_LIMIT=128,SANDBOX_MODULE_BYTE_LIMIT=1048576,SANDBOX_TOTAL_B
|
|
|
23
23
|
export async function buildTypeScriptProject(project ,output ,{dryRun=false} ={}) {
|
|
24
24
|
const root=await realpath(project),loaded=await loadDocument(root),files=new Map (),modules=new Map ();
|
|
25
25
|
const {default:ts}=await import('typescript');
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// Which trust levels a module has already been visited under, so a graph
|
|
27
|
+
// reachable from both a sandboxed and a trusted route is validated under each
|
|
28
|
+
// route's own rules exactly once instead of being refused outright. The
|
|
29
|
+
// module is still emitted once; `emittedBy` keeps target-name collisions
|
|
30
|
+
// between *different* sources refused.
|
|
31
|
+
const moduleTrust=new Map (),emittedBy=new Map ();
|
|
29
32
|
let sandboxedSourceBytes=0,sandboxedModuleCount=0,assetBytes=0;
|
|
30
33
|
await init;
|
|
31
34
|
async function collect(path ,sandboxed ) {
|
|
32
35
|
authoringPath(path);assert(/\.(?:ts|js|mjs)$/.test(path) && !path.endsWith('.d.ts'),'Guest source must be .ts, .js or .mjs');
|
|
33
|
-
|
|
34
|
-
if(
|
|
35
|
-
|
|
36
|
+
let visited=moduleTrust.get(path);
|
|
37
|
+
if(!visited){visited=new Set();moduleTrust.set(path,visited);}
|
|
38
|
+
if(visited.has(sandboxed))return;
|
|
39
|
+
visited.add(sandboxed);
|
|
36
40
|
let bytes ;
|
|
37
41
|
if(sandboxed){
|
|
38
42
|
assert(sandboxedModuleCount<SANDBOX_MODULE_LIMIT,'Function module limit exceeded');sandboxedModuleCount++;
|
|
@@ -93,7 +97,14 @@ export async function buildTypeScriptProject(project ,output ,{dry
|
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
for(const edit of edits.sort((a,b)=>b.start-a.start))code=code.slice(0,edit.start)+edit.value+code.slice(edit.end);
|
|
96
|
-
|
|
100
|
+
// A module reachable from both trust levels is transpiled twice (once per
|
|
101
|
+
// rule set) but emitted once. The two results agree: the sandbox rules are
|
|
102
|
+
// strictly stricter, so anything the trusted pass would leave alone —
|
|
103
|
+
// bare/dynamic specifiers, `import.meta` — fails the sandboxed pass and
|
|
104
|
+
// aborts the whole build before either emission is published.
|
|
105
|
+
const target=emitted(path),previous=emittedBy.get(target);
|
|
106
|
+
assert(previous===undefined||previous===path,'Guest output module collision');
|
|
107
|
+
emittedBy.set(target,path);files.set(target,code);
|
|
97
108
|
}
|
|
98
109
|
for(const route of Object.values(loaded.routes))for(const definition of routeFunctions(route)){
|
|
99
110
|
await collect(definition.source,!!route.sandbox);definition.source=emitted(definition.source);
|
package/docs/AI-AUTHORING.md
CHANGED
|
@@ -106,7 +106,7 @@ The benchmark operates locally; it is not a load test of an external deployment.
|
|
|
106
106
|
| Parameter validation and JSON body syntax checks | Full OpenAPI or JSON Schema validation of request bodies |
|
|
107
107
|
| Local test/audit/benchmark | Route-local YAML tests, managed monitoring, production load certification |
|
|
108
108
|
| Local/self-hosted runtime; limited AWS/Vercel/Cloudflare implementations with local tests | Verified provider deployments or full cross-provider parity |
|
|
109
|
-
| File authoring and snapshot reload | General guest storage broker; stored short links (
|
|
109
|
+
| File authoring and snapshot reload | General guest storage broker; stored short links (no supported package; the `urlcode-dynamic-link` extension was retired) |
|
|
110
110
|
| Optional host `policies` (`throttle`, `agents`, `security`, `compression`, `cache`) and reusable `profiles` | Plugins named in YAML, shared multi-instance counters, CORS, verified-bot checks |
|
|
111
111
|
| Optional top-level `site` (`robots`, `sitemap`, `favicon`, `securityTxt`, `llms`) generating native routes | Per-route `noindex` field, sitemap index files, `humans.txt`, signed `security.txt` |
|
|
112
112
|
|
|
@@ -240,9 +240,12 @@ Put that justification where tooling can see it, not only in a source
|
|
|
240
240
|
comment: an optional `sandboxReason` string on the route (up to 500
|
|
241
241
|
characters, `schemas/urlcode.schema.json`) records why a route needs
|
|
242
242
|
isolation, or why it is safe to trust, regardless of whether `sandbox` is
|
|
243
|
-
`true` or `false`. `urlcode explain`/`context
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
`true` or `false`. `urlcode explain`/`context`, the manifest and the
|
|
244
|
+
`routes` inventory all surface it next to the route's `sandbox` boolean —
|
|
245
|
+
per route, not per handler, so a native handler that runs `middleware`
|
|
246
|
+
reports its execution mode too, and `routes --compare` shows a flip between
|
|
247
|
+
trusted and sandboxed execution as a changed route. The trust decision has a
|
|
248
|
+
reviewable trail without reading every route's source file:
|
|
246
249
|
|
|
247
250
|
```yaml
|
|
248
251
|
routes:
|
|
@@ -323,8 +326,9 @@ trust (adding `sandbox: true` without saying why, or relying on the trusted
|
|
|
323
326
|
default for code that plainly needed isolation) to work around them.
|
|
324
327
|
|
|
325
328
|
There is no native `link` handler or `dynamicLinks` project flag; both were
|
|
326
|
-
removed.
|
|
327
|
-
|
|
329
|
+
removed. The `urlcode-dynamic-link` extension package that briefly owned them
|
|
330
|
+
has been retired and unpublished, so there is no supported replacement. Report a
|
|
331
|
+
request for live stored links as a gap rather than inventing a `link` field.
|
|
328
332
|
|
|
329
333
|
See [capabilities and normalized route representation](CAPABILITIES.md) for the target catalog,
|
|
330
334
|
programmatic compatibility analysis and provider verification limits.
|
package/docs/ASSETS.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Native file handlers are implemented in 0.1.0-alpha.3. They work in the local
|
|
4
4
|
Node runtime and self-hosted process/container. Provider adapters remain planned.
|
|
5
|
-
They do not run user functions or expose filesystem APIs to
|
|
5
|
+
They do not run user functions or expose filesystem APIs to guest code, trusted
|
|
6
|
+
or sandboxed.
|
|
6
7
|
|
|
7
8
|
```yaml
|
|
8
9
|
version: "1"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Auth backup and restore platform guarantees
|
|
2
|
+
|
|
3
|
+
`createBackup({database,destination,projectRoot})` and
|
|
4
|
+
`restoreBackup({backup,destination,projectRoot})` use SQLite's online backup API
|
|
5
|
+
in a bounded worker. They include committed WAL pages, validate integrity and
|
|
6
|
+
foreign keys, and publish to a new path without overwriting an existing file.
|
|
7
|
+
Both source and destination must remain outside the served project. See the
|
|
8
|
+
[auth operator commands](../packages/auth/README.md#operations-and-recovery).
|
|
9
|
+
|
|
10
|
+
The completed snapshot is flushed through a writable file handle before it is
|
|
11
|
+
linked into its destination. Windows requires write access for this flush;
|
|
12
|
+
opening the snapshot read-only fails with `EPERM`. File flush errors fail the
|
|
13
|
+
operation rather than being ignored.
|
|
14
|
+
|
|
15
|
+
On POSIX systems the containing directory is also flushed after publication.
|
|
16
|
+
Node's filesystem API does not provide the equivalent directory-handle flush
|
|
17
|
+
used here on Windows, so Windows does not receive that extra directory-entry
|
|
18
|
+
crash-durability guarantee. A successful Windows backup verifies and flushes
|
|
19
|
+
file contents; it is not proof that the new filename survives abrupt power
|
|
20
|
+
loss. Verify the backup exists and perform an isolated restore rehearsal before
|
|
21
|
+
depending on it. CI does not simulate power loss.
|
|
22
|
+
|
|
23
|
+
POSIX directory/file modes are checked where supported. Windows operators must
|
|
24
|
+
restrict the operator data and backup directories with filesystem ACLs; POSIX
|
|
25
|
+
mode bits cannot establish Windows privacy. Keep encryption and CSRF keys and
|
|
26
|
+
reviewed configuration separately backed up. Restore to an isolated new path,
|
|
27
|
+
and review restored sessions and revocation state before reopening traffic.
|
|
28
|
+
|
|
29
|
+
A failed auth service initialization waits for its SQLite worker to terminate
|
|
30
|
+
before rejecting. After a configuration rejection, callers can retry or clean up
|
|
31
|
+
the database without racing that failed opener's file handle. Configuration
|
|
32
|
+
identity checks and their error codes are unchanged.
|
package/docs/BEST-PRACTICES.md
CHANGED
|
@@ -165,8 +165,10 @@ accepting any user-controlled URL. Keep modules free of top-level work other tha
|
|
|
165
165
|
simple definitions: initialization runs during validation and fresh invocations.
|
|
166
166
|
|
|
167
167
|
Prefer pure helpers with explicit inputs and outputs. Module globals are not a
|
|
168
|
-
cache, database, session store or rate limiter: guest state resets per request
|
|
169
|
-
|
|
168
|
+
cache, database, session store or rate limiter: guest state resets per request
|
|
169
|
+
regardless of trust mode. If a route declares `sandbox: true`, review
|
|
170
|
+
[sandbox constraints](FUNCTION-SECURITY.md) before choosing dependencies —
|
|
171
|
+
trusted (default) routes have ordinary Node module access instead.
|
|
170
172
|
|
|
171
173
|
## Middleware should have one clear responsibility
|
|
172
174
|
|
|
@@ -182,8 +184,10 @@ export default async function responseHeaders(request, context, next) {
|
|
|
182
184
|
|
|
183
185
|
Use middleware for reusable behavior around a handler, not to conceal the entire
|
|
184
186
|
application flow. Prefer YAML headers for fixed route-specific headers; this
|
|
185
|
-
example demonstrates a shared wrapper, but native YAML avoids
|
|
186
|
-
when no custom code is needed
|
|
187
|
+
example demonstrates a shared wrapper, but native YAML avoids any
|
|
188
|
+
function/middleware invocation overhead when no custom code is needed —
|
|
189
|
+
including the extra cost of `sandbox: true` where that is declared. Keep
|
|
190
|
+
middleware order explicit in each route.
|
|
187
191
|
|
|
188
192
|
Always return a Response. Call `await next()` once when continuing, or return
|
|
189
193
|
an early Response when intentionally stopping. Do not launch unawaited work or
|
|
@@ -217,8 +221,9 @@ For the two-route feature layout above:
|
|
|
217
221
|
```
|
|
218
222
|
|
|
219
223
|
Additional ordinary JavaScript unit tests for pure helpers are your project's
|
|
220
|
-
choice. Unit tests alone do not verify
|
|
221
|
-
|
|
224
|
+
choice. Unit tests alone do not verify runtime compatibility — including the
|
|
225
|
+
guest API restrictions of a route declaring `sandbox: true` — or HTTP framing:
|
|
226
|
+
always exercise HTTP behavior through URLCode too. Keep large fixture generation explicit and
|
|
222
227
|
deterministic if you add your own tooling; nested test directories and JSON
|
|
223
228
|
fragments are not automatically discovered or merged by URLCode.
|
|
224
229
|
|
|
@@ -238,9 +243,12 @@ Update expected route counts deliberately when adding or removing a route.
|
|
|
238
243
|
YAML anchors, shell expansion or generated credentials for convenience.
|
|
239
244
|
- Deployment limits, TLS, DNS, DDoS filters and worker tuning belong to operations,
|
|
240
245
|
not invented route fields. Document them separately from portable behavior.
|
|
241
|
-
- Live short-code records need durable storage core does not have
|
|
242
|
-
|
|
243
|
-
published.
|
|
246
|
+
- Live short-code records need durable storage core does not have. The
|
|
247
|
+
`urlcode-dynamic-link` extension provided it and has been retired; its
|
|
248
|
+
published `0.1.0-alpha.1` pins core `0.4.0-alpha.1` exactly and so cannot be
|
|
249
|
+
installed beside `0.4.0-alpha.2`. Treat stored short links as unsupported
|
|
250
|
+
until that work lands somewhere else. General session/application storage
|
|
251
|
+
remains future work.
|
|
244
252
|
|
|
245
253
|
## Refactor without changing the public contract
|
|
246
254
|
|
package/docs/CAPABILITIES.md
CHANGED
|
@@ -166,7 +166,7 @@ proxy headers and signal definitions. Resolved egress headers are private runtim
|
|
|
166
166
|
state and must never be serialized. Capability analysis itself adds no authority
|
|
167
167
|
and does not run in the request path.
|
|
168
168
|
|
|
169
|
-
See the [repository review and incremental plan](NEXT-PHASE-PLAN.md) for the
|
|
169
|
+
See the [repository review and incremental plan](archive/2026-09-19/NEXT-PHASE-PLAN.md) for the
|
|
170
170
|
implementation status. Provider deployment tests, independent security review and
|
|
171
171
|
operational soak/recovery proof are separate work.
|
|
172
172
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CI and release follow-up, September 19, 2026
|
|
2
|
+
|
|
3
|
+
This is the evidence and disposition for [#185](https://github.com/jimhoyd-com/urlcode/issues/185),
|
|
4
|
+
after the Windows fixes (#190) and matrix change (#192). It does not authorize
|
|
5
|
+
publication or change required checks.
|
|
6
|
+
|
|
7
|
+
## Reproducible measurements
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run ci:history -- 100 2026-09-19 > /tmp/ci-history.json
|
|
11
|
+
npm run ci:report -- 35482828280
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The history helper reads GitHub through `gh`, with four concurrent requests at
|
|
15
|
+
most. It does not dispatch new runs. Increase the limit (maximum 1,000) as more
|
|
16
|
+
history accumulates. Use a date cutoff after the rollout when comparing the
|
|
17
|
+
same workflow generation. It groups by event, inferred lane and exact matrix,
|
|
18
|
+
reports counts and nearest-rank p50/p95, and flags groups with fewer than 20
|
|
19
|
+
successful first attempts. Failures/cancellations remain visible but are excluded
|
|
20
|
+
from successful-run percentiles. Reruns are excluded because run creation time
|
|
21
|
+
includes the human delay before retrying. Earlier attempts are not a complete
|
|
22
|
+
usage ledger. Raw job/step durations and run URLs remain in the output.
|
|
23
|
+
|
|
24
|
+
Workflow elapsed time ends at the last active job's completion. Runner-minutes
|
|
25
|
+
sum overlapping job execution durations: they are neither wall time nor billed
|
|
26
|
+
minutes. Job-creation-to-start is an observed scheduling interval, not a guarantee
|
|
27
|
+
of pure runner queue time. Workflow-creation-to-start also includes dependencies.
|
|
28
|
+
Missing timestamps remain missing; they are never treated as zero.
|
|
29
|
+
|
|
30
|
+
The initial 100-run sample spans September 19 04:59 UTC through September 20
|
|
31
|
+
02:00 UTC. Successful first-attempt results:
|
|
32
|
+
|
|
33
|
+
| Configuration | Samples | Wall p50 / p95 | Runner-minute p50 / p95 |
|
|
34
|
+
| --- | ---: | --- | --- |
|
|
35
|
+
| Historical combined main, 9 jobs | 20 | 8m27s / 9m04s | 49.82 / 53.62 |
|
|
36
|
+
| Historical combined PR, 3 jobs | 40 | 6m12s / 8m52s | 17.28 / 24.57 |
|
|
37
|
+
| Split full main, 18 jobs | 1 | 7m36s / 7m36s | 74.30 / 74.30 |
|
|
38
|
+
| Split compact main, 10 jobs | 1 | 6m48s / 6m48s | 43.20 / 43.20 |
|
|
39
|
+
| Split compact PR, 10 jobs | 2 | 6m38s / 7m15s | 40.23 / 43.58 |
|
|
40
|
+
|
|
41
|
+
Historical groups contain changing code and workflow revisions; they are context,
|
|
42
|
+
not a controlled experiment. The sample includes no classified docs-only runs.
|
|
43
|
+
It cannot satisfy the 20-run acceptance criterion for the new lanes. Do not
|
|
44
|
+
manufacture 20 redundant workflow runs to fill the sample.
|
|
45
|
+
|
|
46
|
+
The [compact main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482828280)
|
|
47
|
+
used 31.1 fewer runner-minutes than the preceding
|
|
48
|
+
[full matrix main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482515877),
|
|
49
|
+
an observed 41.9% reduction between two runs, not an established long-term rate.
|
|
50
|
+
Its Windows core job took 377 seconds: 258 in `npm test`, 58 in package smoke,
|
|
51
|
+
13 in installation. Linux Node 22 spent 300 of its 355 seconds in `npm test`.
|
|
52
|
+
The next useful optimization target is the runtime suite, not removing static,
|
|
53
|
+
container, audit or provenance checks. CLI subcommands are separate steps in the
|
|
54
|
+
raw report. File-level profiling is still needed before choosing balanced shards;
|
|
55
|
+
extra shards would add setup and runner pressure.
|
|
56
|
+
|
|
57
|
+
## Changes delivered in this follow-up
|
|
58
|
+
|
|
59
|
+
- A Windows validation failure exposed rejected auth initialization returning
|
|
60
|
+
before its worker terminated. The caller now awaits termination before receiving
|
|
61
|
+
the original error; a deterministic regression test proves the ordering.
|
|
62
|
+
- `ci:history` makes the remaining baseline measurable without new CI jobs.
|
|
63
|
+
- Auth/admin release instructions now use scoped monorepo tags and the correct
|
|
64
|
+
trusted-publisher workflow filenames. Package agent guides file issues here
|
|
65
|
+
and point documentation at root `docs/`.
|
|
66
|
+
- Optional evals checks its credential before checkout/setup/install, and retains
|
|
67
|
+
artifacts for 14 days. This avoids unused setup; it does not remove the weekly
|
|
68
|
+
quality check or claim measured savings from a workflow with no prior runs.
|
|
69
|
+
- The manual signed candidate builds all four tarballs and installs them together
|
|
70
|
+
in a temporary consumer outside the workspace. It verifies the peer dependency
|
|
71
|
+
tree, installed versions, public imports, and `init --with auth,admin,ui`.
|
|
72
|
+
`train.json` records package SHA-512 integrity and the source commit; the
|
|
73
|
+
candidate manifest/checksums and provenance include the extension archives.
|
|
74
|
+
Failure stops the candidate before attestation/upload. Nothing is published.
|
|
75
|
+
|
|
76
|
+
The candidate validates the proposed package set, not live-provider behavior or
|
|
77
|
+
registry OIDC. Per-package release workflows still prepare their own archives
|
|
78
|
+
and retain their original bytes for retries; candidate archives are not silently
|
|
79
|
+
substituted for published release bytes. Before a release, dispatch the candidate
|
|
80
|
+
at the selected main commit, then the full verification workflow, and inspect
|
|
81
|
+
both results before using the authorized coordinator.
|
|
82
|
+
|
|
83
|
+
## Remaining decisions and external validation
|
|
84
|
+
|
|
85
|
+
| Item | Disposition |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| Windows packaging/process coverage | Delivered in #190/#192; Node 24 on relevant PRs/main, all supported versions nightly/manually. |
|
|
88
|
+
| 20-run baseline per new lane | Wait for organic runs, then rerun the helper with a rollout cutoff. |
|
|
89
|
+
| Dependency-aware package selection | Keep conservative coverage for now. UI feeds auth/admin; auth feeds admin; core extension/CLI/scaffold changes affect the full composition. A package map must include integration, generated styles, peers and tooling, not just changed directory names. |
|
|
90
|
+
| Changeset/no-release enforcement | Still open. Define explicit core release intent as well as workspace Changesets; require reviewed reasons for no-release cases before implementing a gate. Blanket source-path rules would misclassify tests/tooling and root core is not versioned by Changesets. |
|
|
91
|
+
| Move core into `packages/core` | Separate migration, not a prerequisite for fast CI. Root-relative build, package files, CLI, Docker and starter paths make this higher risk than keeping the explicit root inventory. |
|
|
92
|
+
| OIDC and retained-artifact retry | Needs the next explicitly authorized release. No synthetic run proves npm's trust configuration or partial publication recovery. |
|
|
93
|
+
| Historical GHCR image labels/digests | Blocked on read access: anonymous registry lookup returned 403; organization package API explicitly requires `read:packages`. No labels/digests were verified and no images or publication settings changed. The repository has no `PUBLISH_CONTAINER` variable; inherited organization variables could not be inspected with this credential. |
|
|
94
|
+
| Dependabot grouping / unchanged nightly reuse | No change yet. Measure update PR fanout before grouping; unchanged source can still acquire new advisory findings, so reusing old verification indiscriminately would hide them. |
|
|
95
|
+
|
|
96
|
+
Keep #185 open for these acceptance items. The remaining items are not evidence
|
|
97
|
+
that a publish or recovery rehearsal has already succeeded.
|