@jimhoyd/urlcode 0.4.2 → 0.4.6

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 (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +19 -15
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +30 -27
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +28 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +60 -2
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +43 -5
  86. package/package.json +15 -22
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -149
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -153
  151. package/docs/README.md +0 -93
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/dist/readiness.js CHANGED
@@ -27,12 +27,27 @@ import { runCompliance } from './compliance.js';
27
27
 
28
28
 
29
29
 
30
+
31
+
30
32
 
33
+ /** Where a captured value comes from: a dotted path into a JSON response body, or one response header. */
34
+
35
+ /** A step that closes and restarts the runtime on the same project and data directory. */
36
+
37
+ /** An ordered fixture: requests that share captured values, optionally with restarts between them. */
38
+
39
+
40
+ export const isStepsFixture = (fixture ) => 'steps' in fixture;
41
+ export const isRestartable = (app ) => typeof (app ).restart === 'function';
42
+ const isRestart = (step ) => 'restart' in step;
31
43
 
32
-
44
+ /** `captured` holds values from a step's `capture`; callers use it for substitution only and never print it. */
45
+
33
46
 
34
47
  /** A started server as the audit and benchmark see it. structural: the real type is startServer's result in src/server.ts. */
35
48
 
49
+ /** An app that can also close and restart itself on the same project and data directory (fixture `restart` steps). */
50
+
36
51
 
37
52
 
38
53
 
@@ -40,9 +55,18 @@ import { runCompliance } from './compliance.js';
40
55
 
41
56
 
42
57
 
43
-
44
-
58
+
59
+
45
60
 
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
46
70
 
47
71
 
48
72
 
@@ -70,7 +94,7 @@ function routeAdvisories(route ) {
70
94
  const advisories = [];
71
95
  const runsCode = Boolean(route.function) || Boolean(route.middleware?.length);
72
96
  if (runsCode && route.methods.includes('POST') && route.request?.body && !route.sandbox && !route.sandboxReason) {
73
- advisories.push("This route accepts POST with a declared request.body policy but declares neither sandbox: true nor sandboxReason; consider whether this route needs sandbox: true.");
97
+ advisories.push("This route accepts POST with a declared request.body policy but declares neither sandbox: true nor sandboxReason; consider whether this route needs sandbox: true. If it is trusted first-party code (anything that touches the filesystem must be trusted; a sandbox has no filesystem), add to the route: sandboxReason: \"Reviewed first-party code; trusted deliberately.\" If it isolates untrusted input, add sandbox: true and a sandboxReason saying why.");
74
98
  }
75
99
  return advisories;
76
100
  }
@@ -80,6 +104,7 @@ export function projectPlan(compiled )
80
104
  const inventory = routes.map(route => { const advisories = routeAdvisories(route); return { path:route.pattern, handler:handlers.find(key => route[key]), methods:route.methods, middleware:route.middleware?.length || 0,
81
105
  policies:[...(route.policy ? Object.keys(route.policy.describe) : []),...(route.extensionPolicyNames??[]).map(name=>`extensions.${name}`)],
82
106
  sandbox:route.sandbox === true, ...(route.sandboxReason ? { sandboxReason:route.sandboxReason } : {}),
107
+ ...(route.coveredElsewhere ? { coveredElsewhere:route.coveredElsewhere } : {}),
83
108
  ...(route.generated ? { generated:route.generated } : {}),
84
109
  ...(advisories.length ? { advisories } : {}),
85
110
  state:route.enabled === false ? 'disabled' : route.expiresAt && now >= route.expiresAt ? 'expired' : 'active' }; });
@@ -112,7 +137,33 @@ export function projectPlan(compiled )
112
137
  }
113
138
  return {inventory,cases,resolve:path => matchRoute(compiled,parseTarget(path))?.route.pattern};
114
139
  }
115
- export async function readCases(root , optional = false) {
140
+ // Bounds for ordered fixtures. A fixture file is data a project author (or an agent) wrote;
141
+ // none of these limits is a security boundary, they keep a mistake from becoming an unbounded run.
142
+ const MAX_STEPS = 50, MAX_RESTARTS = 5, MAX_TOTAL_RESTARTS = 20, MAX_CAPTURES = 16, MAX_CAPTURE_BYTES = 4096, MAX_CAPTURE_BODY = 1024 * 1024;
143
+ const nameShape = /^[A-Za-z_][A-Za-z0-9_]{0,31}$/, pathShape = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/, control = /[\x00-\x1f\x7f]/;
144
+ const templates = (text ) => [...text.matchAll(/\{\{([A-Za-z_][A-Za-z0-9_]{0,31})\}\}/g)].map(m => m[1] ?? '');
145
+ const templated = (test ) => [test.path, test.body, test.expectBody, ...Object.values(test.headers ?? {}), ...Object.values(test.expectHeaders ?? {})].filter((v) => v !== undefined);
146
+ function checkCase(test , inSteps ) {
147
+ assert(isRecord(test), 'Invalid request test');
148
+ // A step path may start with a {{name}} (a captured Location); it is checked again once filled in.
149
+ assert(typeof test.path === 'string' && (test.path.startsWith('/') || (inSteps && test.path.startsWith('{{'))) && !test.path.startsWith('//') && !/[\r\n]/.test(test.path), 'Test path must be local');
150
+ assert(Number.isInteger(test.status) && typeof test.status === 'number' && test.status >= 200 && test.status <= 599, 'Test must declare an HTTP status');
151
+ assert(!test.method || (typeof test.method === 'string' && ['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS'].includes(test.method)), 'Invalid test method');
152
+ assert(test.body === undefined || typeof test.body === 'string', 'Test body must be text');
153
+ assert(test.expectBody === undefined || typeof test.expectBody === 'string', 'Expected body must be text');
154
+ for (const headers of [test.headers,test.expectHeaders]) assert(headers === undefined || (isRecord(headers) && Object.values(headers).every(v => typeof v === 'string')), 'Test headers must be string mappings');
155
+ assert(inSteps || test.capture === undefined, 'capture is only valid inside steps');
156
+ if (test.capture !== undefined) {
157
+ assert(isRecord(test.capture) && Object.keys(test.capture).length <= MAX_CAPTURES, `capture must be a mapping of at most ${MAX_CAPTURES} names`);
158
+ for (const [name,spec] of Object.entries(test.capture)) {
159
+ assert(nameShape.test(name), 'Capture names use letters, digits and underscores (at most 32, not starting with a digit)');
160
+ assert(isRecord(spec) && Object.keys(spec).length === 1, 'A capture is exactly one of {json: "a.b.0.c"} or {header: "name"}');
161
+ if (typeof spec.json === 'string') assert(spec.json.length <= 256 && pathShape.test(spec.json), 'Capture json path is dotted keys and array indexes, such as items.0.id');
162
+ else assert(typeof spec.header === 'string' && /^[A-Za-z0-9-]{1,64}$/.test(spec.header), 'A capture is exactly one of {json: "a.b.0.c"} or {header: "name"}');
163
+ }
164
+ }
165
+ }
166
+ export async function readFixtures(root , optional = false) {
116
167
  if(optional) {
117
168
  try {await lstat(join(root,'tests/requests.json'));}
118
169
  catch(error){if(error instanceof Error && 'code' in error && error.code==='ENOENT')return [];throw error;}
@@ -122,16 +173,94 @@ export async function readCases(root , optional = false)
122
173
  assert(bytes.length <= 16*1024*1024, 'Request fixture file exceeds 16 MiB');
123
174
  const cases = JSON.parse(bytes.toString('utf8'));
124
175
  assert(Array.isArray(cases) && cases.length <= 10000 && (optional || cases.length), 'Request tests must be an array (maximum 10000)');
176
+ let requests = 0, restarts = 0;
125
177
  for (const test of cases ) {
126
- assert(isRecord(test), 'Invalid request test');
127
- assert(typeof test.path === 'string' && test.path.startsWith('/') && !test.path.startsWith('//') && !/[\r\n]/.test(test.path), 'Test path must be local');
128
- assert(Number.isInteger(test.status) && typeof test.status === 'number' && test.status >= 200 && test.status <= 599, 'Test must declare an HTTP status');
129
- assert(!test.method || (typeof test.method === 'string' && ['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS'].includes(test.method)), 'Invalid test method');
130
- assert(test.body === undefined || typeof test.body === 'string', 'Test body must be text');
131
- assert(test.expectBody === undefined || typeof test.expectBody === 'string', 'Expected body must be text');
132
- for (const headers of [test.headers,test.expectHeaders]) assert(headers === undefined || (isRecord(headers) && Object.values(headers).every(v => typeof v === 'string')), 'Test headers must be string mappings');
178
+ if (!(isRecord(test) && 'steps' in test)) { checkCase(test,false); requests++; continue; }
179
+ assert(Object.keys(test).length === 1 && Array.isArray(test.steps) && test.steps.length >= 1 && test.steps.length <= MAX_STEPS, `steps must be the only key and hold 1-${MAX_STEPS} steps`);
180
+ const known = new Set (); let here = 0;
181
+ for (const step of test.steps ) {
182
+ if (isRecord(step) && 'restart' in step) {
183
+ assert(step.restart === true && Object.keys(step).length === 1, 'A restart step is exactly {"restart": true}');
184
+ assert(++here <= MAX_RESTARTS && ++restarts <= MAX_TOTAL_RESTARTS, `At most ${MAX_RESTARTS} restarts per fixture and ${MAX_TOTAL_RESTARTS} per file`);
185
+ continue;
186
+ }
187
+ checkCase(step,true); requests++;
188
+ for (const name of templated(step).flatMap(templates)) assert(known.has(name), 'A {{name}} reference needs an earlier step in the same fixture to capture it');
189
+ for (const name of Object.keys(step.capture ?? {})) known.add(name);
190
+ }
133
191
  }
134
- return cases ; // trust boundary: fixture JSON, validated field by field above
192
+ assert(requests <= 10000, 'Request tests exceed 10000 requests in total');
193
+ return cases ; // trust boundary: fixture JSON, validated field by field above
194
+ }
195
+ /** Single-request fixtures only: the benchmark replays these and cannot run ordered steps. */
196
+ export async function readCases(root , optional = false) {
197
+ return (await readFixtures(root,optional)).filter((fixture) => !isStepsFixture(fixture));
198
+ }
199
+ /** One request a fixture run sent: `test` is the request as sent (captured values filled in), `original` as written. Print `original`, never `test`. */
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+ const fill = (text , values ) => {
209
+ let missing = false;
210
+ const out = text.replace(/\{\{([A-Za-z_][A-Za-z0-9_]{0,31})\}\}/g, (_, name ) => { const v = values.get(name); if (v === undefined) missing = true; return v ?? ''; });
211
+ return missing ? undefined : out;
212
+ };
213
+ function resolveStep(test , values ) {
214
+ const map = (headers ) => {
215
+ if (headers === undefined) return undefined;
216
+ const out = {};
217
+ for (const [k,v] of Object.entries(headers)) { const f = fill(v,values); if (f === undefined) return null; out[k] = f; }
218
+ return out;
219
+ };
220
+ const path = fill(test.path,values), body = test.body === undefined ? undefined : fill(test.body,values), expectBody = test.expectBody === undefined ? undefined : fill(test.expectBody,values);
221
+ const headers = map(test.headers), expectHeaders = map(test.expectHeaders);
222
+ if (path === undefined || !path.startsWith('/') || path.startsWith('//') || /[\r\n]/.test(path) || headers === null || expectHeaders === null || (test.body !== undefined && body === undefined) || (test.expectBody !== undefined && expectBody === undefined)) return undefined;
223
+ return { ...test, path, headers, body, expectHeaders, expectBody };
224
+ }
225
+ /** Runs every fixture in file order. A step after a failed step in the same fixture is reported failed with error `skipped` and never sent, so a broken chain cannot pass. */
226
+ export async function runFixtures(fixtures , host , visit , firstCase = 1) {
227
+ let n = firstCase;
228
+ for (const [f,fixture] of fixtures.entries()) {
229
+ if (!isStepsFixture(fixture)) { await visit({case:n++,fixture:f+1,test:fixture,original:fixture,result:await hit(host.app,fixture,host.agent,host.target)}); continue; }
230
+ if (fixture.steps.some(isRestart) && host.restart === undefined) {
231
+ const reason = 'contains a restart step, which needs a runtime this host can close and restart';
232
+ assert(host.skipped !== undefined, `Fixture ${f+1} ${reason}`);
233
+ host.skipped(f+1,reason); continue;
234
+ }
235
+ const values = new Map (); let broken = false;
236
+ for (const step of fixture.steps) {
237
+ if (isRestart(step)) { if (!broken) { try { await host.restart?.(); } catch { broken = true; } } continue; }
238
+ const resolved = broken ? undefined : resolveStep(step,values);
239
+ let result = {pass:false,status:0,durationMs:0,error:'skipped'};
240
+ if (resolved) {
241
+ const sent = await hit(host.app,resolved,host.agent,host.target);
242
+ if (sent.pass) for (const [name,value] of Object.entries(sent.captured ?? {})) values.set(name,value);
243
+ const {captured: _kept, ...visible} = sent; result = visible;
244
+ } else if (!broken) result = {pass:false,status:0,durationMs:0,error:'unresolved'};
245
+ if (!result.pass) broken = true;
246
+ await visit({case:n++,fixture:f+1,test:resolved ?? step,original:step,result});
247
+ }
248
+ }
249
+ }
250
+ function extract(spec , headers , body ) {
251
+ let value ;
252
+ if ('header' in spec) value = headers[spec.header.toLowerCase()];
253
+ else {
254
+ if (body.length > MAX_CAPTURE_BODY) return undefined;
255
+ try { value = JSON.parse(body.toString('utf8')); } catch { return undefined; }
256
+ for (const key of spec.json.split('.')) {
257
+ if (Array.isArray(value)) value = /^\d+$/.test(key) ? value[Number(key)] : undefined;
258
+ else if (isRecord(value) && Object.hasOwn(value,key)) value = value[key];
259
+ else return undefined;
260
+ }
261
+ }
262
+ const text = typeof value === 'string' ? value : (typeof value === 'number' && Number.isFinite(value)) || typeof value === 'boolean' ? String(value) : undefined;
263
+ return text !== undefined && text.length > 0 && Buffer.byteLength(text) <= MAX_CAPTURE_BYTES && !control.test(text) ? text : undefined;
135
264
  }
136
265
  export function benchmarkTarget(value ) {
137
266
  let url ;
@@ -152,10 +281,17 @@ export function hit(app ,test ,agent ,target
152
281
  : {host:'127.0.0.1',port:app.address.port,path:test.path,method:test.method || 'GET',headers:{'user-agent':probeAgent,...(test.headers || {})},agent,timeout:10000};
153
282
  req=send(options,(res )=>{
154
283
  let size=0;const chunks =[];
155
- res.on('data',(chunk )=>{size+=chunk.length;if(size>16*1024*1024)res.destroy(new Error('Response limit'));else if(test.expectBody!==undefined)chunks.push(chunk);});
284
+ res.on('data',(chunk )=>{size+=chunk.length;if(size>16*1024*1024)res.destroy(new Error('Response limit'));else if(test.expectBody!==undefined || (test.capture && size<=MAX_CAPTURE_BODY))chunks.push(chunk);});
156
285
  res.on('error',fail);
157
- res.on('end',()=>resolve({status:res.statusCode ?? 0,durationMs:performance.now()-began,
158
- pass:res.statusCode===test.status && Object.entries(test.expectHeaders || {}).every(([k,v])=>res.headers[k.toLowerCase()]===v) && (test.expectBody===undefined || Buffer.concat(chunks).toString()===test.expectBody)}));
286
+ res.on('end',()=>{
287
+ const status=res.statusCode ?? 0,durationMs=performance.now()-began,body=Buffer.concat(chunks);
288
+ let pass=status===test.status && Object.entries(test.expectHeaders || {}).every(([k,v])=>res.headers[k.toLowerCase()]===v) && (test.expectBody===undefined || body.toString()===test.expectBody);
289
+ if(!pass || !test.capture)return resolve({status,durationMs,pass});
290
+ // Values are kept only for later steps; a missing one fails the step without saying what the response held.
291
+ const captured ={};
292
+ for(const [name,spec] of Object.entries(test.capture)){const value=extract(spec,res.headers,body);if(value===undefined){pass=false;break;}captured[name]=value;}
293
+ resolve(pass?{status,durationMs,pass,captured}:{status,durationMs,pass,error:'capture'});
294
+ });
159
295
  });
160
296
  req.on('error',fail);req.on('timeout',()=>req?.destroy(new Error('Timeout')));req.end(test.body);
161
297
  } catch { req?.destroy();fail(); }
@@ -167,32 +303,48 @@ export function hit(app ,test ,agent ,target
167
303
  // folded into it: the exit code decision belongs to the caller.
168
304
  export async function auditProject(app , {expectRoutes,log=()=>{},compliance} = {}) {
169
305
  const began=performance.now();
170
- const plan=app.testPlan(), fixtures=await readCases(app.root,true);
306
+ const plan=app.testPlan(), fixtures=await readFixtures(app.root,true);
171
307
  const metadata=new Map(plan.inventory.map(r=>[r.path,r]));
172
- const cases=[...plan.cases,...fixtures], covered=new Set (), unassertedCases =[];let passed=0,failed=0;
308
+ const covered=new Set (), unassertedCases =[];let passed=0,failed=0,checks=0;
173
309
  const agent=new Agent({keepAlive:true,maxSockets:1});
310
+ // One accounting for generated cases and fixture steps, single or ordered: a step counts as
311
+ // a check, and covers a route/method only when it passes and asserts the response. Coverage
312
+ // uses the route the substituted path actually matched.
313
+ const record=(n ,test ,result ,source ) =>{
314
+ checks++;const method=test.method || 'GET';
315
+ let route ;try {route=plan.resolve(test.path);} catch { /* Invalid-path negative fixture. */ }
316
+ const meta=route===undefined?undefined:metadata.get(route);
317
+ // Error-only fixtures cannot prove a function's normal path works.
318
+ const assertsResponse=test.expectBody!==undefined || Object.keys(test.expectHeaders || {}).length>0;
319
+ if(result.pass && meta?.state==='active' && result.status<400 && !assertsResponse)unassertedCases.push(n);
320
+ if(result.pass && assertsResponse && meta?.state==='active' && (result.status<400 || (meta.handler==='respond' && source==='generated')))covered.add(JSON.stringify([route,method]));
321
+ if(result.pass)passed++;else failed++;
322
+ log({event:'check',case:n,source,pass:result.pass,status:result.status,expectedStatus:test.status});
323
+ };
174
324
  try {
175
- for (const [i,test] of cases.entries()) {
176
- const result=await hit(app,test,agent);const method=test.method || 'GET';
177
- let route ;try {route=plan.resolve(test.path);} catch { /* Invalid-path negative fixture. */ }
178
- const meta=route===undefined?undefined:metadata.get(route);
179
- // Error-only fixtures cannot prove a function's normal path works.
180
- const assertsResponse=test.expectBody!==undefined || Object.keys(test.expectHeaders || {}).length>0;
181
- if(result.pass && meta?.state==='active' && result.status<400 && !assertsResponse)unassertedCases.push(i+1);
182
- if(result.pass && assertsResponse && meta?.state==='active' && (result.status<400 || (meta.handler==='respond' && i<plan.cases.length)))covered.add(JSON.stringify([route,method]));
183
- if(result.pass)passed++;else failed++;
184
- log({event:'check',case:i+1,source:i<plan.cases.length?'generated':'fixture',pass:result.pass,status:result.status,expectedStatus:test.status});
185
- }
325
+ for (const [i,test] of plan.cases.entries()) record(i+1,test,await hit(app,test,agent),'generated');
326
+ const restart=isRestartable(app)?()=>app.restart():undefined;
327
+ await runFixtures(fixtures,{app,agent,restart},step=>record(step.case,step.test,step.result,'fixture'),plan.cases.length+1);
186
328
  } finally {agent.destroy();}
187
- const uncovered=plan.inventory.filter(r=>r.state==='active').flatMap(r=>r.methods.filter(m=>!covered.has(JSON.stringify([r.path,m]))).map(method=>({route:r.path,method})));
188
- const counts ={configured:plan.inventory.length,active:0,disabled:0,expired:0,byHandler:{}};
329
+ const missing=plan.inventory.filter(r=>r.state==='active').flatMap(r=>r.methods.filter(m=>!covered.has(JSON.stringify([r.path,m]))).map(method=>({route:r.path,method})));
330
+ const waivedRouteMethods =[],ignoredWaivers =[],redundantWaivers =[];
331
+ const uncovered=missing.filter(({route,method})=>{
332
+ const reason=metadata.get(route)?.coveredElsewhere?.[method];
333
+ if(reason===undefined)return true;
334
+ // A waiver excuses a missing fixture only where the route is otherwise shown to work normally.
335
+ const proven=[...covered].some(key=>(JSON.parse(key) )[0]===route);
336
+ (proven?waivedRouteMethods:ignoredWaivers).push({route,method,reason});
337
+ return !proven;
338
+ });
339
+ for(const r of plan.inventory)if(r.state==='active')for(const [method,reason] of Object.entries(r.coveredElsewhere??{}))if(covered.has(JSON.stringify([r.path,method])))redundantWaivers.push({route:r.path,method,reason});
340
+ const counts ={configured:plan.inventory.length,declared:plan.inventory.filter(r=>!r.generated).length,generated:plan.inventory.filter(r=>r.generated).length,active:0,disabled:0,expired:0,byHandler:{}};
189
341
  for(const route of plan.inventory){counts[route.state]++;const handler=String(route.handler);counts.byHandler[handler]=(counts.byHandler[handler]||0)+1;}
190
342
  const countMatches=expectRoutes===undefined || counts.configured===expectRoutes;
191
343
  const advisories=plan.inventory.flatMap(route=>(route.advisories??[]).map(message=>({route:route.path,message})));
192
344
  // The per-route capability table: which policies apply and whether this
193
345
  // host enforces, compiles or delegates each one. Refusals never get here.
194
346
  const notReadyReasons=[...(counts.active>0?[]:['no-active-routes']),...(countMatches?[]:['route-count-mismatch']),...(failed?['failed-checks']:[]),...(uncovered.length?['uncovered-route-methods']:[])];
195
- return {elapsedMs:performance.now()-began,ready:!notReadyReasons.length,notReadyReasons,counts,expectedRoutes:expectRoutes ?? null,countMatches,checks:cases.length,passed,failed,coveredRouteMethods:covered.size,unassertedCases,uncovered,policies:plan.policies ?? {},compliance:compliance?await runCompliance(app,compliance):null,advisories};
347
+ return {elapsedMs:performance.now()-began,ready:!notReadyReasons.length,notReadyReasons,counts,expectedRoutes:expectRoutes ?? null,countMatches,checks,passed,failed,coveredRouteMethods:covered.size,unassertedCases,uncovered,waivedRouteMethods,ignoredWaivers,redundantWaivers,policies:plan.policies ?? {},compliance:compliance?await runCompliance(app,compliance):null,advisories};
196
348
  }
197
349
  export async function benchmarkProject(app ,{requests=1000,concurrency=2,maxP95Ms,seconds=30,warmup=0,target} ={}) {
198
350
  assert(Number.isInteger(requests)&&requests>=1&&requests<=100000,'Requests must be 1–100000');
package/dist/recipes.js CHANGED
@@ -12,7 +12,7 @@ import {assert} from './errors.js';
12
12
 
13
13
  // This fixed local catalog is trusted package data, never an executable registry:
14
14
  // names come from here, metadata from each recipe's schema-checked recipe.yaml.
15
- export const recipeNames=['redirect','json-api','typescript','middleware','health-page','static-plus-api','cors-api','webhook-receiver','contact-form','authenticated-json-api','protected-download'] ;
15
+ export const recipeNames=['redirect','json-api','typescript','middleware','health-page','static-page','static-plus-api','cors-api','webhook-receiver','contact-form','authenticated-json-api','protected-download','store-crud'] ;
16
16
  const recipesRoot=fileURLToPath(new URL('../recipes/',import.meta.url));
17
17
  const root=(name )=>recipesRoot+name+'/';
18
18
  async function metadata(name ) {const value=await readMetadata(root(name),name,'recipe.yaml');return {name,...value};}
package/dist/router.js CHANGED
@@ -7,6 +7,8 @@ import { normalizeMatch, assertDisjointMatches } from './conditions.js';
7
7
  import { effectivePolicies } from './policies.js';
8
8
  import { setImmediate as yieldTurn } from 'node:timers/promises';
9
9
  import { compileHttp } from './http-policy.js';
10
+ import { assertSafePattern, maxPatternInputLength } from './pattern-guard.js';
11
+ import { uuidFormat } from './body-schema.js';
10
12
  import Ajv from 'ajv/dist/2020.js';
11
13
  import { assert } from './errors.js';
12
14
  import { functionFile } from './config.js';
@@ -64,6 +66,7 @@ export async function compileRoutes(loaded , bindings
64
66
  const exact = new Map (), dynamic = [], mounts = [], modules = new Map ();
65
67
  // Node hands the CJS module.exports (the class) to a default import; TypeScript types it as the namespace, whose .default is the same class.
66
68
  const ajv = new Ajv.default({ strict: false, allErrors: false }), validators = new Map ();
69
+ ajv.addFormat('uuid', uuidFormat);
67
70
  for (const [pattern, config] of Object.entries(loaded.routes)) {
68
71
  if (++processed % 64 === 0) await yieldTurn();
69
72
  assert(performance.now()<deadline, 'Route compilation deadline exceeded');
@@ -78,6 +81,14 @@ export async function compileRoutes(loaded , bindings
78
81
  const { redirect: declaredRedirect, function: declaredFunction, ...declared } = config;
79
82
  const route = { ...declared, pattern, parts, names, specificity: parts.length - names.length,
80
83
  methods: config.methods || methodsDefault, parameters: [], env: dict(), secrets: dict(), responseHeaders: [], middleware: [] };
84
+ if (config.coveredElsewhere) {
85
+ const waived = Object.entries(config.coveredElsewhere);
86
+ assert(waived.length > 0, 'coveredElsewhere must name at least one method');
87
+ for (const [method, reason] of waived) {
88
+ assert(route.methods.includes(method), `coveredElsewhere names ${method}, which is not one of the route's methods`);
89
+ assert(typeof reason === 'string' && reason.trim().length > 0, `coveredElsewhere.${method} needs a non-empty reason`);
90
+ }
91
+ }
81
92
  compileHttp(route);
82
93
  if (config.match) route.match = normalizeMatch(config.match);
83
94
  if(config.extension){assert(!config.middleware?.length&&!config.parameters?.length&&!config.env&&!config.secrets,'Extension handlers cannot declare guest middleware, parameters or bindings');assert(pattern.endsWith('/*')&&!names.length&&pattern!=='/*','Extension handler requires a non-root literal /* mount');}
@@ -113,6 +124,12 @@ export async function compileRoutes(loaded , bindings
113
124
  assert(param.in !== 'path' || !own(schema, 'default'), 'Path parameters cannot have defaults');
114
125
  assert(!['minLength','maxLength'].some(k => own(schema,k)) || schema.type === 'string', 'String bounds require string type');
115
126
  assert(!['minimum','maximum'].some(k => own(schema,k)) || ['integer','number'].includes(schema.type), 'Numeric bounds require numeric type');
127
+ assert(!['pattern','format'].some(k => own(schema,k)) || schema.type === 'string', 'pattern and format require string type');
128
+ assert(!own(schema,'format') || schema.format === 'uuid', 'Unsupported parameter format (supported: uuid)');
129
+ if (own(schema,'pattern')) {
130
+ assert(typeof schema.pattern === 'string', 'pattern must be a string'); assertSafePattern(schema.pattern);
131
+ assert(typeof schema.maxLength === 'number' && schema.maxLength <= maxPatternInputLength, `pattern requires maxLength of at most ${maxPatternInputLength}`);
132
+ }
116
133
  assert(!own(schema,'maxItems') || schema.type === 'array', 'maxItems requires array type');
117
134
  const p = { ...param, name, required: param.required === true, validate: inputValidator(schema, ajv, validators) };
118
135
  if (own(schema, 'default')) assert(p.validate(schema.default), 'Invalid parameter default');
package/dist/runtime.js CHANGED
@@ -26,7 +26,7 @@ import { createObserverSink } from './observability.js';
26
26
 
27
27
  import { applySite } from './site.js';
28
28
 
29
-
29
+
30
30
 
31
31
 
32
32
 
@@ -48,7 +48,22 @@ import { applySite } from './site.js';
48
48
 
49
49
 
50
50
 
51
+
52
+
53
+
54
+
51
55
 
56
+ function withDataDirGrant(loaded , projectSha256 , given ) {
57
+ // An operator policy pinned to another revision stays as it is: it denies, exactly as it would without this option.
58
+ if (given && given.projectSha256 !== projectSha256) return given;
59
+ const routes = { ...(given?.routes ?? {}) };
60
+ for (const [pattern, route] of Object.entries(loaded.routes)) {
61
+ if (!Object.values(route.env || {}).some(ref => ref.env === 'URLCODE_DATA_DIR')) continue;
62
+ const grant = routes[pattern] ?? {};
63
+ routes[pattern] = { ...grant, env: [...new Set([...(grant.env ?? []), 'URLCODE_DATA_DIR'])] };
64
+ }
65
+ return { version: 1, projectSha256, routes };
66
+ }
52
67
  /** Per-request facts the host may read after handle() settles; never request text. */
53
68
 
54
69
 
@@ -93,7 +108,8 @@ export async function createRuntime(project , rawOptions
93
108
  const egressGrants=authorizeEgress(loaded,snapshot.projectSha256,options.permissions);
94
109
  const extensionPlan=prepareExtensions(loaded.document,loaded.routes,options.extensions,{origin:options.origin??'',target:options.target??'node',projectSha256:snapshot.projectSha256,root:loaded.root});
95
110
  const bindings = await loadBindings(loaded.root, options.local, options.environment);
96
- const compiled = await compileRoutes(loaded, bindings, options.permissions, snapshot.projectSha256, options.extensions);
111
+ const notFoundPage = loaded.document.site?.notFound !== undefined && loaded.document.site.notFound !== null;
112
+ const compiled = await compileRoutes(loaded, bindings, options.grantDataDir ? withDataDirGrant(loaded, snapshot.projectSha256, options.permissions) : options.permissions, snapshot.projectSha256, options.extensions);
97
113
  const routes = [...compiled.mounts, ...compiled.exact.values(), ...[...compiled.byLength.values()].flat()];
98
114
  const assets = await compileAssets(loaded.root, routes);
99
115
  // Host policies compile after assets so a policy can see what a route serves
@@ -195,7 +211,14 @@ export async function createRuntime(project , rawOptions
195
211
  try {
196
212
  const parsed = parseTarget(target);
197
213
  const match = matchRoute(compiled, parsed);
198
- if (!match) throw new HttpError(404, 'Not found');
214
+ if (!match) {
215
+ // site.notFound: answer an unmatched GET/HEAD with the configured page and status 404.
216
+ if (notFoundPage && (method === 'GET' || method === 'HEAD')) {
217
+ const page = await (this ).handle({ target: '/404.html', method, headers, headerCounts, trace: {}, origin, ...(client ? { client } : {}) });
218
+ return { ...page, status: 404 };
219
+ }
220
+ throw new HttpError(404, 'Not found');
221
+ }
199
222
  const { route, path } = match;
200
223
  // Configured pattern only; never the request path, query or parameter values.
201
224
  trace.route = route.pattern;
package/dist/scaffold.js CHANGED
Binary file
package/dist/server.js CHANGED
@@ -2,8 +2,9 @@ import http from 'node:http';
2
2
 
3
3
 
4
4
  import { randomUUID, createHash } from 'node:crypto';
5
- import { readdir, lstat } from 'node:fs/promises';
6
- import { join } from 'node:path';
5
+ import { readdir, lstat, mkdir, mkdtemp, rm } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join, resolve as resolvePath } from 'node:path';
7
8
  import { createRuntime } from './runtime.js';
8
9
 
9
10
  import { createJsonLogger } from './logging.js';
@@ -19,6 +20,12 @@ import { compileTrustedProxies, resolveClient } from './client-address.js';
19
20
 
20
21
 
21
22
 
23
+
24
+
25
+
26
+
27
+
28
+
22
29
 
23
30
 
24
31
 
@@ -78,7 +85,23 @@ function originForm(target ) {
78
85
  if (rest === undefined) return target;
79
86
  return rest === '' || rest.startsWith('?') ? '/' + rest : rest;
80
87
  }
81
- export async function startServer({ project = '.', host = '127.0.0.1', port = 3000, watch = false,
88
+ /** Starts the server. `port: 0` picks a free port, so this is also the in-process helper for tests:
89
+ * `startServer({ project, port: 0, local: true, isolateData: true })`. */
90
+ export async function startServer(options = {}) {
91
+ const { dataDir, isolateData, ...rest } = options;
92
+ if (dataDir === undefined && !isolateData) return startServerCore(rest);
93
+ assert(dataDir === undefined || !isolateData, 'Use dataDir or isolateData, not both');
94
+ assert(dataDir === undefined || (typeof dataDir === 'string' && dataDir !== '' && !dataDir.includes('\0')), 'Data directory must be a path');
95
+ const owned = dataDir === undefined;
96
+ const dir = owned ? await mkdtemp(join(tmpdir(), 'urlcode-data-')) : resolvePath(dataDir);
97
+ try {
98
+ if (!owned) await mkdir(dir, { recursive: true });
99
+ const app = await startServerCore({ ...rest, environment: { ...(rest.environment ?? process.env), URLCODE_DATA_DIR: dir }, grantDataDir: true });
100
+ if (!owned) return app;
101
+ return { ...app, close: async () => { try { await app.close(); } finally { await rm(dir, { recursive: true, force: true }); } } };
102
+ } catch (error) { if (owned) await rm(dir, { recursive: true, force: true }); throw error; }
103
+ }
104
+ async function startServerCore({ project = '.', host = '127.0.0.1', port = 3000, watch = false,
82
105
  local = false, log = createJsonLogger(),
83
106
  maxBodyBytes = 1048576, maxInFlightRequests = 64, maxInFlightHealthRequests = 16,
84
107
  requestLog = 'minimal', trustRequestId = false, origin, trustedProxies = [],
package/dist/site.js CHANGED
Binary file
package/dist/tooling.js CHANGED
@@ -68,7 +68,7 @@ export async function previewImport(options ) {return importR
68
68
  export async function previewExport(project ,format ,acceptProviderDifferences=false) {const loaded=await loadDocument(project);const {includes:_includes,...document}=loaded.document;return exportRoutes({format,document:{...document,routes:loaded.routes},acceptProviderDifferences});}
69
69
 
70
70
 
71
-
71
+
72
72
 
73
73
 
74
74
  /** Reports registered extension contracts against the project's declarations. Never activates an extension. */
@@ -85,6 +85,7 @@ export async function describeExtensions(project ,registrations
85
85
  credentialHeaders:Array.isArray(registration.credentialHeaders)?registration.credentialHeaders.map(String):[],
86
86
  schema:structuredClone(registration.schema??{}),policySchema:registration.policySchema?structuredClone(registration.policySchema):null,
87
87
  hooks:structuredClone(registration.hooks??[]) ,
88
+ authoring:registration.authoring?structuredClone(registration.authoring) :null,
88
89
  declared:Object.hasOwn(loaded.document.extensions??{},registration.name),revisionPinned:registration.projectSha256===projectSha256,
89
90
  mounts:mountsOf(registration.name),policyRoutes:policyRoutesOf(registration.name),
90
91
  }));
@@ -0,0 +1,44 @@
1
+ export declare function listSkills(): {
2
+ name: "urlcode";
3
+ description: "Author URLCode projects declaratively, retrieve only the required contract, and validate the result.";
4
+ }[];
5
+ export declare function getSkill(name: string): Promise<{
6
+ name: "urlcode";
7
+ description: "Author URLCode projects declaratively, retrieve only the required contract, and validate the result.";
8
+ content: string;
9
+ }>;
10
+ /** Deterministic lexical search over a deliberately small, agent-facing corpus. */
11
+ export declare function searchDocs(query: string): Promise<{
12
+ query: string;
13
+ results: {
14
+ id: "security" | "authoring" | "llms" | "yaml-reference" | "tooling";
15
+ title: "URLCode agent index" | "AI authoring" | "YAML reference" | "Tooling and local MCP" | "Function security";
16
+ summary: "Compact map of the framework, its declarative primitives and the minimum reference to load next." | "Declarative-first authoring workflow, retrieval order and framework constraints." | "Generated inventory of accepted URLCode YAML fields." | "Bounded local project inspection, validation and MCP tool behavior." | "Trusted versus sandboxed function behavior, bindings and operator grants.";
17
+ matched: string[];
18
+ excerpt: string;
19
+ }[];
20
+ }>;
21
+ /** Returns the two smallest high-value files of a fixed packaged example. */
22
+ export declare function getExample(name: string): Promise<{
23
+ metadata: import("./examples.ts").ExampleSummary;
24
+ content: Record<string, string>;
25
+ }>;
26
+ /** Validates only supplied YAML syntax and the versioned document schema. It never resolves includes or reads source files. */
27
+ export declare function validateYaml(text: string): {
28
+ valid: boolean;
29
+ scope: string;
30
+ version: "1";
31
+ routeCount: number;
32
+ error?: never;
33
+ } | {
34
+ valid: boolean;
35
+ scope: string;
36
+ error: string;
37
+ version?: never;
38
+ routeCount?: never;
39
+ };
40
+ /** Short deterministic remediation for common validator output; no model call or project read occurs. */
41
+ export declare function explainError(error: string): {
42
+ guidance: string;
43
+ nextTools: string[];
44
+ };
@@ -5,6 +5,8 @@ export interface InitOptions {
5
5
  * initialization stays the default: a project whose runtime is managed elsewhere gets no manifest at all.
6
6
  */
7
7
  manifest?: DependencySet | undefined;
8
+ /** `default` (function, middleware, redirect) or `page`: urlcode.yaml, public/index.html, a README and fixtures only. */
9
+ template?: 'default' | 'page' | undefined;
8
10
  }
9
- export declare function initProject(destination: string, { manifest }?: InitOptions): Promise<string>;
11
+ export declare function initProject(destination: string, { manifest, template }?: InitOptions): Promise<string>;
10
12
  export declare function addRedirect(project: string, destination: string, alias?: string | undefined): Promise<string>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The JSON Schema subset a route may declare for `request.body.schema`.
3
+ * It is interpreted here rather than compiled by Ajv so the same code runs on
4
+ * every host, including the Worker, with no code generation and no author
5
+ * regex outside `pattern-guard.ts`.
6
+ */
7
+ export interface BodySchema {
8
+ type?: 'object' | 'array' | 'string' | 'integer' | 'number' | 'boolean' | 'null';
9
+ properties?: Record<string, BodySchema>;
10
+ required?: string[];
11
+ additionalProperties?: boolean;
12
+ items?: BodySchema;
13
+ enum?: (string | number | boolean | null)[];
14
+ minLength?: number;
15
+ maxLength?: number;
16
+ pattern?: string;
17
+ format?: 'uuid';
18
+ minimum?: number;
19
+ maximum?: number;
20
+ minItems?: number;
21
+ maxItems?: number;
22
+ }
23
+ export declare const uuidFormat: RegExp;
24
+ /** Rejects, at load time, any schema outside the supported subset or its limits. */
25
+ export declare function assertBodySchema(schema: unknown): asserts schema is BodySchema;
26
+ /**
27
+ * One validation failure. `pointer` is an RFC 6901 pointer built only from names
28
+ * the schema declared (array positions appear as `[]`, not an index), `keyword`
29
+ * is the schema keyword that failed, and `expected` is the schema's own
30
+ * constraint. Nothing the client sent is ever placed in an issue.
31
+ */
32
+ export interface BodySchemaIssue {
33
+ pointer: string;
34
+ keyword: string;
35
+ message: string;
36
+ expected?: string | number | (string | number | boolean | null)[];
37
+ property?: string;
38
+ }
39
+ /** Structured failures for `value`; `checkBodySchema` renders the same list as text. */
40
+ export declare function bodySchemaIssues(schema: BodySchema, value: unknown, path?: string, issues?: BodySchemaIssue[], max?: number): BodySchemaIssue[];
41
+ /**
42
+ * Returns fixed-wording failures for `value`, each naming only a path the
43
+ * schema itself declared (array positions appear as `[]`). Nothing the client
44
+ * sent is echoed, so the answer stays safe to render as plain text.
45
+ */
46
+ export declare function checkBodySchema(schema: BodySchema, value: unknown): string[];
47
+ /** The plain-text line for an issue: array positions print as `[]` appended to the path, root as `/`. */
48
+ export declare const bodySchemaLine: (issue: BodySchemaIssue) => string;
49
+ /**
50
+ * Negotiation rule (conservative): the structured answer is sent only when the
51
+ * Accept header names application/json explicitly with q > 0 and no higher q
52
+ * for an explicit text/plain. Wildcard ranges, a missing header and everything
53
+ * else keep the plain-text answer, so curl, browsers and existing clients see
54
+ * no change.
55
+ */
56
+ export declare function prefersJson(accept: string | null | undefined): boolean;
57
+ /** Renders the JSON answer: never more than `maxIssueBytes`, dropping trailing issues and saying so. */
58
+ export declare function bodySchemaJson(issues: BodySchemaIssue[]): string;