@jimhoyd/urlcode 0.5.6 → 0.5.8

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 (113) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +15 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +24 -14
  4. package/dist/BUILD-MANIFEST.json +50 -49
  5. package/dist/agents-guide.js +5 -2
  6. package/dist/assets.js +2 -2
  7. package/dist/authoring.js +3 -58
  8. package/dist/aws.js +21 -4
  9. package/dist/body-schema.js +2 -3
  10. package/dist/capability-query.js +3 -3
  11. package/dist/catalog.js +1 -1
  12. package/dist/cli.js +42 -15
  13. package/dist/cloudflare.js +26 -1
  14. package/dist/config.js +1 -1
  15. package/dist/context.js +6 -6
  16. package/dist/egress.js +1 -1
  17. package/dist/examples.js +2 -2
  18. package/dist/explain.js +4 -6
  19. package/dist/extension-artifacts.js +8 -8
  20. package/dist/extension-transport.js +6 -4
  21. package/dist/function-worker.js +5 -2
  22. package/dist/functions.js +1 -1
  23. package/dist/guest-api.js +22 -15
  24. package/dist/http-policy.js +1 -3
  25. package/dist/http-response.js +61 -10
  26. package/dist/init-with.js +8 -8
  27. package/dist/interchange.js +3 -2
  28. package/dist/logging.js +4 -4
  29. package/dist/match.js +1 -1
  30. package/dist/mcp-authoring.js +1 -2
  31. package/dist/mcp.js +2 -2
  32. package/dist/object-guards.js +17 -0
  33. package/dist/pattern-guard.js +33 -3
  34. package/dist/policies/cache.js +16 -4
  35. package/dist/policies/compression.js +1 -1
  36. package/dist/policies/throttle.js +1 -1
  37. package/dist/policy.js +3 -3
  38. package/dist/prerender.js +1 -2
  39. package/dist/project-dependencies.js +8 -8
  40. package/dist/proxy.js +1 -1
  41. package/dist/readiness.js +16 -17
  42. package/dist/review.js +2 -2
  43. package/dist/route-diff.js +4 -4
  44. package/dist/router.js +1 -1
  45. package/dist/runtime.js +4 -1
  46. package/dist/scaffold.js +1 -1
  47. package/dist/schema-query.js +1 -1
  48. package/dist/server.js +49 -7
  49. package/dist/signals.js +2 -2
  50. package/dist/site.js +2 -2
  51. package/dist/trusted-functions.js +1 -1
  52. package/dist/types/assets.d.ts +3 -2
  53. package/dist/types/authoring.d.ts +3 -6
  54. package/dist/types/body-schema.d.ts +2 -1
  55. package/dist/types/catalog.d.ts +2 -1
  56. package/dist/types/config.d.ts +0 -8
  57. package/dist/types/context.d.ts +3 -1
  58. package/dist/types/egress.d.ts +1 -1
  59. package/dist/types/explain.d.ts +4 -4
  60. package/dist/types/extension-artifacts.d.ts +4 -13
  61. package/dist/types/extension-transport.d.ts +6 -4
  62. package/dist/types/functions.d.ts +1 -1
  63. package/dist/types/http-response.d.ts +7 -3
  64. package/dist/types/init-with.d.ts +3 -9
  65. package/dist/types/logging.d.ts +4 -3
  66. package/dist/types/match.d.ts +2 -1
  67. package/dist/types/object-guards.d.ts +7 -0
  68. package/dist/types/pattern-guard.d.ts +0 -1
  69. package/dist/types/policies/cache.d.ts +4 -3
  70. package/dist/types/policies/compression.d.ts +1 -1
  71. package/dist/types/policies/throttle.d.ts +1 -1
  72. package/dist/types/policy.d.ts +4 -3
  73. package/dist/types/project-dependencies.d.ts +1 -40
  74. package/dist/types/proxy.d.ts +2 -1
  75. package/dist/types/readiness.d.ts +12 -16
  76. package/dist/types/review.d.ts +0 -2
  77. package/dist/types/route-diff.d.ts +4 -3
  78. package/dist/types/router.d.ts +1 -1
  79. package/dist/types/server.d.ts +22 -0
  80. package/dist/types/signals.d.ts +3 -2
  81. package/dist/types/site.d.ts +1 -2
  82. package/dist/types/trusted-functions.d.ts +1 -1
  83. package/dist/types/types.d.ts +17 -11
  84. package/dist/types/vercel.d.ts +1 -0
  85. package/dist/types/verify-deployment.d.ts +1 -1
  86. package/dist/types.js +18 -11
  87. package/dist/vercel.js +12 -3
  88. package/dist/verify-deployment.js +29 -11
  89. package/docs/AI-AUTHORING.md +9 -0
  90. package/docs/FUNCTION-SECURITY.md +4 -0
  91. package/docs/README.md +2 -2
  92. package/docs/TOOLING.md +27 -1
  93. package/examples/assets/Makefile +1 -1
  94. package/examples/compliance/rules.mjs +2 -2
  95. package/llms-full.txt +44 -13
  96. package/llms.txt +9 -2
  97. package/package.json +4 -3
  98. package/skills/urlcode/SKILL.md +6 -6
  99. package/starters/default/.github/workflows/urlcode.yml +2 -1
  100. package/starters/default/AGENTS.md +3 -3
  101. package/starters/default/Makefile +1 -1
  102. package/starters/default/README.md +13 -11
  103. package/starters/default/tests/requests.json +0 -50
  104. package/starters/default/urlcode.yaml +1 -4
  105. package/dist/scripts/operational-drills.js +0 -33
  106. package/starters/default/functions/hello.mjs +0 -3
  107. package/starters/default/middleware/headers.mjs +0 -6
  108. package/starters/default/routes/functions.yaml +0 -20
  109. package/starters/default/routes/marketing/links.yaml +0 -7
  110. package/starters/page/README.md +0 -14
  111. package/starters/page/public/index.html +0 -12
  112. package/starters/page/tests/requests.json +0 -17
  113. package/starters/page/urlcode.yaml +0 -6
package/dist/vercel.js CHANGED
@@ -12,6 +12,17 @@ import { assert, HttpError } from './errors.js';
12
12
 
13
13
  const platformOrigins = ['VERCEL_PROJECT_PRODUCTION_URL','VERCEL_URL','VERCEL_BRANCH_URL'];
14
14
 
15
+ // `x-forwarded-for` is documented as overwritten (not appended to) by
16
+ // Vercel's edge, but only when Vercel itself is the client-facing proxy; a
17
+ // project's own proxy in front of Vercel can still set it before Vercel ever
18
+ // sees the request. `x-vercel-forwarded-for` is Vercel's own copy of the same
19
+ // value and is the header Vercel's docs say to prefer for exactly that reason
20
+ // (https://vercel.com/docs/headers/request-headers#x-vercel-forwarded-for).
21
+ // Exported so the adapter's client-IP resolution can be verified directly.
22
+ export function forwardedClient(headers , headerCounts ) {
23
+ return headerCounts['x-vercel-forwarded-for'] === 1 ? (headers.get('x-vercel-forwarded-for') ?? '').split(',')[0]?.trim() || undefined : undefined;
24
+ }
25
+
15
26
  function readBody(req , limit ) {
16
27
  if (req.headers['content-length'] && Number(req.headers['content-length']) > limit) return Promise.reject(new HttpError(413,'Request body too large'));
17
28
  return new Promise((resolve,reject) => {
@@ -49,9 +60,7 @@ export function createVercelHandler({ project = process.cwd(), origin, environme
49
60
  }
50
61
  const limit = Math.min(maxBodyBytes, runtime.requestLimit(target) ?? maxBodyBytes);
51
62
  const body = await readBody(req,limit);
52
- // The platform terminates TLS and sets the forwarded header itself, so
53
- // its leftmost entry is the client; the socket peer is the platform.
54
- const forwarded = headerCounts['x-forwarded-for'] === 1 ? (headers.get('x-forwarded-for') ?? '').split(',')[0]?.trim() : undefined;
63
+ const forwarded = forwardedClient(headers, headerCounts);
55
64
  const publicOrigin = resolveOrigin(origin,environment,platformOrigins) ?? 'http://localhost';
56
65
  const result = await runtime.handle({ target, method, headers, headerCounts, body,
57
66
  origin: publicOrigin, client: forwarded || req.socket?.remoteAddress });
@@ -3,6 +3,7 @@ import { request, Agent } from 'node:http';
3
3
  import { request as secureRequest, Agent as SecureAgent } from 'node:https';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { assert } from './errors.js';
6
+ import { isRecord } from './object-guards.js';
6
7
  import { createRuntime } from './runtime.js';
7
8
 
8
9
  import { benchmarkTarget, hit, readFixtures, runFixtures, isStepsFixture, probeAgent } from './readiness.js';
@@ -26,7 +27,7 @@ import { lists as bundledAgents } from '../data/agents/index.js';
26
27
  // snapshot would; it says nothing about the host, the proxy or the network.
27
28
 
28
29
 
29
-
30
+
30
31
 
31
32
 
32
33
 
@@ -46,32 +47,49 @@ const MAX_REQUESTS = 10000;
46
47
  const BODY_LIMIT = 1048576;
47
48
  const SNIPPET = 200;
48
49
  const siteTypes = { 'site.robots': 'text/plain', 'site.sitemap': 'application/xml', 'site.securityTxt': 'text/plain', 'site.llms': 'text/plain', 'site.notFound': 'text/html' };
49
- const isRecord = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
50
50
  const snippet = (body ) => body.length > SNIPPET ? `${body.length} bytes: ${body.subarray(0, SNIPPET).toString('utf8')}` : body.toString('utf8');
51
51
  const tlsCode = /CERT|TLS|SSL|SELF_SIGNED/;
52
52
 
53
53
  // One request, one answer. Bodies are read up to 1 MiB and never logged
54
54
  // beyond the snippet a failing assertion carries. Redirects are not followed
55
55
  // and a transport error is reported as text, never thrown.
56
+ //
57
+ // `timeout` on a Node request/response is an *idle* timer: it only fires
58
+ // once a socket has gone quiet, so a deployment that drips one byte just
59
+ // under that interval keeps resetting it and can hold this probe open
60
+ // indefinitely. A single wall-clock deadline covering the whole request
61
+ // (connect through body) closes that gap. Past `BODY_LIMIT` the response is
62
+ // destroyed rather than left to keep streaming into a discard loop: this
63
+ // probe only ever needs a bounded snippet, never the rest of an oversized
64
+ // or endless body.
56
65
  function probe(target , { path, method = 'GET', headers = {} } , agent , timeoutMs ) {
57
66
  return new Promise(resolve => {
58
- const fail = (error ) => {
67
+ let settled = false;
68
+ const finish = (answer ) => { if (settled) return; settled = true; clearTimeout(deadline); resolve(answer); };
69
+ const fail = (error ) => {
59
70
  const code = isRecord(error) && typeof error.code === 'string' ? error.code : error instanceof Error ? error.message : 'transport';
60
- resolve({ status: 0, headers: {}, body: Buffer.alloc(0), error: code });
71
+ finish({ status: 0, headers: {}, body: Buffer.alloc(0), error: code });
61
72
  };
62
73
  let req ;
74
+ const deadline = setTimeout(() => { req?.destroy(new Error('timeout')); }, timeoutMs);
63
75
  try {
64
76
  const options = { host: target.hostname, port: target.port, path, method, agent, timeout: timeoutMs,
65
77
  headers: { host: target.hostname, 'user-agent': probeAgent, 'accept-encoding': 'identity', ...headers } };
66
78
  req = (target.protocol === 'https:' ? secureRequest : request)(options, (res ) => {
79
+ const out = {};
80
+ for (const [key, value] of Object.entries(res.headers)) if (value !== undefined) out[key] = Array.isArray(value) ? value.join(', ') : value;
67
81
  const chunks = []; let size = 0;
68
- res.on('data', (chunk ) => { if (size < BODY_LIMIT) chunks.push(chunk); size += chunk.length; });
69
- res.on('error', fail);
70
- res.on('end', () => {
71
- const out = {};
72
- for (const [key, value] of Object.entries(res.headers)) if (value !== undefined) out[key] = Array.isArray(value) ? value.join(', ') : value;
73
- resolve({ status: res.statusCode ?? 0, headers: out, body: Buffer.concat(chunks) });
82
+ const done = () => finish({ status: res.statusCode ?? 0, headers: out, body: Buffer.concat(chunks) });
83
+ res.on('data', (chunk ) => {
84
+ if (size < BODY_LIMIT) chunks.push(chunk);
85
+ size += chunk.length;
86
+ if (size > BODY_LIMIT) res.destroy();
74
87
  });
88
+ res.on('error', fail);
89
+ res.on('end', done);
90
+ // A body cut off by the BODY_LIMIT destroy() above ends here, not on
91
+ // 'end'; the snippet already collected is still a valid answer.
92
+ res.on('close', () => { if (!settled) done(); });
75
93
  });
76
94
  req.on('error', fail); req.on('timeout', () => req?.destroy(new Error('timeout'))); req.end();
77
95
  } catch (error) { req?.destroy(); fail(error); }
@@ -148,7 +166,7 @@ export async function verifyDeployment(project , { target, origin, expect
148
166
  const parse = (answer ) => { try { const json = JSON.parse(answer.body.toString('utf8')); return isRecord(json) ? json : undefined; } catch { return undefined; } };
149
167
  const healthJson = parse(health);
150
168
  check(health.status === 200 && healthJson?.status === 'ok' && typeof healthJson.version === 'string' && typeof healthJson.routes === 'number',
151
- { check: 'probes', severity: 'high', message: '/_urlcode/health must answer 200 with {status:"ok",version,routes}', expected: '200', observed: `${health.status} ${snippet(health.body)}` });
169
+ { check: 'probes', severity: 'high', message: '/_urlcode/health must answer 200 with {status:"ok",version,routes}; start the target with --health-details (or --metrics) if it currently answers {status} only', expected: '200', observed: `${health.status} ${snippet(health.body)}` });
152
170
  const ready = await send({ path: '/_urlcode/ready' }), readyJson = parse(ready);
153
171
  check(ready.status === 200 && readyJson?.status === 'ok', { check: 'probes', severity: 'high', message: '/_urlcode/ready must answer 200 (the deployment reports itself degraded or is not URLCode)', expected: '200', observed: `${ready.status} ${snippet(ready.body)}` });
154
172
  if (typeof readyJson?.version === 'string') version.observed = readyJson.version;
@@ -63,6 +63,11 @@ The root [llms.txt](../llms.txt) is a compact discovery index; the generated
63
63
  reading order for agents that want complete context in one fetch. It is a convenience,
64
64
  not a runtime protocol or a guarantee that AI clients automatically consume it.
65
65
  The generated reference is checked against the schema in `npm run verify`.
66
+ [URLCode AI](https://urlcode.ai/) is the optional hosted companion for shared
67
+ skills and LLM tooling. It complements the project-local MCP server; see
68
+ [hosted AI MCP setup](TOOLING.md#optional-hosted-ai-mcp) for its authenticated
69
+ remote connection details. Its machine-readable entry point is
70
+ [`https://urlcode.ai/llms.txt`](https://urlcode.ai/llms.txt).
66
71
 
67
72
  Follow [organization and readability practices](BEST-PRACTICES.md): preserve local
68
73
  conventions, use clear names, keep middleware focused and avoid needless layers.
@@ -432,6 +437,10 @@ the operator, never by tool arguments; `--allow-authoring` on the operator's
432
437
  command line adds project-confined route, recipe, scaffold and runner tools.
433
438
  `urlcode init` writes `.mcp.json` so Claude Code and Codex register the read-only
434
439
  server for the project ([registering the server](TOOLING.md#registering-the-server)).
440
+ The optional hosted URLCode AI MCP is a separate authenticated connection for
441
+ shared skills and LLM tools; it does not replace the local project server. Its
442
+ endpoint and credential-handling requirements are in
443
+ [hosted AI MCP setup](TOOLING.md#optional-hosted-ai-mcp).
435
444
  Inspection is not activation/deployment readiness: real grants, asset snapshots
436
445
  and service availability still need normal runtime checks. Provider conformance replay is local evidence; only
437
446
  explicit live [deployment observations](PROVIDER-VERIFICATION.md) test ingress.
@@ -82,6 +82,10 @@ into it; only the default for routes that declare neither option has changed.
82
82
  - A fresh guest heap/module state per invocation prevents state crossing requests.
83
83
  - 32 MiB guest heap, 512 KiB stack, source/input/output/header limits, bounded
84
84
  concurrency, guest interruption and an independent worker termination deadline.
85
+ - The guest's result is recorded where guest code cannot rewrite it, and the
86
+ host checks its shape before trusting it. A result that states a body length
87
+ for any method other than HEAD is invalid and answers 502; the runtime frames
88
+ the response by the bytes it sends ([responses](HTTP.md#responses)).
85
89
  - External bindings are denied by default. Project YAML cannot self-authorize.
86
90
  Operator grants are exact-name, route-scoped and pinned to configuration/source.
87
91
 
package/docs/README.md CHANGED
@@ -8,7 +8,7 @@ Use documentation from the same pinned revision as your runtime.
8
8
  | Understand what URLCode does | [Framework](FRAMEWORK.md) |
9
9
  | Build my first project | [Installation](INSTALL.md), then [YAML guide](YAML-GUIDE.md) |
10
10
  | Build a site with UI, accounts and admin | [Composing a site](COMPOSING-A-SITE.md) |
11
- | Have an AI author a project | [AI authoring](AI-AUTHORING.md), [agent index](../llms.txt) |
11
+ | Have an AI author a project | [AI authoring](AI-AUTHORING.md), [agent index](../llms.txt), [hosted agent guide](https://urlcode.ai/llms.txt) |
12
12
  | Deploy and operate a project | [Operations](OPERATIONS.md) |
13
13
  | Contribute to URLCode | [Contributing](../CONTRIBUTING.md), [local development](LOCAL-DEVELOPMENT.md) |
14
14
 
@@ -26,7 +26,7 @@ The [specification](SPECIFICATION.md) owns implemented semantics; the
26
26
  | Load authoring/operations rules into an agent | [Authoring skill](../.claude/skills/urlcode-authoring/SKILL.md), [operations skill](../.claude/skills/urlcode-operations/SKILL.md), [how they are distributed](AI-AUTHORING.md#agent-skills) |
27
27
  | Understand exact behavior | [Specification](SPECIFICATION.md), [routing](ROUTING.md), [HTTP](HTTP.md) |
28
28
  | Run examples | [Executable cookbook](../examples/cookbook/README.md), [prerender recipe](../examples/prerender/README.md), [small starter](STARTERS.md) |
29
- | Let an AI build routes | [The framework](FRAMEWORK.md), [AI authoring guide](AI-AUTHORING.md), [llms.txt](../llms.txt), [SDK and read-only MCP](TOOLING.md) |
29
+ | Let an AI build routes | [The framework](FRAMEWORK.md), [AI authoring guide](AI-AUTHORING.md), [llms.txt](../llms.txt), [hosted agent guide](https://urlcode.ai/llms.txt), [SDK and read-only MCP](TOOLING.md) |
30
30
  | Reuse code around routes | [Middleware](MIDDLEWARE.md), [middleware examples](MIDDLEWARE-EXAMPLES.md) |
31
31
  | Handle secrets and decide what to sandbox | [Function security](FUNCTION-SECURITY.md) |
32
32
  | Author guest functions in TypeScript | [Build-time guest transpilation](TYPESCRIPT-AUTHORING.md) |
package/docs/TOOLING.md CHANGED
@@ -330,7 +330,7 @@ choices added by hand, never by `init` or by an agent.
330
330
 
331
331
  - **Claude Code** reads `.mcp.json` in the project directory as a project-scoped
332
332
  server and asks for approval on first use. A project that pins the runtime in its
333
- `package.json` (`--template redirects`, `--with`, `--manifest`) gets
333
+ `package.json` (`--with`, `--manifest`) gets
334
334
  `"command": "npx"` with `--no --package @jimhoyd/urlcode urlcode mcp ...`, which runs the
335
335
  installed copy and never fetches (do not use a bare `npx urlcode`: that names an unrelated
336
336
  registry package). A project without one keeps the bare `urlcode` command for a global
@@ -354,6 +354,32 @@ The generated `AGENTS.md` and the packaged skill tell agents to prefer
354
354
  `get_manifest` when the server is registered and to fall back to the matching
355
355
  CLI commands otherwise.
356
356
 
357
+ ## Optional hosted AI MCP
358
+
359
+ The local `urlcode mcp` server remains the project-aware URLCode integration:
360
+ it reads the selected checkout, validates its configuration and never needs a
361
+ network credential. Do not replace its generated `.mcp.json` entry with a
362
+ hosted service.
363
+
364
+ [URLCode AI](https://urlcode.ai/) is a separate, opt-in hosted service for
365
+ shared skills and LLM-assisted work. A client that supports authenticated HTTP
366
+ MCP can add it as a second server with these connection details:
367
+
368
+ - URL: `https://mcp.urlcode.ai/mcp`
369
+ - request header: `Authorization: Bearer <URLCODE_AI_TOKEN>`
370
+
371
+ Store `URLCODE_AI_TOKEN` in the MCP client's secret or environment-variable
372
+ facility. Do not put a literal bearer token in `.mcp.json`, `urlcode.yaml`, a
373
+ checked-in client configuration, or a shell history. Each client has its own
374
+ remote-server configuration syntax, so configure that endpoint explicitly in
375
+ the client rather than asking `urlcode init` to generate it.
376
+
377
+ The hosted tools are not a proxy for this local server: they do not receive the
378
+ project root and do not replace local `get_context`, validation, manifest,
379
+ extension-artifact or authoring tools. Keep the local server registered for
380
+ framework- and project-specific work; add the hosted server only where its
381
+ shared skill catalog or LLM tools are useful.
382
+
357
383
  ## Authoring mode
358
384
 
359
385
  `urlcode mcp --allow-authoring --project DIR` adds six tools to the twenty-two read
@@ -1,6 +1,6 @@
1
1
  .DEFAULT_GOAL := help
2
2
 
3
- # Install the runtime separately, or override: URLCODE='node /path/to/urlcode/src/cli.js'
3
+ # Install the runtime separately, or override: URLCODE='node /path/to/urlcode/dist/cli.js'
4
4
  URLCODE ?= urlcode
5
5
  HOST ?= 127.0.0.1
6
6
  PORT ?= 3000
@@ -1,7 +1,7 @@
1
1
  // Operator compliance rules for `urlcode audit --compliance-rules`.
2
- // Runnable against the cookbook from the runtime checkout:
2
+ // Runnable against the cookbook from the runtime checkout (after `npm run build`):
3
3
  //
4
- // node src/cli.js audit --project examples/cookbook \
4
+ // node dist/cli.js audit --project examples/cookbook \
5
5
  // --compliance baseline --compliance-rules "$PWD/examples/compliance/rules.mjs" --compliance-warn
6
6
  //
7
7
  // The file must sit outside the audited project (it is trusted host code, the
package/llms-full.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  <!-- Generated by scripts/build-llms-full.ts (npm run docs:llms). Do not edit; edit the source documents. -->
2
- <!-- Consolidated URLCode authoring reference: 16 documents, about 61,257 tokens (estimate: characters / 4). -->
2
+ <!-- Consolidated URLCode authoring reference: 16 documents, about 61,970 tokens (estimate: characters / 4). -->
3
3
 
4
4
  <!-- urlcode-current-version:start -->
5
5
  # URLCode
@@ -7,7 +7,7 @@
7
7
  > A portable runtime for programmable URL behavior, and the framework that grows
8
8
  > from it: routes in YAML, functions and middleware, then accounts, administration and
9
9
  > stored links as operator-installed extensions. Stable project format
10
- > `version: "1"`. Core is Apache-2.0; this revision is `0.5.6`. `function`/`middleware`
10
+ > `version: "1"`. Core is Apache-2.0; this revision is `0.5.8`. `function`/`middleware`
11
11
  > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
12
12
  > auth, admin, ui and store workspace packages are bundle sources. New sites install
13
13
  > core from npm and verified executable extensions from an immutable GitHub Release;
@@ -114,8 +114,10 @@ Each rung's YAML is valid on every rung above it.
114
114
 
115
115
  Stored short links previously sat here as a native `link` route; that handler
116
116
  was removed from core. A `urlcode-dynamic-link` package owned them the same way
117
- `auth`/`admin` own their mounts, but it has been retired and unpublished; no
118
- package occupies this rung today.
117
+ `auth`/`admin` own their mounts, but it has been retired and unpublished. It has
118
+ no direct successor; a project that wants stored short links declares a
119
+ collection through the `store` extension above (see [docs/STORE.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STORE.md))
120
+ rather than a native `link` route.
119
121
 
120
122
  Rungs 1 to 3 need only the core package. Rungs 4 to 7 need a verified extension
121
123
  bundle installed into an explicit operator host, once its source package appears
@@ -364,6 +366,11 @@ The root [llms.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms.txt) i
364
366
  reading order for agents that want complete context in one fetch. It is a convenience,
365
367
  not a runtime protocol or a guarantee that AI clients automatically consume it.
366
368
  The generated reference is checked against the schema in `npm run verify`.
369
+ [URLCode AI](https://urlcode.ai/) is the optional hosted companion for shared
370
+ skills and LLM tooling. It complements the project-local MCP server; see
371
+ [hosted AI MCP setup](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md#optional-hosted-ai-mcp) for its authenticated
372
+ remote connection details. Its machine-readable entry point is
373
+ [`https://urlcode.ai/llms.txt`](https://urlcode.ai/llms.txt).
367
374
 
368
375
  Follow [organization and readability practices](https://github.com/jimhoyd-com/urlcode/blob/main/docs/BEST-PRACTICES.md): preserve local
369
376
  conventions, use clear names, keep middleware focused and avoid needless layers.
@@ -733,6 +740,10 @@ the operator, never by tool arguments; `--allow-authoring` on the operator's
733
740
  command line adds project-confined route, recipe, scaffold and runner tools.
734
741
  `urlcode init` writes `.mcp.json` so Claude Code and Codex register the read-only
735
742
  server for the project ([registering the server](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md#registering-the-server)).
743
+ The optional hosted URLCode AI MCP is a separate authenticated connection for
744
+ shared skills and LLM tools; it does not replace the local project server. Its
745
+ endpoint and credential-handling requirements are in
746
+ [hosted AI MCP setup](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md#optional-hosted-ai-mcp).
736
747
  Inspection is not activation/deployment readiness: real grants, asset snapshots
737
748
  and service availability still need normal runtime checks. Provider conformance replay is local evidence; only
738
749
  explicit live [deployment observations](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PROVIDER-VERIFICATION.md) test ingress.
@@ -773,7 +784,7 @@ programmatic compatibility analysis and provider verification limits.
773
784
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md
774
785
 
775
786
  <!-- urlcode-current-version:start -->
776
- This guide targets URLCode 0.5.6. Start with the function example below,
787
+ This guide targets URLCode 0.5.8. Start with the function example below,
777
788
  then add only the fields your route needs. The authoritative machine-readable
778
789
  shape is [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json); semantic rules are in the
779
790
  [specification](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md). Unsupported fields fail validation.
@@ -795,8 +806,9 @@ node packages/core/src/cli.ts dev --project examples/cookbook
795
806
  ```
796
807
 
797
808
  The cookbook is a larger learning project. The normal `urlcode init ../my-links`
798
- remains a small two-route starter. For an independent application with a pinned
799
- runtime dependency, clone [urlcode-template](https://github.com/jimhoyd-com/urlcode-template).
809
+ creates a bare zero-route scaffold; add only the routes the application needs.
810
+ For an independent application with a pinned runtime dependency, clone
811
+ [urlcode-template](https://github.com/jimhoyd-com/urlcode-template).
800
812
 
801
813
  ### Pages
802
814
 
@@ -1920,10 +1932,18 @@ values into header literals. Header configuration applies to handler responses,
1920
1932
  including declared error statuses, but not runtime validation/errors (400, 404,
1921
1933
  405, 413, 415, 500, etc.). Defaults remain `no-store`, `nosniff` and a request ID.
1922
1934
  Header policy is bounded to 64 keys/16 KiB; merged function headers remain bounded.
1935
+ A header repeated in the result the runtime writes (from a policy, or from a
1936
+ handler result that carries more than one pair for the same name) is sent as
1937
+ separate wire lines, the same as a declared `Set-Cookie` list; it is never
1938
+ collapsed to only its last value.
1923
1939
 
1924
1940
  Framing, hop-by-hop headers, Location, Allow, range/cache validators,
1925
1941
  Content-Encoding, X-Request-ID and X-Content-Type-Options are reserved to the
1926
- runtime/handler. Configure redirect URLs/status on `redirect`; asset content type,
1942
+ runtime/handler. The runtime frames every response the same way on every host:
1943
+ Content-Length is the UTF-8 byte length of the body it sends, whatever length a
1944
+ handler states. Only a HEAD answer carries a stated length, the one GET would
1945
+ send, and no body. The self-hosted and Vercel writers also make Node refuse a
1946
+ body that differs from the stated length. Configure redirect URLs/status on `redirect`; asset content type,
1927
1947
  cache and disposition on its own handler. Asset metadata cannot be overridden by
1928
1948
  `response.headers`. On functions/declared responses, Content-Type may be configured;
1929
1949
  JSON declarations require a JSON type. No response header secret interpolation.
@@ -2393,9 +2413,11 @@ cannot enforce`; the Cloudflare build reports
2393
2413
  build time and carried in the Worker artifact; the Worker has no filesystem and
2394
2414
  no Node imports, so only modules free of both qualify. The Worker's client
2395
2415
  identity is the platform's `cf-connecting-ip`; the serverless adapters use the
2396
- platform-set source address (`sourceIp` on Lambda, the leftmost
2397
- `X-Forwarded-For` entry the platform writes on Vercel). None of these read a
2398
- forwarded header a client could have set.
2416
+ platform-set source address (`sourceIp` on Lambda, Vercel's own
2417
+ `X-Vercel-Forwarded-For` Vercel's documented copy of the client IP that
2418
+ survives even when a project puts another proxy in front of Vercel, unlike
2419
+ plain `X-Forwarded-For`, which that outer proxy can overwrite before Vercel
2420
+ ever sees it). None of these read a forwarded header a client could have set.
2399
2421
 
2400
2422
  The cross-request state a policy keeps (throttle counters, the origin cache) is
2401
2423
  per runtime instance on every target, never shared between replicas or
@@ -2760,6 +2782,11 @@ portable scalar header contract. Upstream redirects are returned as responses;
2760
2782
  Location is forwarded only if explicitly selected and is never fetched. Headers
2761
2783
  nominated by an incoming or upstream Connection field are removed even if selected.
2762
2784
  Header names are normalized to lowercase; array-valued upstream fields are omitted.
2785
+ A proxy route reads incoming headers from the same credential-free projection a
2786
+ trusted function receives: a header an installed plugin or extension declares
2787
+ as a credential is withheld before `requestHeaders` selection runs, even when a
2788
+ project's own `requestHeaders` names it explicitly, so a route cannot forward a
2789
+ credential header upstream by declaring it.
2763
2790
  An encoded request body requires explicitly selecting its Content-Encoding
2764
2791
  header; literal header injection cannot change or replace that coding. Bodies
2765
2792
  remain raw bytes, including content encoding; select Content-Encoding when
@@ -3673,9 +3700,9 @@ navigation or labels to an auth/admin view without editing either package.
3673
3700
 
3674
3701
  | Hook | Input | Returns | Called |
3675
3702
  |---|---|---|---|
3676
- | `beforeRoleChange` | `{accountId, currentRoles, requestedRoles, actorId, reason}` | `{allow: boolean, reason?}` | Before roles are applied, after the administrator's permission check. A veto means the auth service is never asked. |
3703
+ | `beforeRoleChange` | `{accountId, currentRoles, requestedRoles, actorId, reason}` | `{allow: boolean, reason?}` | Before roles are applied, after the administrator's permission check, on every path that assigns roles: `/users/roles`, bulk role assignment through `/account-operations`, and a role-change support case's creation and approval. A veto means the auth service is never asked; for a bulk assignment it runs once per affected account before anything is staged. |
3677
3704
  | `onRegistrationApproved` | `{requestId, accountId, email, actorId, reason}` | ignored | After a registration request is approved. |
3678
- | `onAccountStatusChanged` | `{accountId, status, actorId, reason}` | ignored | After an account is locked or unlocked. |
3705
+ | `onAccountStatusChanged` | `{accountId, status, actorId, reason}` | ignored | After an account is locked or unlocked, on every path: `/users/status`, `/users/bulk` (once per affected account), and an approved lock/unlock support case. |
3679
3706
 
3680
3707
  #### Verdicts and failure
3681
3708
 
@@ -3816,6 +3843,10 @@ into it; only the default for routes that declare neither option has changed.
3816
3843
  - A fresh guest heap/module state per invocation prevents state crossing requests.
3817
3844
  - 32 MiB guest heap, 512 KiB stack, source/input/output/header limits, bounded
3818
3845
  concurrency, guest interruption and an independent worker termination deadline.
3846
+ - The guest's result is recorded where guest code cannot rewrite it, and the
3847
+ host checks its shape before trusting it. A result that states a body length
3848
+ for any method other than HEAD is invalid and answers 502; the runtime frames
3849
+ the response by the bytes it sends ([responses](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#responses)).
3819
3850
  - External bindings are denied by default. Project YAML cannot self-authorize.
3820
3851
  Operator grants are exact-name, route-scoped and pinned to configuration/source.
3821
3852
 
package/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
  > A portable runtime for programmable URL behavior, and the framework that grows
5
5
  > from it: routes in YAML, functions and middleware, then accounts, administration and
6
6
  > stored links as operator-installed extensions. Stable project format
7
- > `version: "1"`. Core is Apache-2.0; this revision is `0.5.6`. `function`/`middleware`
7
+ > `version: "1"`. Core is Apache-2.0; this revision is `0.5.8`. `function`/`middleware`
8
8
  > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
9
9
  > auth, admin, ui and store workspace packages are bundle sources. New sites install
10
10
  > core from npm and verified executable extensions from an immutable GitHub Release;
@@ -21,9 +21,16 @@ grants. Unsupported features fail with the route named; nothing degrades silentl
21
21
  1. Run `npx --no --package @jimhoyd/urlcode urlcode context --project DIR` (MCP `get_context`). For redirect-only work run
22
22
  `npx --no --package @jimhoyd/urlcode urlcode context --project DIR --task redirects`: supported shapes with exact YAML, gaps
23
23
  with their exact validation error, a paste-ready `starter`, and this project's own redirects.
24
- 2. After installing `@jimhoyd/urlcode`, run `npx --no --package @jimhoyd/urlcode urlcode init . --template redirects` (it works in a directory holding only `package.json`, `package-lock.json`, `node_modules` or `.git`, and merges `scripts.start` into your `package.json`): yaml, 404 page, `npm start` honoring `PORT`, fixtures. Otherwise write `urlcode.yaml`, then run `npx --no --package @jimhoyd/urlcode urlcode validate --local --project DIR` and `npx --no --package @jimhoyd/urlcode urlcode test`. `--no` refuses a registry fetch, so it always uses the installed scoped runtime.
24
+ 2. After installing `@jimhoyd/urlcode`, run `npx --no --package @jimhoyd/urlcode urlcode init .` (it works in a directory holding only `package.json`, `package-lock.json`, `node_modules` or `.git` and preserves existing package metadata). It writes a bare agent-ready scaffold: `urlcode.yaml`, a minimal 404 fixture, `AGENTS.md`, local `.mcp.json`, and project CI. Then run `npx --no --package @jimhoyd/urlcode urlcode validate --local --project DIR` and `npx --no --package @jimhoyd/urlcode urlcode test`. `--no` refuses a registry fetch, so it always uses the installed scoped runtime.
25
25
  3. Do not read or grep [llms-full.txt](llms-full.txt) (about 60k tokens) for a routine task; it is the
26
26
  deliberate fallback when the steps above do not answer the question.
27
+ 4. Hosted skills and LLM assistance are optional: connect a compatible HTTP MCP
28
+ client to `https://mcp.urlcode.ai/mcp` with
29
+ `Authorization: Bearer <URLCODE_AI_TOKEN>`. Keep `urlcode mcp` registered
30
+ for project-aware inspection and validation, and store the hosted token in
31
+ the client's secret facility rather than in project files. Start with the
32
+ hosted [agent guide](https://urlcode.ai/llms.txt), then see
33
+ [hosted AI MCP setup](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md#optional-hosted-ai-mcp).
27
34
 
28
35
  | Shape | Supported? | Use |
29
36
  |---|---|---|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jimhoyd/urlcode",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Portable runtime for programmable URL behavior",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -75,6 +75,7 @@
75
75
  "files": [
76
76
  ".claude/skills",
77
77
  "dist",
78
+ "!dist/scripts",
78
79
  "schemas",
79
80
  "data",
80
81
  "starters",
@@ -155,9 +156,9 @@
155
156
  "@changesets/cli": "3.0.3",
156
157
  "@eslint/js": "10.0.1",
157
158
  "@types/mime-types": "3.0.1",
158
- "@types/node": "26.5.1",
159
+ "@types/node": "26.6.2",
159
160
  "@types/semver": "7.8.0",
160
- "eslint": "10.10.0",
161
+ "eslint": "10.11.0",
161
162
  "globals": "17.12.0",
162
163
  "semver": "7.8.5",
163
164
  "typescript": "6.0.3",
@@ -12,24 +12,23 @@ A URLCode project declares URL behavior in YAML; the installed runtime serves it
12
12
  > Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
13
13
 
14
14
  Check installed primitives, YAML configuration, policies, extensions and recipes/templates before writing custom code. Keep it focused and report the gap; never invent fields or bypass target limits or operator grants. Source checkouts have `docs/PROJECT-DIRECTION.md`; npm installations have it in `llms-full.txt`.
15
-
16
15
  ## 1. Recognize the project
17
16
 
17
+ - No `urlcode.yaml` yet? `npm install @jimhoyd/urlcode` (scoped; no unscoped `urlcode` package exists), then `npx urlcode init .`.
18
18
  - The root has `urlcode.yaml` with `version: "1"`. Included route files are
19
- listed under `includes`; functions, middleware and assets are referenced from
20
- the project root.
19
+ listed under `includes`; functions, middleware and assets are project-relative.
21
20
  - Read the project's `AGENTS.md` first if present; it lists the handlers,
22
21
  policies and commands this runtime version supports.
23
22
  - Find the runtime: `urlcode` on the PATH, or
24
23
  `node node_modules/@jimhoyd/urlcode/dist/cli.js`, or
25
24
  `node /path/to/urlcode/packages/core/src/cli.ts` for a source checkout. Use one form for
26
25
  every command below.
27
-
28
26
  ## 2. Retrieve the minimum, do not read everything
29
27
 
30
28
  **First step: one bounded query.** MCP `get_context` when the `urlcode` server is registered, otherwise `urlcode context --project DIR` (add `--budget N` to cap it). It returns a compact summary, constraints and exact commands, never a schema dump. Then retrieve only what the task needs: `capabilities NAME`/`get_capability` (its limits), `get_schema`, `recipes search`/`search_recipes`, `explain`, and `get_extensions` when an operator host file exists. The bare `urlcode capabilities` and `recipes list` catalogs, `llms.txt` and `llms-full.txt` are deliberate fallback/reference, not the opening move.
31
29
 
32
30
  If the project carries `.mcp.json` (written by `urlcode init`) and your client has the `urlcode` server, prefer its tools: `get_context` (project summary, constraints, exact commands), `get_capability` and `get_schema` (one capability or YAML fragment), `search_recipes`, `search_examples`, `explain` (a route's effective behavior) and `get_manifest`. For framework discovery, use `list_skills` before `get_skill`, `search_docs` for a short package-owned excerpt, and `get_example` for one runnable example. Use `validate_yaml` for pasted YAML syntax/schema feedback only; use `validate` for the actual project. The server is read-only; `--allow-authoring` is an operator opt-in you never add yourself.
31
+ [URLCode AI](https://urlcode.ai/) is an optional, separate hosted service for shared skills and LLM tooling. Its remote MCP supplements this local project server; never replace `.mcp.json` or put its bearer token in project files. Its machine-readable entry point is `https://urlcode.ai/llms.txt`; connection details belong to the MCP client's secret facility and are documented in the URLCode tooling guide.
33
32
  When the MCP server was started with an operator host file, `get_extensions`
34
33
  returns installed extension configuration/policy schemas, declared project
35
34
  hook contracts, supported authoring surfaces and fast checks. Otherwise use `urlcode extensions --project DIR --host-file
@@ -118,8 +117,9 @@ urlcode audit --project DIR --expect-routes N
118
117
  ```
119
118
 
120
119
  Add fixtures to `tests/requests.json` for each new route: positive and negative
121
- cases, every active method, `HEAD`. Update `N` deliberately when routes are
122
- added or removed, and update any `--expect-routes` in the project's README,
120
+ cases, every active method, `HEAD`. `N` is declared routes plus one per active
121
+ `site.*` convention; an audit mismatch reports the declared/generated split. Update it
122
+ deliberately when routes are added or removed, and update any `--expect-routes` in the project's README,
123
123
  Makefile or CI workflow to match. A failing validation names the route; fix
124
124
  the declaration rather than working around it.
125
125
 
@@ -19,5 +19,6 @@ jobs:
19
19
  # Pin to a release tag or commit of the runtime you depend on.
20
20
  - uses: jimhoyd-com/urlcode/action@main
21
21
  with:
22
- expect-routes: 2 # update when adding or removing routes
22
+ expect-routes: 0 # update when adding or removing routes
23
+ allow-empty-project: true # remove after adding the first active route
23
24
  compliance: baseline
@@ -22,7 +22,7 @@ static serving and authentication. Read this file before changing anything.
22
22
 
23
23
  When present, `.mcp.json` registers the read-only `urlcode mcp` server; prefer its
24
24
  tools (also `get_manifest`) to reading documents. Inspect `get_extensions` before
25
- replacing extension behavior. `--allow-authoring` is an operator opt-in; never add it. For a committed artifact lock, use `get_extension_artifacts`/`get_extension_artifact`; they expose verified inert data and never activate an extension.
25
+ replacing extension behavior. `--allow-authoring` is an operator opt-in; never add it. For a committed artifact lock, use `get_extension_artifacts`/`get_extension_artifact`; they expose verified inert data and never activate an extension. [URLCode AI](https://urlcode.ai/) is a separate optional hosted service for shared skills and LLM tooling; its remote MCP never replaces this local project server, and its credential belongs only in a client secret facility, never project files. Its machine-readable entry point is `https://urlcode.ai/llms.txt`.
26
26
 
27
27
  ## What the runtime provides (this version)
28
28
 
@@ -56,10 +56,10 @@ record the reason in `sandboxReason`. Try `redirect` (relative or `/**`) or `res
56
56
  ```sh
57
57
  urlcode validate --local
58
58
  urlcode test
59
- urlcode audit --expect-routes 2
59
+ urlcode audit --expect-routes 0
60
60
  ```
61
61
 
62
- Run all three after every change, updating the route count deliberately and adding `tests/requests.json` fixtures for every new route (positive/negative, every active method, HEAD). No global install: use `node /path/to/urlcode/packages/core/src/cli.ts`.
62
+ With no active routes, this initial audit intentionally exits nonzero with `no-active-routes`. Add the first route and its fixture, then make the audit pass; remove `allow-empty-project: true` from the generated GitHub workflow at that point. `N` counts declared routes plus one route for each active `site.*` convention; an audit mismatch reports the declared/generated split. Update it deliberately and add `tests/requests.json` fixtures for every new route (positive/negative, every active method, HEAD). No global install: use `node /path/to/urlcode/packages/core/src/cli.ts`.
63
63
 
64
64
  ## Feedback
65
65
 
@@ -1,6 +1,6 @@
1
1
  .DEFAULT_GOAL := help
2
2
 
3
- # Install the runtime separately, or override: URLCODE='node /path/to/urlcode/src/cli.js'
3
+ # Install the runtime separately, or override: URLCODE='node /path/to/urlcode/dist/cli.js'
4
4
  URLCODE ?= urlcode
5
5
  HOST ?= 127.0.0.1
6
6
  PORT ?= 3000
@@ -1,29 +1,31 @@
1
1
  # Your URLCode project
2
2
 
3
- One starter: a URL that runs your function, and a regular redirect.
4
- Created with `urlcode init ../my-links`. No template choice is needed.
3
+ This is a bare, agent-ready URLCode scaffold. It starts with no routes so your
4
+ application's YAML and tests describe only the behavior you intend to ship. Its
5
+ single fixture proves the empty project returns 404.
5
6
 
6
7
  ```sh
7
8
  urlcode dev
8
9
  # In another terminal:
9
10
  urlcode test
10
- urlcode audit --expect-routes 2
11
- urlcode benchmark --requests 1000 --concurrency 2
12
11
  ```
13
12
 
14
- Visit http://127.0.0.1:3000/hello/Ada or http://127.0.0.1:3000/go.
15
- `urlcode.yaml` loads `routes/functions.yaml` and `routes/marketing/links.yaml`.
16
- Organize those files however you like; references are relative to the project root.
17
- The function route uses `middleware/headers.mjs` to add a response header around
18
- `await next()`. Edit or reuse it on other routes as needed.
19
- GET/HEAD, redirect 302 and no-store defaults keep YAML short. Valid edits reload.
13
+ `urlcode audit --expect-routes 0` intentionally reports `no-active-routes` until
14
+ you add the first route. The included GitHub workflow permits only that initial
15
+ audit result; remove `allow-empty-project: true` after adding a route.
16
+
17
+ Start with the local MCP `get_context` tool (or `urlcode context --project .`),
18
+ then add the smallest declarative route or custom code the task requires. Keep
19
+ `tests/requests.json` aligned with every route you add and update the audit
20
+ count deliberately. `AGENTS.md` explains the workflow and points to the
21
+ optional hosted shared tooling at https://urlcode.ai/llms.txt.
20
22
 
21
23
  This app uses the runtime you installed separately (compatible with 0.1.0).
22
24
  Without a global install, invoke `node /path/to/urlcode/packages/core/src/cli.ts` instead of
23
25
  `urlcode`. Optional Make shortcuts accept `URLCODE='node /path/to/urlcode/packages/core/src/cli.ts'`.
24
26
  For a cloneable project with a pinned npm runtime dependency, use
25
27
  [urlcode-template](https://github.com/jimhoyd-com/urlcode-template). Both start
26
- with the same two route examples. No runtime fork or provider account is needed.
28
+ with the same bare scaffold. No runtime fork or provider account is needed.
27
29
 
28
30
  Keep this app in your own Git repository. Secrets stay in ignored `.env.local`
29
31
  or provider environment values, with external operator policy for function grants.
@@ -1,56 +1,6 @@
1
1
  [
2
- {
3
- "path": "/hello/Ada",
4
- "status": 200,
5
- "expectBody": "{\"message\":\"Hello, Ada!\"}",
6
- "expectHeaders": {
7
- "x-powered-by": "URLCode",
8
- "cache-control": "no-store",
9
- "x-example-middleware": "active"
10
- }
11
- },
12
- {
13
- "path": "/hello/Ada",
14
- "method": "HEAD",
15
- "status": 200,
16
- "expectBody": "",
17
- "expectHeaders": {
18
- "x-example-middleware": "active"
19
- }
20
- },
21
- {
22
- "path": "/go",
23
- "status": 302,
24
- "expectHeaders": {
25
- "location": "https://example.com/"
26
- }
27
- },
28
- {
29
- "path": "/go",
30
- "method": "POST",
31
- "status": 405,
32
- "expectHeaders": {
33
- "allow": "GET, HEAD"
34
- }
35
- },
36
2
  {
37
3
  "path": "/missing",
38
4
  "status": 404
39
- },
40
- {
41
- "path": "/hello/Ada",
42
- "method": "POST",
43
- "status": 405,
44
- "expectHeaders": {
45
- "allow": "GET, HEAD"
46
- }
47
- },
48
- {
49
- "path": "/hello/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
50
- "status": 400
51
- },
52
- {
53
- "path": "/hello/%ZZ",
54
- "status": 400
55
5
  }
56
6
  ]