@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +59 -64
- package/ROADMAP.md +66 -376
- package/dist/BUILD-MANIFEST.json +26 -25
- package/dist/agents-guide.js +6 -6
- package/dist/authoring.js +15 -1
- package/dist/build-static.js +2 -0
- package/dist/capability-query.js +0 -1
- package/dist/catalog.js +0 -1
- package/dist/cli.js +25 -9
- package/dist/config.js +1 -1
- package/dist/explain-cli.js +4 -2
- package/dist/explain.js +8 -2
- package/dist/extensions.js +1 -1
- package/dist/http-response.js +1 -1
- package/dist/index.js +1 -0
- package/dist/init-with.js +36 -11
- package/dist/manifest.js +8 -2
- package/dist/mcp-authoring.js +4 -4
- package/dist/mcp.js +1 -1
- package/dist/policies/cache.js +2 -2
- package/dist/policies.js +3 -1
- package/dist/prerender.js +4 -0
- package/dist/project-dependencies.js +305 -0
- package/dist/readiness.js +3 -0
- package/dist/route-diff.js +12 -5
- package/dist/runtime.js +1 -1
- package/dist/trusted-functions.js +4 -5
- package/dist/types/authoring.d.ts +9 -1
- package/dist/types/capability-query.d.ts +0 -1
- package/dist/types/catalog.d.ts +0 -4
- package/dist/types/config.d.ts +1 -9
- package/dist/types/explain.d.ts +6 -1
- package/dist/types/extensions.d.ts +1 -1
- package/dist/types/http-response.d.ts +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/init-with.d.ts +7 -13
- package/dist/types/manifest.d.ts +5 -2
- package/dist/types/project-dependencies.d.ts +78 -0
- package/dist/types/readiness.d.ts +2 -0
- package/dist/types/trusted-functions.d.ts +1 -4
- 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 +15 -7
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/AWS.md +9 -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 +284 -0
- package/docs/COMPOSING-A-SITE.md +278 -0
- package/docs/DEVELOPMENT-PIPELINE.md +270 -0
- package/docs/EXTENSIONS.md +51 -11
- package/docs/FRAMEWORK.md +67 -48
- package/docs/FUNCTION-SECURITY.md +44 -0
- package/docs/INSTALL.md +13 -8
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/MIDDLEWARE.md +10 -4
- package/docs/OPEN-DECISIONS.md +224 -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 +10 -5
- package/docs/README.md +20 -44
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-0.4.1.md +73 -0
- package/docs/RELEASE-READINESS.md +6 -6
- package/docs/RELEASE-SECURITY.md +96 -181
- package/docs/RESILIENCE.md +3 -3
- package/docs/ROUTING.md +3 -4
- package/docs/SECURITY-AUDIT.md +2 -2
- package/docs/SPECIFICATION.md +12 -6
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
- package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
- package/docs/SPIKE-CORE-LAYERING.md +49 -18
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
- package/docs/STARTERS.md +17 -5
- package/docs/STATIC.md +14 -3
- package/docs/TOOLING.md +10 -7
- package/docs/TYPESCRIPT-AUTHORING.md +6 -1
- package/docs/VERCEL.md +10 -2
- package/docs/VERSION-ALIGNMENT.md +76 -201
- 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 +387 -0
- package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
- package/docs/archive/README.md +28 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/docs/yaml/functions.md +10 -2
- package/docs/yaml/middleware.md +5 -3
- package/examples/assets/example.yaml +1 -1
- package/examples/cookbook/middleware/envelope.mjs +4 -2
- 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 +503 -88
- package/llms.txt +6 -4
- package/package.json +27 -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/middleware/middleware/envelope.mjs +4 -2
- 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-LAMBDA-COMPILE.md +0 -201
- package/docs/SPIKE-MONOREPO.md +0 -322
package/dist/capability-query.js
CHANGED
|
@@ -44,7 +44,6 @@ function usage(base ,files ,name )
|
|
|
44
44
|
}
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
47
|
-
export function capabilityNameList() {return capabilityNames;}
|
|
48
47
|
/** One catalog entry with its schema fragments and bundled usage. No project, credentials or network are read. */
|
|
49
48
|
export function getCapability(name ) {
|
|
50
49
|
if(typeof name!=='string'||!(capabilityNames ).includes(name))throw new ConfigError('Unknown capability; valid names: '+capabilityNames.join(', '));
|
package/dist/catalog.js
CHANGED
|
@@ -36,7 +36,6 @@ async function validate() {
|
|
|
36
36
|
}
|
|
37
37
|
return validator;
|
|
38
38
|
}
|
|
39
|
-
export const metadataFiles={recipe:'recipe.yaml',example:'example.yaml'} ;
|
|
40
39
|
/** Reads and schema-validates one metadata file; the id must equal the directory name, and file lists stay authoring-safe paths. */
|
|
41
40
|
export async function readMetadata(root ,id ,file ) {
|
|
42
41
|
const text=(await readAuthoringFile(root,file,65536)).toString('utf8');
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { startServer } from './server.js';
|
|
|
14
14
|
import {scaffoldProject} from './scaffold.js';
|
|
15
15
|
import { initProject, addRedirect } from './authoring.js';
|
|
16
16
|
import { initProjectWith, parseWithNames } from './init-with.js';
|
|
17
|
+
import { collectDependencySet, installSteps, parsePin } from './project-dependencies.js';
|
|
17
18
|
import { runProjectTests } from './project-tests.js';
|
|
18
19
|
import { verifyDeployment, failLevels } from './verify-deployment.js';
|
|
19
20
|
|
|
@@ -27,8 +28,11 @@ import { loadComplianceRules, profileNames as complianceProfiles } from './compl
|
|
|
27
28
|
import { parseRouteSnapshot, diffRoutes, renderRouteDiff } from './route-diff.js';
|
|
28
29
|
import { readFile } from 'node:fs/promises';
|
|
29
30
|
|
|
30
|
-
const usage = `URLCode 0.4.
|
|
31
|
-
urlcode init <directory> [--with auth,admin]
|
|
31
|
+
const usage = `URLCode 0.4.1 — local/self-hosted runtime
|
|
32
|
+
urlcode init <directory> [--with ui,auth,admin] [--manifest|--no-manifest] [--pin @scope/pkg=specifier]
|
|
33
|
+
# --with: layered site from installed @jimhoyd/urlcode-<name> packages, with a package.json pinning them exactly; name ui first
|
|
34
|
+
# --manifest: also pin the runtime for a route-only project; --no-manifest: --with without a package.json
|
|
35
|
+
# --pin: record a local path or tarball instead of the registry version; repeatable. No install is ever run for you.
|
|
32
36
|
urlcode scaffold [--project directory] [--dry-run]
|
|
33
37
|
urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
|
|
34
38
|
urlcode dev [--project directory] [--port 3000] [--host 127.0.0.1]
|
|
@@ -74,12 +78,13 @@ const usage = `URLCode 0.4.0-alpha.2 — local/self-hosted runtime
|
|
|
74
78
|
# compact facts for an authoring agent from the compiled project; --stats compares estimated tokens with the docs
|
|
75
79
|
urlcode doctor
|
|
76
80
|
serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp: --host-file /absolute/operator/host.mjs (trusted code outside project)
|
|
77
|
-
Dev loads .env.local and watches; serve does neither. Functions run in WASM isolation
|
|
81
|
+
Dev loads .env.local and watches; serve does neither. Functions run trusted and in-process by default; a route declaring sandbox: true runs in WASM isolation. External bindings require --policy outside the project.
|
|
78
82
|
`;
|
|
79
83
|
const print = (value ) => process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value) + '\n');
|
|
80
84
|
const options = {
|
|
81
85
|
json:{ type:'boolean' }, yaml:{ type:'boolean' }, report:{type:'string'}, 'accept-provider-differences':{type:'boolean'},
|
|
82
86
|
project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
|
|
87
|
+
manifest:{type:'boolean'}, 'no-manifest':{type:'boolean'}, pin:{type:'string', multiple:true},
|
|
83
88
|
port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
|
|
84
89
|
'expect-routes':{type:'string'}, requests:{type:'string'}, concurrency:{type:'string'}, seconds:{type:'string'}, 'max-p95-ms':{type:'string'}, warmup:{type:'string'}, target:{type:'string'},
|
|
85
90
|
workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
|
|
@@ -149,6 +154,8 @@ try {
|
|
|
149
154
|
if (command !== 'mcp' && command !== 'context') operatorHost = await loadOperatorHost(values['host-file'], values.project);
|
|
150
155
|
}
|
|
151
156
|
if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
|
|
157
|
+
if ((values.manifest || values['no-manifest'] || values.pin !== undefined) && command !== 'init') throw new ConfigError('--manifest/--no-manifest/--pin are only supported by init');
|
|
158
|
+
if (values.manifest && values['no-manifest']) throw new ConfigError('Use either --manifest or --no-manifest');
|
|
152
159
|
if (values['allow-authoring'] && command !== 'mcp') throw new ConfigError('--allow-authoring is only supported by mcp');
|
|
153
160
|
const hostOptions = { extensions: operatorHost.extensions, plugins: operatorHost.plugins };
|
|
154
161
|
if ((!['import','recipes','recipe','examples','example','bulk-import'].includes(command) && extra.length) || (!['init','add','import','recipes','recipe','examples','example','bulk-import','explain','capabilities','schema'].includes(command) && arg)) throw new ConfigError('Unexpected positional arguments');
|
|
@@ -266,14 +273,23 @@ try {
|
|
|
266
273
|
const loaded = await loadDocument(values.project);
|
|
267
274
|
print(requestedPermissions(loaded,await prepareFunctionSnapshot(loaded))); break;
|
|
268
275
|
}
|
|
269
|
-
case 'init':
|
|
276
|
+
case 'init': {
|
|
270
277
|
if (!arg) throw new ConfigError('Provide a new project directory');
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
// Pins are opt-in for a route-only project (its runtime may be managed elsewhere) and the default for
|
|
279
|
+
// --with, which has just resolved the very packages the generated site depends on.
|
|
280
|
+
const wanted = values.with === undefined ? values.manifest === true : !values['no-manifest'];
|
|
281
|
+
const pins = new Map((values.pin ?? []).map(parsePin));
|
|
282
|
+
if (pins.size && !wanted) throw new ConfigError('--pin needs a manifest; drop --no-manifest or add --manifest');
|
|
283
|
+
if (values.with === undefined) {
|
|
284
|
+
const set = wanted ? await collectDependencySet([], [], { overrides: pins }) : undefined;
|
|
285
|
+
const created = await initProject(arg, { manifest: set });
|
|
286
|
+
print(set ? { event:'created', dependencies:set.pins, nextSteps:installSteps(created, set) } : { event:'created' });
|
|
287
|
+
break;
|
|
275
288
|
}
|
|
289
|
+
const created = await initProjectWith(arg, parseWithNames(values.with), { manifest: wanted, pins });
|
|
290
|
+
print({ event:'created', ...created, review:`Review ${created.project}/urlcode.yaml and pin its revision explicitly (for example PROJECT_SHA256=${created.projectSha256}); re-review after any project change` });
|
|
276
291
|
break;
|
|
292
|
+
}
|
|
277
293
|
case 'validate': {
|
|
278
294
|
const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions, origin:values.origin });
|
|
279
295
|
print({ event:'valid', routes:runtime.count, version:runtime.version }); await runtime.close(); break;
|
|
@@ -286,7 +302,7 @@ try {
|
|
|
286
302
|
print(result); if (result.failed) process.exitCode = 1; break;
|
|
287
303
|
}
|
|
288
304
|
case 'doctor':
|
|
289
|
-
print({ node:process.version, platform:process.platform, architecture:process.arch, runtime:'node-process',
|
|
305
|
+
print({ node:process.version, platform:process.platform, architecture:process.arch, runtime:'node-process', functionDefault:'trusted-in-process', sandboxEngine:'quickjs-wasm', trustedFilesystem:true, trustedNetwork:true, sandboxedFilesystem:false, sandboxedNetwork:false, hostEgress:'revision-pinned-origin-grants', tooling:['recipes','examples','bulk-import','build-typescript','mcp','verify-provider'], providers:[], capabilityTargets:getCapabilities().targets, policies:Object.keys(policyRegistry), license:'Apache-2.0' }); break;
|
|
290
306
|
case 'dev': case 'serve': {
|
|
291
307
|
const port = Number(values.port);
|
|
292
308
|
if (!/^\d+$/.test(values.port) || !Number.isInteger(port) || port < 0 || port > 65535) throw new ConfigError('Invalid port');
|
package/dist/config.js
CHANGED
|
@@ -74,7 +74,7 @@ function modulePath(pattern , kind , file
|
|
|
74
74
|
* a route-level `cache` becomes `policies.cache` (refused alongside a direct `policies.cache`).
|
|
75
75
|
* Everything downstream (routes, audit, the compiled table) sees only the long form.
|
|
76
76
|
*/
|
|
77
|
-
|
|
77
|
+
function normalizeRoute(pattern , route ) {
|
|
78
78
|
const authored = route ;
|
|
79
79
|
const needsFunction = typeof authored.function === 'string';
|
|
80
80
|
const needsMiddleware = authored.middleware?.some(entry => typeof entry === 'string') ?? false;
|
package/dist/explain-cli.js
CHANGED
|
@@ -12,7 +12,7 @@ const targetsOf=(target ) =>target===undefined
|
|
|
12
12
|
function summary(explanation ,targets ) {
|
|
13
13
|
const handler=explanation.handler,detail=handler.kind==='function'?`${handler.source }#${handler.export }`:handler.kind==='redirect'?`${handler.status } ${handler.url }`:handler.kind==='extension'?handler.name :handler.kind==='page'||handler.kind==='download'?handler.file :handler.kind==='static'?handler.directory :handler.kind==='proxy'?handler.url :handler.kind==='respond'?String(handler.status):'';
|
|
14
14
|
const support=targets.map(target=>explanation.targets[target].compatible?target:`${target}:refused`).join(',');
|
|
15
|
-
return [explanation.path,explanation.methods.join(','),`${handler.kind}${detail?` ${detail}`:''}`,explanation.state,String(explanation.middleware.length),explanation.policies.names.join(',')||'-',explanation.cache.outcome,support].join('\t');
|
|
15
|
+
return [explanation.path,explanation.methods.join(','),`${handler.kind}${detail?` ${detail}`:''}`,explanation.state,explanation.sandbox?'sandboxed':'trusted',String(explanation.middleware.length),explanation.policies.names.join(',')||'-',explanation.cache.outcome,support].join('\t');
|
|
16
16
|
}
|
|
17
17
|
function detail(explanation ,targets ) {
|
|
18
18
|
const lines =[`route: ${explanation.path}`];
|
|
@@ -20,6 +20,8 @@ function detail(explanation ,targets )
|
|
|
20
20
|
if(explanation.generated)lines.push(`generated: site.${explanation.generated}`);
|
|
21
21
|
lines.push(`state: ${explanation.state}${explanation.expires?` (expires ${explanation.expires})`:''}`,`methods: ${explanation.methods.join(', ')}`,`handler: ${JSON.stringify(explanation.handler)}`);
|
|
22
22
|
lines.push(`middleware: ${explanation.middleware.length?explanation.middleware.map(item=>`${item.source}#${item.export}`).join(' -> '):'none'}`);
|
|
23
|
+
// Route-level, so it is printed for a native handler with middleware too.
|
|
24
|
+
lines.push(`execution: ${explanation.sandbox?'sandboxed (QuickJS)':'trusted (in-process)'}${explanation.sandboxReason?`; ${explanation.sandboxReason}`:''}`);
|
|
23
25
|
lines.push(`inputs: ${explanation.inputs.parameters.length?explanation.inputs.parameters.map(p=>`${p.in}:${p.name}${p.required?'':'?'}`).join(', '):'none'}${explanation.inputs.body?`; body ${JSON.stringify(explanation.inputs.body)}`:''}`);
|
|
24
26
|
lines.push(`policies: ${explanation.policies.names.length?explanation.policies.names.join(', '):'none'}`);
|
|
25
27
|
for(const [name,entry] of Object.entries(explanation.policies.inventory))lines.push(` ${name}: ${JSON.stringify(entry)}`);
|
|
@@ -52,7 +54,7 @@ export async function runExplainCommand(command ,route
|
|
|
52
54
|
if(route===undefined){
|
|
53
55
|
const report=await explainProject(options.project,base);
|
|
54
56
|
if(options.json){print(report);return 0;}
|
|
55
|
-
print(table([['route','methods','handler','state','mw','policies','cache','targets'],...report.routes.map(item=>summary(item,targets).split('\t'))]));return 0;
|
|
57
|
+
print(table([['route','methods','handler','state','execution','mw','policies','cache','targets'],...report.routes.map(item=>summary(item,targets).split('\t'))]));return 0;
|
|
56
58
|
}
|
|
57
59
|
if(!route.startsWith('/'))throw new ConfigError('Provide an absolute route path such as /docs');
|
|
58
60
|
const explanation=await explainRoute(options.project,route,base);
|
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}:{})};
|
|
@@ -89,7 +94,7 @@ function targetsOf(loaded ,route ,options
|
|
|
89
94
|
}
|
|
90
95
|
return result;
|
|
91
96
|
}
|
|
92
|
-
|
|
97
|
+
function routeState(route ,now ) {return route.enabled===false?'disabled':route.expiresAt&&now>=route.expiresAt?'expired':'active';}
|
|
93
98
|
/** Describe one compiled route. `chain` is the policy chain compiled for it, when the project declares policies. */
|
|
94
99
|
export function explainCompiledRoute(loaded ,route ,chain ,options ={}) {
|
|
95
100
|
const root=loaded.root,declared=loaded.routes[route.pattern];
|
|
@@ -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/http-response.js
CHANGED
|
@@ -16,7 +16,7 @@ import { HttpError } from './errors.js';
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
// Hop-by-hop and runtime-owned headers a handler must never set on the wire.
|
|
19
|
-
|
|
19
|
+
const forbiddenHeaders = new Set(['connection','keep-alive','transfer-encoding','content-length','upgrade','trailer','proxy-authenticate','proxy-authorization','te']);
|
|
20
20
|
|
|
21
21
|
// One place decides what a URLCode response *is*, independent of how a host
|
|
22
22
|
// delivers it. A Node server writes it to a socket; a Lambda returns it as
|
package/dist/index.js
CHANGED
package/dist/init-with.js
CHANGED
|
@@ -8,13 +8,21 @@ import { mcpConfigFile, renderMcpConfig } from './agents-guide.js';
|
|
|
8
8
|
import { loadDocument, parseYaml, validateDocument } from './config.js';
|
|
9
9
|
import { inspectExtensionRevision } from './extensions.js';
|
|
10
10
|
|
|
11
|
+
import { collectDependencySet, installSteps, renderPackageManifest } from './project-dependencies.js';
|
|
12
|
+
|
|
11
13
|
import { ConfigError, assert } from './errors.js';
|
|
12
14
|
|
|
13
15
|
/** Directory names inside the generated site. The route project lives under `app/`; everything else is operator-owned. */
|
|
14
|
-
|
|
16
|
+
const PROJECT_DIRECTORY = 'app', HOST_FILE = 'host.mjs', ROUTES_FILE = 'routes/extensions.yaml';
|
|
15
17
|
const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
|
|
19
27
|
export function parseWithNames(value ) {
|
|
20
28
|
const names = value.split(',').map(name => name.trim());
|
|
@@ -22,7 +30,7 @@ export function parseWithNames(value ) {
|
|
|
22
30
|
assert(new Set(names).size === names.length, 'Duplicate --with names');
|
|
23
31
|
return names;
|
|
24
32
|
}
|
|
25
|
-
|
|
33
|
+
const packageName = (name ) => `@jimhoyd/urlcode-${name}`;
|
|
26
34
|
const isCode = (error , code ) => error instanceof Error && 'code' in error && error.code === code;
|
|
27
35
|
const strings = (value ) => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
28
36
|
const record = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -32,7 +40,7 @@ const record = (value ) => value !== n
|
|
|
32
40
|
* conditions), imports it, and calls its `scaffold` export. Nothing is bundled; core never imports these packages
|
|
33
41
|
* at build time. Refuses a missing package or a package without `scaffold` before anything is written.
|
|
34
42
|
*/
|
|
35
|
-
|
|
43
|
+
async function loadScaffold(name , request , cwd ) {
|
|
36
44
|
const pkg = packageName(name);
|
|
37
45
|
let entry ;
|
|
38
46
|
try { entry = createRequire(join(cwd, 'package.json')).resolve(pkg); }
|
|
@@ -68,7 +76,7 @@ async function write(target , content , mode = 0o644)
|
|
|
68
76
|
const file = await open(target, 'wx', mode);
|
|
69
77
|
try { await file.writeFile(content); await file.sync(); } finally { await file.close(); }
|
|
70
78
|
}
|
|
71
|
-
|
|
79
|
+
function renderHost(names , results ) {
|
|
72
80
|
const lines = [`// Generated by urlcode init --with ${names.join(',')}. Trusted operator code: keep it outside ${PROJECT_DIRECTORY}/ and review before serving.`];
|
|
73
81
|
for (const result of results) lines.push(...result.hostImports);
|
|
74
82
|
lines.push('');
|
|
@@ -85,12 +93,23 @@ function demote(markdown ) {
|
|
|
85
93
|
let fence = false;
|
|
86
94
|
return markdown.split('\n').map(line => { if (/^\s*(?:```|~~~)/.test(line)) fence = !fence; return !fence && /^#{1,5} /.test(line) ? `#${line}` : line; }).join('\n');
|
|
87
95
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
96
|
+
function renderDependencySection(directory , set ) {
|
|
97
|
+
const rows = set.pins.map(pin => `- \`${pin.name}\` ${pin.version} (${pin.role})${pin.specifier === pin.version ? '' : ` installed from \`${pin.specifier}\``}`);
|
|
98
|
+
const lines = ['## Dependencies', '',
|
|
99
|
+
'`package.json` pins the runtime, every extension named in `--with` and their declared peers to the exact versions that were installed when this site was generated. Those versions were checked against each package\'s own `peerDependencies` as one set.', '',
|
|
100
|
+
...rows, '',
|
|
101
|
+
...installSteps(directory, set).flatMap(step => [step, '']),
|
|
102
|
+
set.local ? 'At least one pin is a local path or tarball rather than a registry version: reproducing this install needs that path to exist, so keep it under your control or replace the specifier before publishing the site.' : 'The pins are registry versions; `npm install` resolves them without the network only if your cache or mirror already holds them.', '',
|
|
103
|
+
'There is no upgrade command. Changing a pinned version today means editing `package.json` yourself and re-running `npm install`; review the extension changelogs first.', ''];
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
function renderReadme(directory , names , results , starter , env , projectSha256 , set ) {
|
|
107
|
+
const steps = [...(set ? installSteps(directory, set) : []), ...results.flatMap(result => result.nextSteps)];
|
|
90
108
|
const parts = [`# ${basename(directory)}`, '',
|
|
91
109
|
`Created with \`urlcode init ${basename(directory)} --with ${names.join(',')}\`. \`${PROJECT_DIRECTORY}/\` is the route project (\`urlcode.yaml\`, functions, tests); \`${HOST_FILE}\` is the trusted operator host that wires the installed extension packages; operator modules and private data stay outside the project. Run every command with \`--project ${PROJECT_DIRECTORY} --host-file "$PWD/${HOST_FILE}"\`.`, '',
|
|
92
110
|
'## Starter', '', `The starter files live in \`${PROJECT_DIRECTORY}/\`; add \`--project ${PROJECT_DIRECTORY}\` and the host file to the commands below.`, '', demote(starter).trim(), ''];
|
|
93
111
|
for (const result of results) parts.push(`## Extension: ${result.name}`, '', result.readme.trim(), '');
|
|
112
|
+
if (set) parts.push(renderDependencySection(directory, set));
|
|
94
113
|
parts.push('## Next steps', '', ...steps.map((step, index) => `${index + 1}. ${step}`), '');
|
|
95
114
|
if (Object.keys(env).length) parts.push('## Environment', '', ...Object.entries(env).map(([key, text]) => `- \`${key}\`: ${text}`), '');
|
|
96
115
|
parts.push('## Project revision', '', `\`${PROJECT_DIRECTORY}/urlcode.yaml\` currently has revision \`${projectSha256}\` (\`inspectExtensionRevision\`). Review the project, then pin exactly that value where the host expects it; any change to extension YAML, policies or mounts changes it and needs a new explicit review.`, '');
|
|
@@ -102,7 +121,7 @@ export function renderReadme(directory , names , result
|
|
|
102
121
|
* `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
|
|
103
122
|
* their scaffolds computed before anything is written, so a refusal leaves no directory behind.
|
|
104
123
|
*/
|
|
105
|
-
export async function initProjectWith(destination , names , { cwd = process.cwd() } = {}) {
|
|
124
|
+
export async function initProjectWith(destination , names , { cwd = process.cwd(), manifest = true, pins } = {}) {
|
|
106
125
|
assert(names.length > 0, 'Provide at least one --with name');
|
|
107
126
|
const directory = resolve(destination), project = join(directory, PROJECT_DIRECTORY), hostFile = join(directory, HOST_FILE);
|
|
108
127
|
const request = { directory, project, hostFile, names };
|
|
@@ -120,6 +139,9 @@ export async function initProjectWith(destination , names
|
|
|
120
139
|
const seen = new Set ();
|
|
121
140
|
for (const file of result.files) { const path = filePath(directory, file.path); assert(!seen.has(path), `${result.name} scaffolds ${file.path} twice`); seen.add(path); }
|
|
122
141
|
}
|
|
142
|
+
// Also resolved before the destination exists: an incompatible or incompletely installed set refuses with
|
|
143
|
+
// nothing written. It runs after the scaffold conflicts so a composition error is still reported as one.
|
|
144
|
+
const dependencies = manifest ? await collectDependencySet(names, names.map(packageName), { cwd, ...(pins === undefined ? {} : { overrides: pins }) }) : undefined;
|
|
123
145
|
await mkdir(dirname(directory), { recursive: true });
|
|
124
146
|
await mkdir(directory, { mode: 0o700 }); // refuses an existing destination
|
|
125
147
|
try {
|
|
@@ -153,13 +175,16 @@ export async function initProjectWith(destination , names
|
|
|
153
175
|
await write(target, file.content, file.mode ?? 0o644); written.add(target);
|
|
154
176
|
}
|
|
155
177
|
await write(hostFile, renderHost(names, results), 0o600);
|
|
156
|
-
await write(join(directory, '
|
|
178
|
+
if (dependencies) await write(join(directory, 'package.json'), renderPackageManifest(directory, dependencies));
|
|
179
|
+
await write(join(directory, 'README.md'), renderReadme(directory, names, results, starter, env, projectSha256, dependencies));
|
|
157
180
|
await write(join(directory, '.gitignore'), 'node_modules/\ndata/\n.env\n.env.*\n');
|
|
158
181
|
// The read-only MCP server for agents opened at the site root; --host-file and --allow-authoring stay operator choices.
|
|
159
182
|
await write(join(directory, mcpConfigFile), renderMcpConfig(PROJECT_DIRECTORY));
|
|
160
183
|
// AGENTS.md: initProject writes the application-level file into app/ once it produces one (NEXT-STEPS 1.1);
|
|
161
184
|
// nothing here overrides it. A site-level agent note would be assembled beside README.md at this point.
|
|
162
|
-
return { directory, project, hostFile, extensions: [...names], projectSha256,
|
|
185
|
+
return { directory, project, hostFile, extensions: [...names], projectSha256,
|
|
186
|
+
nextSteps: [...(dependencies ? installSteps(directory, dependencies) : []), ...results.flatMap(result => result.nextSteps)],
|
|
187
|
+
dependencies: dependencies?.pins ?? [] };
|
|
163
188
|
} catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
|
|
164
189
|
} finally { wipe(); }
|
|
165
190
|
}
|
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,
|
|
@@ -105,5 +111,5 @@ export async function buildManifest(project ,options ={})
|
|
|
105
111
|
}
|
|
106
112
|
/** The manifest as `build` writes it: two-space JSON with a trailing newline. */
|
|
107
113
|
export function renderManifest(manifest ) {return JSON.stringify(manifest,null,2)+'\n';}
|
|
108
|
-
|
|
114
|
+
const manifestFileName='manifest.json';
|
|
109
115
|
export function manifestPath(out ) {return join(out,manifestFileName);}
|
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'}]};
|
|
@@ -54,7 +54,7 @@ async function verdict(root ,origin ) {
|
|
|
54
54
|
catch{return {valid:false ,note:'Project does not validate; call run_validate for the CLI report.'};}
|
|
55
55
|
}
|
|
56
56
|
function object(value ) {return value!==null&&typeof value==='object'&&!Array.isArray(value);}
|
|
57
|
-
function expandHandler(
|
|
57
|
+
function expandHandler(handler ) {
|
|
58
58
|
if(object(handler))return handler;
|
|
59
59
|
assert(typeof handler==='string','Handler must be a route object or a short form');
|
|
60
60
|
if(/^https?:\/\//.test(handler))return {redirect:{url:handler}};
|
|
@@ -83,7 +83,7 @@ async function createRoute(root ,args ,origin
|
|
|
83
83
|
assert(file==='urlcode.yaml'||(loaded.document.includes??[]).includes(file),'file must be urlcode.yaml or an include listed in it');
|
|
84
84
|
const target=await confinedPath(root,file);
|
|
85
85
|
assert(!Object.hasOwn(loaded.routes,path),'Route already exists');
|
|
86
|
-
const route={...expandHandler(
|
|
86
|
+
const route={...expandHandler(args.handler)};
|
|
87
87
|
const middleware=expandMiddleware(args.middleware);if(middleware)route.middleware=middleware;
|
|
88
88
|
const lockPath=join(root,'urlcode.yaml.lock'),lock=await open(lockPath,'wx',0o600);
|
|
89
89
|
let temp ;
|
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.
|
|
81
|
+
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.1'}}});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/cache.js
CHANGED
|
@@ -198,7 +198,7 @@ function bodyOf(result ) { return result.body ? (Buffer.is
|
|
|
198
198
|
|
|
199
199
|
// Conditional requests for results the handler did not validate itself:
|
|
200
200
|
// assets answer 304 before this phase, so only 200 results are examined.
|
|
201
|
-
function revalidate(
|
|
201
|
+
function revalidate(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) {
|
|
@@ -249,7 +249,7 @@ export function onResponse(state , req , result
|
|
|
249
249
|
// refusal produced ahead of the handler keeps its own headers.
|
|
250
250
|
if (flight || state.statuses.has(result.status)) headers = mergeVary(headers, state.vary);
|
|
251
251
|
let out = { ...result, headers };
|
|
252
|
-
if (state.strategy === 'revalidate') out = revalidate(
|
|
252
|
+
if (state.strategy === 'revalidate') out = revalidate(req, out);
|
|
253
253
|
if (!flight) return out;
|
|
254
254
|
// Store decision for the request that reached the handler; waiters are
|
|
255
255
|
// released either way, with the entry or with nothing.
|
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'}});
|