@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.
Files changed (232) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
  2. package/.claude/skills/urlcode-operations/SKILL.md +16 -22
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +83 -80
  6. package/ROADMAP.md +61 -331
  7. package/SECURITY.md +23 -8
  8. package/dist/BUILD-MANIFEST.json +42 -47
  9. package/dist/agents-guide.js +18 -18
  10. package/dist/build-static.js +136 -0
  11. package/dist/capabilities.js +85 -31
  12. package/dist/capability-query.js +0 -1
  13. package/dist/cli.js +24 -32
  14. package/dist/compliance-rules/baseline.js +2 -10
  15. package/dist/compliance-rules/privacy.js +5 -16
  16. package/dist/compliance-rules/shared.js +0 -2
  17. package/dist/compliance.js +6 -8
  18. package/dist/config.js +15 -8
  19. package/dist/context.js +9 -10
  20. package/dist/examples.js +2 -2
  21. package/dist/explain-cli.js +7 -5
  22. package/dist/explain.js +10 -5
  23. package/dist/extensions.js +61 -2
  24. package/dist/function-sources.js +34 -2
  25. package/dist/function-worker.js +3 -1
  26. package/dist/functions.js +84 -13
  27. package/dist/guest-api.js +29 -3
  28. package/dist/index.js +3 -6
  29. package/dist/manifest.js +11 -7
  30. package/dist/mcp-authoring.js +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/observability.js +1 -21
  33. package/dist/policies/cache.js +8 -3
  34. package/dist/policies.js +3 -1
  35. package/dist/policy.js +28 -9
  36. package/dist/prerender.js +4 -0
  37. package/dist/project-tests.js +3 -3
  38. package/dist/readiness.js +35 -9
  39. package/dist/route-diff.js +12 -5
  40. package/dist/router.js +5 -7
  41. package/dist/runtime.js +77 -58
  42. package/dist/sandbox.js +48 -0
  43. package/dist/scaffold.js +0 -0
  44. package/dist/scripts/operational-drills.js +12 -54
  45. package/dist/server.js +3 -29
  46. package/dist/tooling.js +1 -1
  47. package/dist/trusted-functions.js +210 -0
  48. package/dist/types/build-static.d.ts +43 -0
  49. package/dist/types/capabilities.d.ts +14 -5
  50. package/dist/types/compliance-rules/shared.d.ts +0 -2
  51. package/dist/types/compliance.d.ts +0 -3
  52. package/dist/types/config.d.ts +2 -1
  53. package/dist/types/context.d.ts +2 -1
  54. package/dist/types/examples.d.ts +1 -1
  55. package/dist/types/explain.d.ts +6 -0
  56. package/dist/types/extensions.d.ts +55 -0
  57. package/dist/types/function-sources.d.ts +4 -0
  58. package/dist/types/functions.d.ts +48 -5
  59. package/dist/types/guest-api.d.ts +1 -0
  60. package/dist/types/index.d.ts +3 -6
  61. package/dist/types/manifest.d.ts +5 -3
  62. package/dist/types/observability.d.ts +1 -14
  63. package/dist/types/project-tests.d.ts +1 -2
  64. package/dist/types/readiness.d.ts +12 -3
  65. package/dist/types/router.d.ts +2 -1
  66. package/dist/types/runtime.d.ts +0 -27
  67. package/dist/types/sandbox.d.ts +12 -0
  68. package/dist/types/scaffold.d.ts +0 -2
  69. package/dist/types/server.d.ts +1 -4
  70. package/dist/types/tooling.d.ts +3 -3
  71. package/dist/types/trusted-functions.d.ts +29 -0
  72. package/dist/types/types.d.ts +22 -8
  73. package/dist/types/verify-deployment.d.ts +2 -2
  74. package/dist/types.js +25 -5
  75. package/dist/typescript-authoring.js +67 -18
  76. package/dist/verify-deployment.js +3 -3
  77. package/docs/AI-AUTHORING.md +109 -10
  78. package/docs/ASSETS.md +2 -1
  79. package/docs/AUTH-BACKUP.md +32 -0
  80. package/docs/AWS.md +4 -4
  81. package/docs/BEST-PRACTICES.md +17 -8
  82. package/docs/CAPABILITIES.md +30 -17
  83. package/docs/CAPACITY.md +128 -31
  84. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  85. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  86. package/docs/CI.md +8 -3
  87. package/docs/CLOUDFLARE.md +1 -2
  88. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  89. package/docs/COMPLIANCE.md +6 -9
  90. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  91. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  92. package/docs/EXTENSIONS.md +193 -11
  93. package/docs/FRAMEWORK.md +53 -33
  94. package/docs/FUNCTION-SECURITY.md +173 -32
  95. package/docs/INSTALL.md +0 -5
  96. package/docs/LOAD-TESTING.md +4 -4
  97. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  98. package/docs/MIDDLEWARE.md +27 -16
  99. package/docs/MONITORING.md +2 -19
  100. package/docs/OBSERVABILITY.md +7 -16
  101. package/docs/OPEN-DECISIONS.md +184 -0
  102. package/docs/OPERATIONAL-PROOF.md +26 -30
  103. package/docs/OPERATIONS.md +23 -32
  104. package/docs/POLICIES.md +27 -8
  105. package/docs/PRERENDER.md +25 -13
  106. package/docs/PROJECT-DIRECTION.md +35 -10
  107. package/docs/READINESS.md +6 -2
  108. package/docs/README.md +20 -41
  109. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  110. package/docs/RELEASE-READINESS.md +50 -30
  111. package/docs/RELEASE-SECURITY.md +72 -86
  112. package/docs/RESILIENCE.md +16 -15
  113. package/docs/ROUTING.md +7 -10
  114. package/docs/SANDBOX-REVIEW.md +19 -6
  115. package/docs/SCAFFOLDING.md +0 -2
  116. package/docs/SECURITY-AUDIT.md +42 -2
  117. package/docs/SPECIFICATION.md +63 -27
  118. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
  119. package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
  120. package/docs/SPIKE-CORE-LAYERING.md +368 -0
  121. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
  122. package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
  123. package/docs/STARTERS.md +1 -1
  124. package/docs/STATIC.md +105 -0
  125. package/docs/TOOLING.md +17 -12
  126. package/docs/TUNNELS.md +0 -3
  127. package/docs/TYPESCRIPT-AUTHORING.md +32 -12
  128. package/docs/TYPESCRIPT.md +25 -4
  129. package/docs/VERCEL.md +4 -5
  130. package/docs/VERSION-ALIGNMENT.md +46 -0
  131. package/docs/YAML-GUIDE.md +0 -3
  132. package/docs/YAML-REFERENCE.md +16 -6
  133. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  134. package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
  135. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
  136. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
  137. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  138. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  139. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  140. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
  141. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
  142. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  143. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
  144. package/docs/archive/README.md +27 -0
  145. package/docs/policies/agents.md +1 -1
  146. package/docs/policies/cache.md +13 -0
  147. package/docs/policies/compression.md +3 -2
  148. package/docs/policies/security.md +3 -2
  149. package/examples/assets/example.yaml +2 -2
  150. package/examples/aws/example.yaml +1 -1
  151. package/examples/cloudflare/example.yaml +1 -1
  152. package/examples/conditions/example.yaml +1 -1
  153. package/examples/cookbook/example.yaml +1 -1
  154. package/examples/cookbook/middleware/bucket.mjs +12 -2
  155. package/examples/cookbook/middleware/locale.mjs +7 -3
  156. package/examples/cookbook/route-index.json +1 -1
  157. package/examples/cookbook/routes/middleware.yaml +1 -1
  158. package/examples/egress/example.yaml +1 -1
  159. package/examples/extensions/example.yaml +1 -1
  160. package/examples/prerender/README.md +14 -6
  161. package/examples/prerender/example.yaml +1 -1
  162. package/examples/prerender/functions/page.mjs +4 -2
  163. package/examples/prerender/middleware/template.mjs +1 -1
  164. package/examples/prerender/prerender.mjs +1 -1
  165. package/examples/prerender/urlcode.yaml +8 -4
  166. package/examples/provider-conformance/example.yaml +1 -1
  167. package/examples/vercel/example.yaml +1 -1
  168. package/llms-full.txt +686 -239
  169. package/llms.txt +27 -15
  170. package/package.json +32 -5
  171. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  172. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
  173. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
  174. package/recipes/authenticated-json-api/README.md +4 -3
  175. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  176. package/recipes/authenticated-json-api/recipe.yaml +2 -2
  177. package/recipes/contact-form/functions/contact.mjs +2 -1
  178. package/recipes/contact-form/recipe.yaml +3 -3
  179. package/recipes/cors-api/README.md +2 -2
  180. package/recipes/cors-api/recipe.yaml +2 -2
  181. package/recipes/health-page/README.md +1 -1
  182. package/recipes/health-page/recipe.yaml +1 -1
  183. package/recipes/json-api/README.md +1 -1
  184. package/recipes/json-api/recipe.yaml +4 -4
  185. package/recipes/middleware/README.md +8 -4
  186. package/recipes/middleware/middleware/bucket.mjs +12 -2
  187. package/recipes/middleware/middleware/locale.mjs +7 -3
  188. package/recipes/middleware/recipe.yaml +1 -1
  189. package/recipes/protected-download/README.md +1 -1
  190. package/recipes/protected-download/recipe.yaml +2 -2
  191. package/recipes/redirect/recipe.yaml +1 -1
  192. package/recipes/static-plus-api/README.md +2 -2
  193. package/recipes/static-plus-api/public/index.html +1 -1
  194. package/recipes/static-plus-api/recipe.yaml +2 -2
  195. package/recipes/static-plus-api/urlcode.yaml +1 -1
  196. package/recipes/typescript/README.md +2 -1
  197. package/recipes/typescript/recipe.yaml +5 -5
  198. package/recipes/webhook-receiver/README.md +5 -1
  199. package/recipes/webhook-receiver/recipe.yaml +2 -1
  200. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  201. package/schemas/recipe.schema.json +4 -3
  202. package/schemas/urlcode.schema.json +14 -41
  203. package/skills/urlcode/SKILL.md +32 -11
  204. package/starters/default/AGENTS.md +18 -18
  205. package/starters/default/urlcode.yaml +0 -1
  206. package/dist/link-api.js +0 -136
  207. package/dist/link-cli.js +0 -141
  208. package/dist/link-events.js +0 -76
  209. package/dist/link-records.js +0 -31
  210. package/dist/link-store-worker.js +0 -150
  211. package/dist/link-store.js +0 -250
  212. package/dist/management-policy.js +0 -40
  213. package/dist/sqlite-version.js +0 -6
  214. package/dist/types/link-api.d.ts +0 -30
  215. package/dist/types/link-cli.d.ts +0 -37
  216. package/dist/types/link-events.d.ts +0 -27
  217. package/dist/types/link-records.d.ts +0 -11
  218. package/dist/types/link-store-worker.d.ts +0 -1
  219. package/dist/types/link-store.d.ts +0 -130
  220. package/dist/types/management-policy.d.ts +0 -8
  221. package/dist/types/sqlite-version.d.ts +0 -1
  222. package/docs/DYNAMIC-LINKS.md +0 -61
  223. package/docs/links/cli.md +0 -110
  224. package/docs/links/limits.md +0 -175
  225. package/docs/links/management-api.md +0 -80
  226. package/docs/links/pools.md +0 -75
  227. package/docs/links/setup.md +0 -135
  228. package/docs/yaml/links.md +0 -30
  229. package/examples/live-links/README.md +0 -11
  230. package/examples/live-links/example.yaml +0 -21
  231. package/examples/live-links/tests/requests.json +0 -6
  232. package/examples/live-links/urlcode.yaml +0 -16
package/dist/cli.js CHANGED
@@ -21,15 +21,13 @@ import { loadOperatorPolicy, prepareFunctionSnapshot, requestedPermissions } fro
21
21
  import { loadDocument } from './config.js';
22
22
  import { describeExtensions } from './tooling.js';
23
23
 
24
- import {parseLinkBinding,runLinkCommand,linkPoolOptions} from './link-cli.js';
25
24
  import { ConfigError, HttpError } from './errors.js';
26
- import {supportsConcurrentWal} from './sqlite-version.js';
27
25
  import { registry as policyRegistry } from './policies.js';
28
26
  import { loadComplianceRules, profileNames as complianceProfiles } from './compliance.js';
29
27
  import { parseRouteSnapshot, diffRoutes, renderRouteDiff } from './route-diff.js';
30
28
  import { readFile } from 'node:fs/promises';
31
29
 
32
- const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
30
+ const usage = `URLCode 0.4.0-alpha.3 — local/self-hosted runtime
33
31
  urlcode init <directory> [--with auth,admin] # --with: layered site from installed @jimhoyd/urlcode-<name> packages
34
32
  urlcode scaffold [--project directory] [--dry-run]
35
33
  urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
@@ -41,7 +39,8 @@ const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
41
39
  policies: [--trusted-proxies 10.0.0.0/8,fd00::/8] # peers allowed to set X-Forwarded-For for client policies
42
40
  urlcode add <destination-url> [--alias short-code] [--project directory]
43
41
  urlcode test [--project directory] [--origin https://links.example]
44
- urlcode build --target cloudflare [--project directory] [--out dist/cloudflare] [--origin https://links.example]
42
+ urlcode build --target cloudflare|static [--project directory] [--out dist/cloudflare|dist/static] [--origin https://links.example]
43
+ # static: redirect/respond/page/static/download only, compiled for S3 + CloudFront; no server, see docs/STATIC.md
45
44
  urlcode routes [--project directory] [--origin https://links.example]
46
45
  diff: [--compare previous-routes.json] [--format json|markdown] # added/removed/changed routes against an earlier report; always exits 0
47
46
  urlcode audit [--project directory] [--expect-routes 2]
@@ -54,16 +53,10 @@ const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
54
53
  [--compliance baseline|strict|privacy|none] [--compliance-rules ...] [--compliance-ignore id,id] [--compliance-warn]
55
54
  # compares the running deployment's responses with what this project declares; never follows redirects, no --insecure
56
55
  urlcode permissions [--project directory] # inspect requested bindings and egress origins; grants nothing
57
- urlcode explain [/route] [--project directory] [--target self-hosted|cloudflare|aws|vercel] [--host-file ...] [--json]
56
+ urlcode explain [/route] [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--host-file ...] [--json]
58
57
  # effective methods, handler, middleware, inputs, policies, cache outcome, bindings and target support from the compiled configuration
59
58
  urlcode manifest [--project directory] [--json] # generated semantic manifest; build writes the same file as manifest.json
60
59
  urlcode extensions [--project directory] [--host-file /absolute/operator/host.mjs] [--json] # registered contracts and schemas; executes trusted host code, activates nothing
61
- urlcode links init|create|get|list|update|delete|export|import|api --store /absolute/links.sqlite [--collection links]
62
- create/update: --destination https://example.com [--code abc] [--status 302] [--enabled true] [--expires UTC]
63
- update/delete: --code abc --if-version N (update replaces all mutable fields)
64
- export: consistent NDJSON snapshot to stdout [--collection links] [--page-size 100]
65
- import: --input /absolute/export.ndjson restores into empty collections (versions are reassigned)
66
- api: --auth-file /operator/management.json (or legacy --token-file /operator/token) --port 3001 (separate authenticated server)
67
60
  urlcode import [netlify|cloudflare|vercel|netlify-toml] <file> [--format csv|json|yaml] [--out new-file] [--dry-run] [--report json]
68
61
  urlcode export --target netlify|cloudflare|vercel|netlify-toml|csv|json|yaml [--project directory] [--out new-file] [--report json]
69
62
  conversion: [--accept-provider-differences] # explicit non-lossless migration candidate; exact behavior requires runtime
@@ -74,16 +67,14 @@ const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
74
67
  urlcode verify-provider --target self-hosted|aws|vercel|cloudflare --origin https://owned-fixture.example
75
68
  [--timeout-ms 3000] [--release label] [--git-commit sha] # explicitly invokes synthetic deployment probes
76
69
  urlcode mcp [--project directory] [--allow-authoring] [--host-file ...] # bounded stdio tooling; --allow-authoring adds project-confined authoring tools, host file adds get_extensions
77
- urlcode capabilities [--target self-hosted|cloudflare|aws|vercel] [--json]
70
+ urlcode capabilities [--target self-hosted|cloudflare|aws|vercel|static] [--json]
78
71
  urlcode capabilities <name> [--json] # one catalog entry: schema fragment, constraints, grants, targets, bundled uses
79
72
  urlcode schema <path> [--json|--yaml] # schema fragment for route, redirect, policies.cache, site.sitemap, ...
80
- urlcode context [--project directory] [--target self-hosted|cloudflare|aws|vercel] [--budget 500] [--json] [--stats]
73
+ urlcode context [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--budget 500] [--json] [--stats]
81
74
  # compact facts for an authoring agent from the compiled project; --stats compares estimated tokens with the docs
82
75
  urlcode doctor
83
76
  serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp: --host-file /absolute/operator/host.mjs (trusted code outside project)
84
- serve/dev/validate/test/routes/audit/benchmark: --link-store links=/absolute/links.sqlite
85
- Store pool controls: --link-readers 2 (1–8), --link-read-limit 32, --link-write-limit 32 (1–32 each)
86
- Dev loads .env.local and watches; serve does neither. Functions run in WASM isolation; external bindings require --policy outside the project.
77
+ 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.
87
78
  `;
88
79
  const print = (value ) => process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value) + '\n');
89
80
  const options = {
@@ -91,10 +82,8 @@ const options = {
91
82
  project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
92
83
  port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
93
84
  'expect-routes':{type:'string'}, requests:{type:'string'}, concurrency:{type:'string'}, seconds:{type:'string'}, 'max-p95-ms':{type:'string'}, warmup:{type:'string'}, target:{type:'string'},
94
- 'link-readers':{type:'string'}, 'link-read-limit':{type:'string'}, 'link-write-limit':{type:'string'},
95
85
  workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
96
86
  'max-in-flight':{type:'string'}, 'max-in-flight-health':{type:'string'}, 'request-log':{type:'string'}, 'trust-request-id':{type:'boolean'}, 'trusted-proxies':{type:'string'}, metrics:{type:'boolean'},
97
- 'link-store':{type:'string'}, store:{type:'string'}, collection:{type:'string'}, code:{type:'string'}, destination:{type:'string'}, status:{type:'string'}, enabled:{type:'string'}, expires:{type:'string'}, 'if-version':{type:'string'}, limit:{type:'string'}, after:{type:'string'}, 'token-file':{type:'string'}, 'auth-file':{type:'string'}, input:{type:'string'}, 'page-size':{type:'string'},
98
87
  release:{type:'string'}, 'git-commit':{type:'string'}, 'timeout-ms':{type:'string'}, 'fail-on':{type:'string'}, 'expect-metrics':{type:'boolean'},
99
88
  budget:{type:'string'}, stats:{type:'boolean'}, out:{type:'string'}, 'dry-run':{type:'boolean'}, compare:{type:'string'}, format:{type:'string'}, compliance:{type:'string'}, 'compliance-rules':{type:'string'}, 'compliance-ignore':{type:'string'}, 'compliance-warn':{type:'boolean'}, policy:{ type:'string' }, origin:{ type:'string' }, alias:{ type:'string' }, local:{ type:'boolean' }, 'allow-authoring':{ type:'boolean' }, help:{ type:'boolean', short:'h' },
100
89
  } ;
@@ -131,7 +120,7 @@ async function complianceOptions(values )
131
120
  if(!complianceProfiles.includes(profile))throw new ConfigError(`Use --compliance ${complianceProfiles.join('|')}`);
132
121
  const ignore=(values['compliance-ignore'] ?? '').split(',').map(id=>id.trim()).filter(Boolean);
133
122
  const operator=await loadComplianceRules(values['compliance-rules'],values.project);
134
- const host={requestLog:values['request-log'] ?? 'minimal',linkEvents:false};
123
+ const host={requestLog:values['request-log'] ?? 'minimal'};
135
124
  if(!['minimal','detailed'].includes(host.requestLog))throw new ConfigError('Use --request-log minimal or detailed');
136
125
  return {profile,ignore,origin:values.origin,host,rules:operator?.rules ?? [],disable:operator?.disable ?? [],override:operator?.override ?? {}};
137
126
  }
@@ -151,7 +140,7 @@ let serving = false;
151
140
  try {
152
141
  const { values, positionals } = parseArgs({ allowPositionals:true, options });
153
142
  const [command, arg, ...extra] = positionals;
154
- values.port ??= command==='links' && arg==='api' ? '3001' : '3000';
143
+ values.port ??= '3000';
155
144
  if (values.help || !command) print(usage);
156
145
  else {
157
146
  if (values['host-file'] !== undefined) {
@@ -162,7 +151,7 @@ try {
162
151
  if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
163
152
  if (values['allow-authoring'] && command !== 'mcp') throw new ConfigError('--allow-authoring is only supported by mcp');
164
153
  const hostOptions = { extensions: operatorHost.extensions, plugins: operatorHost.plugins };
165
- if ((!['import','recipes','recipe','examples','example','bulk-import'].includes(command) && extra.length) || (!['init','add','links','import','recipes','recipe','examples','example','bulk-import','explain','capabilities','schema'].includes(command) && arg)) throw new ConfigError('Unexpected positional arguments');
154
+ 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');
166
155
 
167
156
  if(command==='import'||command==='export'){
168
157
  const { runInterchange } = await import('./interchange-cli.js');
@@ -190,9 +179,8 @@ try {
190
179
  print(text);
191
180
  // Estimates only (characters / 4); a tokenizer is not a dependency. Stats go to stderr so stdout stays parseable.
192
181
  if (values.stats) process.stderr.write(JSON.stringify({ event:'stats', estimate:'characters/4', documentationTokens:await documentationTokens(), contextTokens:estimateTokens(text) }) + '\n');
193
- }else if(command==='links'){await runLinkCommand(arg,values,print);}else{
182
+ }else{
194
183
  const permissions = await loadOperatorPolicy(values.policy,values.project);
195
- const linkStore=parseLinkBinding(values['link-store'],linkPoolOptions(values));
196
184
  switch (command) {
197
185
  case 'routes': case 'audit': case 'benchmark': {
198
186
  const number = (key ,fallback ) => {
@@ -208,12 +196,12 @@ try {
208
196
  if(!['json','markdown'].includes(format))throw new ConfigError('Use --format json or markdown');
209
197
  if(values.format!==undefined && values.compare===undefined)throw new ConfigError('--format applies to routes --compare');
210
198
  const started=performance.now();
211
- const app=await startServer({...hostOptions,project:values.project,port:0,local:true,permissions,linkStore,origin:values.origin,log:()=>{}});
199
+ const app=await startServer({...hostOptions,project:values.project,port:0,local:true,permissions,origin:values.origin,log:()=>{}});
212
200
  const startupMs=performance.now()-started;
213
201
  try {
214
202
  if(command==='routes') {
215
203
  const plan=app.testPlan();
216
- if(values.compare===undefined) print({routes:plan.inventory.length,dynamicLinks:plan.dynamicLinks,inventory:plan.inventory,policies:plan.policies});
204
+ if(values.compare===undefined) print({routes:plan.inventory.length,inventory:plan.inventory,policies:plan.policies});
217
205
  else {
218
206
  // The diff reports; it never judges, so the exit code stays 0.
219
207
  const before=parseRouteSnapshot(JSON.parse(await readFile(values.compare,'utf8'))); // file boundary: an earlier `routes` report
@@ -243,7 +231,7 @@ try {
243
231
  const isFailOn = (value ) => (failLevels ).includes(value);
244
232
  if (!isFailOn(failOn)) throw new ConfigError(`Use --fail-on ${failLevels.join('|')}`);
245
233
  const report = await verifyDeployment(values.project, { target: values.target, origin: values.origin, expectRoutes: integer('expect-routes'), timeoutMs: integer('timeout-ms'),
246
- expectMetrics: values['expect-metrics'], failOn, compliance: await complianceOptions(values), complianceWarn: values['compliance-warn'], permissions, linkStore, log: print });
234
+ expectMetrics: values['expect-metrics'], failOn, compliance: await complianceOptions(values), complianceWarn: values['compliance-warn'], permissions, log: print });
247
235
  print(report); if (!report.pass) process.exitCode = 1; break;
248
236
  }
249
237
  case 'verify-provider': {
@@ -260,7 +248,11 @@ try {
260
248
  print(report);if(!report.pass)process.exitCode=1;break;
261
249
  }
262
250
  case 'build': {
263
- if (values.target !== 'cloudflare') throw new ConfigError('Use --target cloudflare');
251
+ if (values.target === 'static') {
252
+ const { buildStatic } = await import('./build-static.js');
253
+ print({ event:'built', ...await buildStatic(values.project,{ out:values.out, origin:values.origin }) }); break;
254
+ }
255
+ if (values.target !== 'cloudflare') throw new ConfigError('Use --target cloudflare or static');
264
256
  const { buildCloudflare } = await import('./build-cloudflare.js');
265
257
  print({ event:'built', ...await buildCloudflare(values.project,{ out:values.out, origin:values.origin }) }); break;
266
258
  }
@@ -283,24 +275,24 @@ try {
283
275
  }
284
276
  break;
285
277
  case 'validate': {
286
- const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions, linkStore, origin:values.origin });
287
- print({ event:'valid', dynamicLinks:runtime.testPlan().dynamicLinks, routes:runtime.count, version:runtime.version }); await runtime.close(); break;
278
+ const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions, origin:values.origin });
279
+ print({ event:'valid', routes:runtime.count, version:runtime.version }); await runtime.close(); break;
288
280
  }
289
281
  case 'add':
290
282
  if (!arg) throw new ConfigError('Provide an HTTP(S) destination URL');
291
283
  print({ event:'added', path:await addRedirect(values.project,arg,values.alias) }); break;
292
284
  case 'test': {
293
- const result = await runProjectTests(values.project, { ...hostOptions, log:print, permissions, linkStore, origin:values.origin });
285
+ const result = await runProjectTests(values.project, { ...hostOptions, log:print, permissions, origin:values.origin });
294
286
  print(result); if (result.failed) process.exitCode = 1; break;
295
287
  }
296
288
  case 'doctor':
297
- print({ node:process.version, sqlite:process.versions.sqlite, liveLinks:supportsConcurrentWal(process.versions.sqlite), platform:process.platform, architecture:process.arch, runtime:'node-process', functionSandbox:'quickjs-wasm', network:false, filesystem:false, guestNetwork: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;
289
+ 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;
298
290
  case 'dev': case 'serve': {
299
291
  const port = Number(values.port);
300
292
  if (!/^\d+$/.test(values.port) || !Number.isInteger(port) || port < 0 || port > 65535) throw new ConfigError('Invalid port');
301
293
  if (command === 'serve' && values.local) throw new ConfigError('serve never reads local dotenv files');
302
294
  const app = await startServer({ ...hostOptions, project:values.project, host:values.host, port,
303
- local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions, linkStore,
295
+ local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions,
304
296
  ...serverCapacity(values) });
305
297
  print({ event:'listening', address:app.address.address, port:app.address.port, mode:command, origin:app.origin });
306
298
  serving = true;
@@ -1,4 +1,4 @@
1
- import { oshp, rfc9111, rfc9110, rfc6585, rfc9309, breach, management, active, functionLike, hasSecrets, yamlHeader, handlerCacheControl, directive, emittedSecurityHeaders } from './shared.js';
1
+ import { oshp, rfc9111, rfc9110, rfc6585, rfc9309, breach, active, functionLike, hasSecrets, yamlHeader, handlerCacheControl, directive, emittedSecurityHeaders } from './shared.js';
2
2
 
3
3
 
4
4
  export const profile = 'baseline';
@@ -87,12 +87,4 @@ const expiredRoutes = {
87
87
  },
88
88
  };
89
89
 
90
- const managementPrivate = {
91
- id: 'ops/management-private', title: 'Management and probe listeners stay private', standard: management, severity: 'info', appliesTo: 'project',
92
- check({ document }) {
93
- if (document.dynamicLinks !== true) return [];
94
- return [{ message: 'dynamicLinks is enabled; the link management API and the /_urlcode probes are meant for a private bind', remediation: 'Run `urlcode links api` on a private interface with an operator auth file, and keep /_urlcode/health and /_urlcode/ready internal' }];
95
- },
96
- };
97
-
98
- export const rules = Object.freeze([securityHeaders, hstsOrigin, secretsCompression, secretsNoStore, cacheControlDeclared, throttleFunctions, robots, expiredRoutes, managementPrivate]);
90
+ export const rules = Object.freeze([securityHeaders, hstsOrigin, secretsCompression, secretsNoStore, cacheControlDeclared, throttleFunctions, robots, expiredRoutes]);
@@ -1,13 +1,12 @@
1
- import { monitoring, linkChannel, active } from './shared.js';
1
+ import { monitoring, active } from './shared.js';
2
2
 
3
3
 
4
4
  export const profile = 'privacy';
5
5
 
6
6
  // These rules check the host settings the operator declares for the
7
- // deployment under review (`host.requestLog`, `host.linkEvents`,
8
- // `host.includeCode`), because the runtime's log guarantees are settings of
9
- // the serving process, not of the project. An undeclared setting is reported
10
- // as unknown rather than assumed.
7
+ // deployment under review (`host.requestLog`), because the runtime's log
8
+ // guarantees are settings of the serving process, not of the project. An
9
+ // undeclared setting is reported as unknown rather than assumed.
11
10
  const requestLogMinimal = {
12
11
  id: 'privacy/request-log-minimal', title: 'Request log records stay minimal', standard: monitoring, severity: 'medium', appliesTo: 'project',
13
12
  check({ host }) {
@@ -17,16 +16,6 @@ const requestLogMinimal = {
17
16
  },
18
17
  };
19
18
 
20
- const linkEventsOff = {
21
- id: 'privacy/link-events-off', title: 'Link events stay off unless declared', standard: linkChannel, severity: 'medium', appliesTo: 'project',
22
- check({ document, host }) {
23
- if (document.dynamicLinks !== true || host.linkEvents === false) return [];
24
- if (host.linkEvents === null) return [{ severity: 'info', message: 'dynamicLinks is enabled and the deployment did not declare whether the link event channel is on', remediation: 'Declare host.linkEvents (and host.includeCode) for the deployment under review' }];
25
- if (host.includeCode === true) return [{ severity: 'high', message: 'The link event channel is on with includeCode, so every event names the short code a person followed', remediation: 'Leave includeCode off; the channel then redacts the code and reports only route, outcome and timing' }];
26
- return [{ message: 'The link event channel is on, so per-click records leave the process', remediation: 'Enable linkEvents only for a declared click-collection purpose and keep the collector bounded' }];
27
- },
28
- };
29
-
30
19
  const detailedParameters = {
31
20
  id: 'privacy/detailed-log-parameters', title: 'No detailed logging on parameterised routes', standard: monitoring, severity: 'low', appliesTo: 'route',
32
21
  check({ route, config, host }) {
@@ -35,4 +24,4 @@ const detailedParameters = {
35
24
  },
36
25
  };
37
26
 
38
- export const rules = Object.freeze([requestLogMinimal, linkEventsOff, detailedParameters]);
27
+ export const rules = Object.freeze([requestLogMinimal, detailedParameters]);
@@ -12,8 +12,6 @@ export const rfc6585 = Object.freeze({ name: 'RFC 6585 Additional HTTP
12
12
  export const rfc9309 = Object.freeze({ name: 'RFC 9309 Robots Exclusion Protocol', reference: 'https://www.rfc-editor.org/rfc/rfc9309' });
13
13
  export const breach = Object.freeze({ name: 'BREACH (compression side channel)', reference: 'https://www.breachattack.com/', section: 'Mitigations' });
14
14
  export const monitoring = Object.freeze({ name: 'URLCode logging guarantees', reference: 'docs/MONITORING.md', section: 'Log records' });
15
- export const linkChannel = Object.freeze({ name: 'URLCode link event channel', reference: 'docs/MONITORING.md', section: 'The link event channel' });
16
- export const management = Object.freeze({ name: 'URLCode management security', reference: 'docs/MANAGEMENT-SECURITY.md' });
17
15
  export const agentLists = Object.freeze({ name: 'URLCode bundled agent lists', reference: 'docs/policies/agents.md', section: 'Bundled lists' });
18
16
 
19
17
  export const active = (route ) => route.state === 'active';
@@ -37,7 +37,7 @@ import * as privacy from './compliance-rules/privacy.js';
37
37
 
38
38
 
39
39
  /** The host settings under review, null where the operator declared nothing. */
40
-
40
+
41
41
 
42
42
 
43
43
 
@@ -58,14 +58,14 @@ import * as privacy from './compliance-rules/privacy.js';
58
58
 
59
59
 
60
60
 
61
-
61
+
62
62
 
63
63
 
64
64
 
65
65
  export const severities = Object.freeze(['high','medium','low','info']);
66
66
  export const idPattern = /^[a-z][a-z0-9-]{0,31}\/[a-z][a-z0-9-]{0,63}$/;
67
67
  const referencePattern = /^(?:https?:\/\/\S+|RFC ?\d{3,5}|docs\/[A-Za-z0-9./-]+\.md)$/;
68
- const hostKeys = ['requestLog','linkEvents','includeCode'];
68
+ const hostKeys = ['requestLog'];
69
69
  const isSeverity = (value ) => (severities ).includes(value);
70
70
 
71
71
 
@@ -147,11 +147,9 @@ export async function loadComplianceRules(file , project
147
147
 
148
148
  function validateHost(host = {}) {
149
149
  assert(host && typeof host === 'object' && !Array.isArray(host) && Object.keys(host).every(key => hostKeys.includes(key)), `Compliance host settings accept ${hostKeys.join(', ')}`);
150
- const { requestLog, linkEvents, includeCode } = host ;
150
+ const { requestLog } = host ;
151
151
  assert(requestLog === undefined || requestLog === 'minimal' || requestLog === 'detailed', 'Compliance host.requestLog must be minimal or detailed');
152
- assert(linkEvents === undefined || typeof linkEvents === 'boolean', 'Compliance host.linkEvents must be a boolean');
153
- assert(includeCode === undefined || typeof includeCode === 'boolean', 'Compliance host.includeCode must be a boolean');
154
- return { requestLog: requestLog ?? null, linkEvents: linkEvents ?? null, includeCode: includeCode ?? null };
152
+ return { requestLog: requestLog ?? null };
155
153
  }
156
154
 
157
155
  function finding(rule , raw , route ) {
@@ -209,7 +207,7 @@ export async function runCompliance(app , { rules = [], profile =
209
207
  return {
210
208
  profile, rules: set.length, ruleIds: set.map(rule => rule.id), ignored: [...ignore], findings, counts, pass: counts.high === 0,
211
209
  evidence: {
212
- routes: plan.inventory.length, active, dynamicLinks: plan.dynamicLinks === true,
210
+ routes: plan.inventory.length, active,
213
211
  policies: [...new Set(Object.values(policies).flatMap(Object.keys))].sort(),
214
212
  files: loaded.files.map(file => relative(loaded.root, file)),
215
213
  origin: origin ?? null, target, host: hostSettings,
package/dist/config.js CHANGED
@@ -70,16 +70,20 @@ function modulePath(pattern , kind , file
70
70
  /**
71
71
  * Expands the YAML short forms into the canonical long form. `function: functions/x.mjs`
72
72
  * becomes `{source, args}` with an argument per `{param}` in the path, declaring any
73
- * parameter the route does not declare itself; a string middleware entry becomes `{source}`.
73
+ * parameter the route does not declare itself; a string middleware entry becomes `{source}`;
74
+ * a route-level `cache` becomes `policies.cache` (refused alongside a direct `policies.cache`).
74
75
  * Everything downstream (routes, audit, the compiled table) sees only the long form.
75
76
  */
76
77
  export function normalizeRoute(pattern , route ) {
77
78
  const authored = route ;
78
- if (typeof authored.function !== 'string' && !authored.middleware?.some(entry => typeof entry === 'string')) return route ;
79
+ const needsFunction = typeof authored.function === 'string';
80
+ const needsMiddleware = authored.middleware?.some(entry => typeof entry === 'string') ?? false;
81
+ const needsCache = authored.cache !== undefined;
82
+ if (!needsFunction && !needsMiddleware && !needsCache) return route ;
79
83
  const result = { ...(route ) };
80
- if (authored.middleware) result.middleware = authored.middleware.map((entry) => typeof entry === 'string' ? { source: modulePath(pattern, 'middleware', entry) } : entry);
81
- if (typeof authored.function === 'string') {
82
- const source = modulePath(pattern, 'function', authored.function);
84
+ if (needsMiddleware) result.middleware = authored.middleware .map((entry) => typeof entry === 'string' ? { source: modulePath(pattern, 'middleware', entry) } : entry);
85
+ if (needsFunction) {
86
+ const source = modulePath(pattern, 'function', authored.function );
83
87
  const names = pattern.split('/').flatMap(part => { const match = /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(part); return match ? [match[1] ] : []; });
84
88
  const declared = authored.parameters ?? [];
85
89
  const parameters = [...declared, ...names.filter(name => !declared.some(p => p.in === 'path' && p.name === name)).map(name => ({ name, in: 'path' , required: true, schema: { ...SHORT_FORM_PATH_SCHEMA } }))];
@@ -87,6 +91,11 @@ export function normalizeRoute(pattern , route
87
91
  if (parameters.length) result.parameters = parameters;
88
92
  result.function = expanded;
89
93
  }
94
+ if (needsCache) {
95
+ assert(result.policies?.cache === undefined, `Route ${pattern} declares both cache and policies.cache; use one form`);
96
+ result.policies = { ...result.policies, cache: authored.cache };
97
+ delete result.cache;
98
+ }
90
99
  return result;
91
100
  }
92
101
  export async function safeFile(root , file ) {
@@ -178,7 +187,6 @@ export async function loadDocumentInWorker(project )
178
187
  files.push(path);
179
188
  const part = validateDocument(await readConfig(path, budget));
180
189
  assert(!part.includes?.length, 'Nested includes are unsupported');
181
- assert(part.dynamicLinks===undefined, 'dynamicLinks may only be set in the entry urlcode.yaml');
182
190
  assert(part.site===undefined, 'site may only be set in the entry urlcode.yaml');
183
191
  for(const [name,extension]of Object.entries(part.extensions??{})){assert(!Object.hasOwn(extensions,name),'Duplicate extension declaration across files');extensions[name]=extension;assert(Object.keys(extensions).length<=16,'Maximum 16 extensions per project');}
184
192
  for (const [pattern, route] of Object.entries(part.routes)) {
@@ -190,8 +198,7 @@ export async function loadDocumentInWorker(project )
190
198
  if(Object.keys(extensions).length)document.extensions=extensions;
191
199
  normalizeRouteAuth(document, routes);
192
200
  assert(Object.keys(routes).length <= 100000, 'Maximum 100000 routes per project');
193
- assert(document.dynamicLinks===true || !Object.values(routes).some(route=>route.link), 'Link routes require dynamicLinks: true in urlcode.yaml');
194
- return { root, document, routes, files, version: createHash('sha256').update(JSON.stringify(document.extensions?{routes,extensions:document.extensions,policies:document.policies,profiles:document.profiles,site:document.site,dynamicLinks:document.dynamicLinks}: document.site ? {...(document.dynamicLinks===true?{routes,dynamicLinks:true}:{routes}), site:document.site} : document.dynamicLinks===true?{routes,dynamicLinks:true}:routes)).digest('hex').slice(0, 16) };
201
+ return { root, document, routes, files, version: createHash('sha256').update(JSON.stringify(document.extensions?{routes,extensions:document.extensions,policies:document.policies,profiles:document.profiles,site:document.site}: document.site ? {routes, site:document.site} : routes)).digest('hex').slice(0, 16) };
195
202
  }
196
203
  export async function loadBindings(root , local = false, environment = process.env) {
197
204
  const vars = {};
package/dist/context.js CHANGED
@@ -27,11 +27,11 @@ import {loadOperatorHost} from './operator-host.js';
27
27
 
28
28
 
29
29
 
30
-
30
+
31
31
 
32
32
 
33
33
 
34
-
34
+
35
35
 
36
36
 
37
37
 
@@ -40,14 +40,14 @@ import {loadOperatorHost} from './operator-host.js';
40
40
  /** Characters divided by four, rounded up: an estimate, not a tokenizer. */
41
41
  export function estimateTokens(text ) {return Math.ceil(text.length/4);}
42
42
  export function renderContext(context ) {return stringify(context,{lineWidth:0,aliasDuplicateObjects:false});}
43
- const handlerNames=['redirect','respond','page','static','download','function','link','proxy','conditional','extension'] ;
43
+ const handlerNames=['redirect','respond','page','static','download','function','proxy','conditional','extension'] ;
44
44
  const policyNames=Object.keys(registry).sort() ;
45
45
  // Fixed for every project: what generation must not attempt, whatever the documentation says.
46
46
  const constraints ={
47
- guestNetwork:{value:false,note:'Functions and middleware run in a WASM sandbox without fetch or sockets; outbound calls are proxy or signals routes under operator grants'},
48
- nodeApis:{value:false,note:'No Node built-ins, process, filesystem or npm packages in guest code; relative ES-module imports only'},
47
+ guestNetwork:{value:true,note:'Trusted (default) functions and middleware run in-process with full Node network access; route `sandbox: true` runs that route in a WASM sandbox without fetch or sockets, where outbound calls must be proxy or signals routes under operator grants'},
48
+ nodeApis:{value:true,note:'Trusted (default) functions and middleware have full Node built-ins, process, filesystem and npm packages available, same as any other project code; route `sandbox: true` restricts that route to relative ES-module imports only, no Node built-ins/filesystem/npm packages'},
49
49
  regexRoutes:{value:false,note:'Paths are whole segments: exact literals or {param} placeholders declared as required string parameters'},
50
- oneHandlerPerRoute:{value:true,note:'Exactly one of redirect, respond, page, static, download, function, link, proxy, conditional or extension; middleware wraps it'},
50
+ oneHandlerPerRoute:{value:true,note:'Exactly one of redirect, respond, page, static, download, function, proxy, conditional or extension; middleware wraps it'},
51
51
  pathShape:{value:'exact or {param}',note:'No greedy captures or general-purpose wildcards; a segment is a literal or a named placeholder'},
52
52
  wildcardMounts:{value:false,note:'Only static and extension routes mount a subtree; nothing else matches below its path'},
53
53
  yamlInterpolation:{value:false,note:'No ${...} templating; bind typed inputs through parameters, args and context'},
@@ -79,7 +79,6 @@ export async function buildContext(project ,options ={})
79
79
  for(const name of handlerNames)handlers[name]=0;
80
80
  for(const name of policyNames)policyCounts[name]=0;
81
81
  if(Object.keys(document.extensions??{}).length)used.add('extension');
82
- if(document.dynamicLinks)used.add('dynamicLinks');
83
82
  for(const route of routes) {
84
83
  handlers[handlerOf(route)]=(handlers[handlerOf(route)]??0)+1;
85
84
  for(const capability of routeCapabilities(route,document))used.add(capability);
@@ -113,11 +112,11 @@ export async function buildContext(project ,options ={})
113
112
  project:{
114
113
  entry:'urlcode.yaml',routes:compiled.count,handlers,extensions:sorted(Object.keys(document.extensions??{})),
115
114
  policies:{project:policyNames.filter(name=>topLevel[name]),routes:policyCounts},
116
- bindings:{env:sorted(env),secrets:sorted(secrets)},dynamicLinks:document.dynamicLinks===true,site:sorted(Object.keys(document.site??{})),
115
+ bindings:{env:sorted(env),secrets:sorted(secrets)},site:sorted(Object.keys(document.site??{})),
117
116
  files:{includes:loaded.files.slice(1).map(file=>relative(loaded.root,file).split('\\').join('/')),functions:sorted(functions),middleware:sorted(middleware)},
118
117
  ...(options.hostFile===undefined?{}:{host:{extensions:sorted((host.extensions??[]).map(item=>item.name)),plugins:(host.plugins??[]).length}}),
119
118
  },
120
- routes:routes.map(route=>({path:route.pattern,methods:route.methods,handler:handlerOf(route)})).sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0),
119
+ routes:routes.map(route=>({path:route.pattern,methods:route.methods,handler:handlerOf(route),sandbox:route.sandbox===true,...(route.sandboxReason?{sandboxReason:route.sandboxReason}:{})})).sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0),
121
120
  constraints:{...constraints},
122
121
  targets,
123
122
  commands:{
@@ -138,7 +137,7 @@ const drops =[
138
137
  ['constraintNotes',context=>{for(const [key,item] of Object.entries(context.constraints))context.constraints[key]=typeof item==='object'?item.value:item;}],
139
138
  ['files',context=>{delete context.project.files;}],
140
139
  ['commands',context=>{delete context.commands;}],
141
- ['summary',context=>{context.project={entry:context.project.entry,routes:context.project.routes,handlers:context.project.handlers,extensions:[],policies:{project:[],routes:{}},bindings:{env:[],secrets:[]},dynamicLinks:context.project.dynamicLinks,site:[]};}],
140
+ ['summary',context=>{context.project={entry:context.project.entry,routes:context.project.routes,handlers:context.project.handlers,extensions:[],policies:{project:[],routes:{}},bindings:{env:[],secrets:[]},site:[]};}],
142
141
  ];
143
142
  function fitBudget(context ,budget ) {
144
143
  const omitted =[];
package/dist/examples.js CHANGED
@@ -21,7 +21,7 @@ import {assert} from './errors.js';
21
21
 
22
22
 
23
23
  // Fixed package data, like the recipe catalog: names come from here, metadata from each example.yaml.
24
- export const exampleNames=['assets','aws','cloudflare','compliance','conditions','cookbook','egress','extensions','live-links','monitoring','prerender','provider-conformance','tunnel','vercel'] ;
24
+ export const exampleNames=['assets','aws','cloudflare','compliance','conditions','cookbook','egress','extensions','monitoring','prerender','provider-conformance','tunnel','vercel'] ;
25
25
  export const routeIndexFile='route-index.json';
26
26
  const examplesRoot=fileURLToPath(new URL('../examples/',import.meta.url));
27
27
  const root=(name )=>examplesRoot+name+'/';
@@ -31,7 +31,7 @@ export async function listExamples() {
31
31
  for(const name of exampleNames)result.push(await metadata(name));
32
32
  return result;
33
33
  }
34
- const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond','link'] ;
34
+ const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond'] ;
35
35
  function handlerOf(route ) {return handlerNames.find(name=>route[name]!==undefined)??'unknown';}
36
36
  /**
37
37
  * Derives the per-route tag index of one project from its loaded routes: handler,
@@ -1,5 +1,5 @@
1
1
  import {ConfigError} from './errors.js';
2
- import {normalizeCapabilityTarget} from './capabilities.js';
2
+ import {capabilityTargets, normalizeCapabilityTarget} from './capabilities.js';
3
3
 
4
4
  import {explainRoute,explainProject} from './tooling.js';
5
5
 
@@ -8,11 +8,11 @@ import {buildManifest,renderManifest} from './manifest.js';
8
8
 
9
9
 
10
10
  const table=(rows ) =>{const widths=rows[0] .map((_,i)=>Math.max(...rows.map(row=>row[i] .length)));return rows.map(row=>row.map((cell,i)=>cell.padEnd(widths[i] )).join(' ').trimEnd()).join('\n')+'\n';};
11
- const targetsOf=(target ) =>target===undefined?['self-hosted','cloudflare','aws','vercel']:[normalizeCapabilityTarget(target)];
11
+ const targetsOf=(target ) =>target===undefined?[...capabilityTargets]:[normalizeCapabilityTarget(target)];
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)}`);
@@ -43,7 +45,7 @@ export async function runExplainCommand(command ,route
43
45
  if(options.json){print(renderManifest(manifest));return 0;}
44
46
  const lines=[`revision: ${manifest.revision}`,`urlcode: ${manifest.urlcode}`,`files: ${manifest.files.join(', ')}`,`routes: ${manifest.routeCount}`,`capabilities: ${manifest.capabilities.join(', ')}`,
45
47
  `extensions: ${Object.keys(manifest.extensions).join(', ')||'none'}`,`recipes: ${manifest.recipes.map(recipe=>recipe.id).join(', ')||'none'}`,
46
- `external: env ${manifest.external.env.join(', ')||'-'}; secrets ${manifest.external.secrets.join(', ')||'-'}; proxy ${manifest.external.egress.proxy.join(', ')||'-'}; signals ${manifest.external.egress.signals.join(', ')||'-'}; link stores ${manifest.external.linkStores.join(', ')||'-'}`,
48
+ `external: env ${manifest.external.env.join(', ')||'-'}; secrets ${manifest.external.secrets.join(', ')||'-'}; proxy ${manifest.external.egress.proxy.join(', ')||'-'}; signals ${manifest.external.egress.signals.join(', ')||'-'}`,
47
49
  `functions: ${manifest.functions.map(item=>`${item.source}#${item.export}`).join(', ')||'none'}`,`middleware: ${manifest.middleware.map(item=>`${item.source}#${item.export}`).join(', ')||'none'}`,
48
50
  `targets: ${Object.entries(manifest.targets).map(([target,support])=>`${target} ${support.compatible?'supported':`${support.issues} issue${support.issues===1?'':'s'}`}`).join('; ')}`,'Use --json for the full manifest.'];
49
51
  print(lines.join('\n')+'\n');return 0;
@@ -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
@@ -15,7 +15,7 @@ import {effectivePolicies} from './policies.js';
15
15
  // binding values are replaced by their names, module paths are made
16
16
  // project-relative and secrets never appear.
17
17
 
18
- const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond','link'] ;
18
+ const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond'] ;
19
19
 
20
20
 
21
21
 
@@ -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
 
@@ -57,7 +62,6 @@ function handlerOf(route ,root ) {
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}:{})};
59
64
  case 'respond':return {kind,status:route.reply?.status??route.respond .status??200};
60
- case 'link':return {kind,collection:route.link .collection};
61
65
  default:return {kind:'none'};
62
66
  }
63
67
  }
@@ -80,11 +84,11 @@ function providerOf(name ,requirement ,opt
80
84
  if(requirement)try{requirementValid=registration.policySchema?Boolean(new Ajv.default({strict:false,allErrors:false}).compile(registration.policySchema)(requirement)):false;}catch{requirementValid=false;}
81
85
  return {registered:true,version:registration.version,targets:[...registration.targets],revisionMatch:options.projectSha256===undefined?false:registration.projectSha256===options.projectSha256,requirementValid};
82
86
  }
83
- function targetsOf(loaded ,route ) {
87
+ function targetsOf(loaded ,route ,options ) {
84
88
  const table={exact:new Map([[route.pattern,route]]),byLength:new Map(),mounts:[],modules:[],count:1};
85
89
  const result={} ;
86
90
  for(const target of capabilityTargets){
87
- const report=analyzeCompiledCapabilities(loaded.document,table,target);
91
+ const report=analyzeCompiledCapabilities(loaded.document,table,target,options.extensions);
88
92
  const issues=report.issues.filter(issue=>issue.path===route.pattern).map(({capability,support,reason})=>({capability,support,reason}));
89
93
  result[target]={compatible:issues.length===0,issues};
90
94
  }
@@ -111,12 +115,13 @@ export function explainCompiledRoute(loaded ,route ,c
111
115
  state:routeState(route,options.now??Date.now()),enabled:route.enabled!==false,...(route.expires?{expires:route.expires}:{}),
112
116
  methods:[...route.methods],conditional:Boolean(route.match||route.conditional),handler,
113
117
  middleware:route.middleware.map(item=>({source:relativeSource(root,item.source),export:item.export})),
118
+ sandbox:route.sandbox===true,...(route.sandboxReason?{sandboxReason:route.sandboxReason}:{}),
114
119
  inputs:{parameters:route.parameters.map(({name,in:location,required,schema})=>({name,in:location,required,schema})),...(route.request?.body?{body:route.request.body}:{})},
115
120
  policies:{names,inventory,extensions},
116
121
  cache:cacheOf(route,chain,extensionNames),
117
122
  bindings:{env,secrets},egress,
118
123
  responseHeaders:route.responseHeaders.map(([name,value])=>[name,value]),
119
- capabilities:routeCapabilities(route,loaded.document),targets:targetsOf(loaded,route),
124
+ capabilities:routeCapabilities(route,loaded.document),targets:targetsOf(loaded,route,options),
120
125
  note:'Derived from the compiled configuration; request conditions, parameter values and handler execution are not evaluated.',
121
126
  };
122
127
  }