@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
  2. package/.claude/skills/urlcode-operations/SKILL.md +16 -22
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +83 -80
  6. package/ROADMAP.md +61 -331
  7. package/SECURITY.md +23 -8
  8. package/dist/BUILD-MANIFEST.json +42 -47
  9. package/dist/agents-guide.js +18 -18
  10. package/dist/build-static.js +136 -0
  11. package/dist/capabilities.js +85 -31
  12. package/dist/capability-query.js +0 -1
  13. package/dist/cli.js +24 -32
  14. package/dist/compliance-rules/baseline.js +2 -10
  15. package/dist/compliance-rules/privacy.js +5 -16
  16. package/dist/compliance-rules/shared.js +0 -2
  17. package/dist/compliance.js +6 -8
  18. package/dist/config.js +15 -8
  19. package/dist/context.js +9 -10
  20. package/dist/examples.js +2 -2
  21. package/dist/explain-cli.js +7 -5
  22. package/dist/explain.js +10 -5
  23. package/dist/extensions.js +61 -2
  24. package/dist/function-sources.js +34 -2
  25. package/dist/function-worker.js +3 -1
  26. package/dist/functions.js +84 -13
  27. package/dist/guest-api.js +29 -3
  28. package/dist/index.js +3 -6
  29. package/dist/manifest.js +11 -7
  30. package/dist/mcp-authoring.js +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/observability.js +1 -21
  33. package/dist/policies/cache.js +8 -3
  34. package/dist/policies.js +3 -1
  35. package/dist/policy.js +28 -9
  36. package/dist/prerender.js +4 -0
  37. package/dist/project-tests.js +3 -3
  38. package/dist/readiness.js +35 -9
  39. package/dist/route-diff.js +12 -5
  40. package/dist/router.js +5 -7
  41. package/dist/runtime.js +77 -58
  42. package/dist/sandbox.js +48 -0
  43. package/dist/scaffold.js +0 -0
  44. package/dist/scripts/operational-drills.js +12 -54
  45. package/dist/server.js +3 -29
  46. package/dist/tooling.js +1 -1
  47. package/dist/trusted-functions.js +210 -0
  48. package/dist/types/build-static.d.ts +43 -0
  49. package/dist/types/capabilities.d.ts +14 -5
  50. package/dist/types/compliance-rules/shared.d.ts +0 -2
  51. package/dist/types/compliance.d.ts +0 -3
  52. package/dist/types/config.d.ts +2 -1
  53. package/dist/types/context.d.ts +2 -1
  54. package/dist/types/examples.d.ts +1 -1
  55. package/dist/types/explain.d.ts +6 -0
  56. package/dist/types/extensions.d.ts +55 -0
  57. package/dist/types/function-sources.d.ts +4 -0
  58. package/dist/types/functions.d.ts +48 -5
  59. package/dist/types/guest-api.d.ts +1 -0
  60. package/dist/types/index.d.ts +3 -6
  61. package/dist/types/manifest.d.ts +5 -3
  62. package/dist/types/observability.d.ts +1 -14
  63. package/dist/types/project-tests.d.ts +1 -2
  64. package/dist/types/readiness.d.ts +12 -3
  65. package/dist/types/router.d.ts +2 -1
  66. package/dist/types/runtime.d.ts +0 -27
  67. package/dist/types/sandbox.d.ts +12 -0
  68. package/dist/types/scaffold.d.ts +0 -2
  69. package/dist/types/server.d.ts +1 -4
  70. package/dist/types/tooling.d.ts +3 -3
  71. package/dist/types/trusted-functions.d.ts +29 -0
  72. package/dist/types/types.d.ts +22 -8
  73. package/dist/types/verify-deployment.d.ts +2 -2
  74. package/dist/types.js +25 -5
  75. package/dist/typescript-authoring.js +67 -18
  76. package/dist/verify-deployment.js +3 -3
  77. package/docs/AI-AUTHORING.md +109 -10
  78. package/docs/ASSETS.md +2 -1
  79. package/docs/AUTH-BACKUP.md +32 -0
  80. package/docs/AWS.md +4 -4
  81. package/docs/BEST-PRACTICES.md +17 -8
  82. package/docs/CAPABILITIES.md +30 -17
  83. package/docs/CAPACITY.md +128 -31
  84. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  85. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  86. package/docs/CI.md +8 -3
  87. package/docs/CLOUDFLARE.md +1 -2
  88. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  89. package/docs/COMPLIANCE.md +6 -9
  90. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  91. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  92. package/docs/EXTENSIONS.md +193 -11
  93. package/docs/FRAMEWORK.md +53 -33
  94. package/docs/FUNCTION-SECURITY.md +173 -32
  95. package/docs/INSTALL.md +0 -5
  96. package/docs/LOAD-TESTING.md +4 -4
  97. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  98. package/docs/MIDDLEWARE.md +27 -16
  99. package/docs/MONITORING.md +2 -19
  100. package/docs/OBSERVABILITY.md +7 -16
  101. package/docs/OPEN-DECISIONS.md +184 -0
  102. package/docs/OPERATIONAL-PROOF.md +26 -30
  103. package/docs/OPERATIONS.md +23 -32
  104. package/docs/POLICIES.md +27 -8
  105. package/docs/PRERENDER.md +25 -13
  106. package/docs/PROJECT-DIRECTION.md +35 -10
  107. package/docs/READINESS.md +6 -2
  108. package/docs/README.md +20 -41
  109. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  110. package/docs/RELEASE-READINESS.md +50 -30
  111. package/docs/RELEASE-SECURITY.md +72 -86
  112. package/docs/RESILIENCE.md +16 -15
  113. package/docs/ROUTING.md +7 -10
  114. package/docs/SANDBOX-REVIEW.md +19 -6
  115. package/docs/SCAFFOLDING.md +0 -2
  116. package/docs/SECURITY-AUDIT.md +42 -2
  117. package/docs/SPECIFICATION.md +63 -27
  118. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
  119. package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
  120. package/docs/SPIKE-CORE-LAYERING.md +368 -0
  121. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
  122. package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
  123. package/docs/STARTERS.md +1 -1
  124. package/docs/STATIC.md +105 -0
  125. package/docs/TOOLING.md +17 -12
  126. package/docs/TUNNELS.md +0 -3
  127. package/docs/TYPESCRIPT-AUTHORING.md +32 -12
  128. package/docs/TYPESCRIPT.md +25 -4
  129. package/docs/VERCEL.md +4 -5
  130. package/docs/VERSION-ALIGNMENT.md +46 -0
  131. package/docs/YAML-GUIDE.md +0 -3
  132. package/docs/YAML-REFERENCE.md +16 -6
  133. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  134. package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
  135. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
  136. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
  137. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  138. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  139. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  140. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
  141. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
  142. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  143. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
  144. package/docs/archive/README.md +27 -0
  145. package/docs/policies/agents.md +1 -1
  146. package/docs/policies/cache.md +13 -0
  147. package/docs/policies/compression.md +3 -2
  148. package/docs/policies/security.md +3 -2
  149. package/examples/assets/example.yaml +2 -2
  150. package/examples/aws/example.yaml +1 -1
  151. package/examples/cloudflare/example.yaml +1 -1
  152. package/examples/conditions/example.yaml +1 -1
  153. package/examples/cookbook/example.yaml +1 -1
  154. package/examples/cookbook/middleware/bucket.mjs +12 -2
  155. package/examples/cookbook/middleware/locale.mjs +7 -3
  156. package/examples/cookbook/route-index.json +1 -1
  157. package/examples/cookbook/routes/middleware.yaml +1 -1
  158. package/examples/egress/example.yaml +1 -1
  159. package/examples/extensions/example.yaml +1 -1
  160. package/examples/prerender/README.md +14 -6
  161. package/examples/prerender/example.yaml +1 -1
  162. package/examples/prerender/functions/page.mjs +4 -2
  163. package/examples/prerender/middleware/template.mjs +1 -1
  164. package/examples/prerender/prerender.mjs +1 -1
  165. package/examples/prerender/urlcode.yaml +8 -4
  166. package/examples/provider-conformance/example.yaml +1 -1
  167. package/examples/vercel/example.yaml +1 -1
  168. package/llms-full.txt +686 -239
  169. package/llms.txt +27 -15
  170. package/package.json +32 -5
  171. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  172. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
  173. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
  174. package/recipes/authenticated-json-api/README.md +4 -3
  175. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  176. package/recipes/authenticated-json-api/recipe.yaml +2 -2
  177. package/recipes/contact-form/functions/contact.mjs +2 -1
  178. package/recipes/contact-form/recipe.yaml +3 -3
  179. package/recipes/cors-api/README.md +2 -2
  180. package/recipes/cors-api/recipe.yaml +2 -2
  181. package/recipes/health-page/README.md +1 -1
  182. package/recipes/health-page/recipe.yaml +1 -1
  183. package/recipes/json-api/README.md +1 -1
  184. package/recipes/json-api/recipe.yaml +4 -4
  185. package/recipes/middleware/README.md +8 -4
  186. package/recipes/middleware/middleware/bucket.mjs +12 -2
  187. package/recipes/middleware/middleware/locale.mjs +7 -3
  188. package/recipes/middleware/recipe.yaml +1 -1
  189. package/recipes/protected-download/README.md +1 -1
  190. package/recipes/protected-download/recipe.yaml +2 -2
  191. package/recipes/redirect/recipe.yaml +1 -1
  192. package/recipes/static-plus-api/README.md +2 -2
  193. package/recipes/static-plus-api/public/index.html +1 -1
  194. package/recipes/static-plus-api/recipe.yaml +2 -2
  195. package/recipes/static-plus-api/urlcode.yaml +1 -1
  196. package/recipes/typescript/README.md +2 -1
  197. package/recipes/typescript/recipe.yaml +5 -5
  198. package/recipes/webhook-receiver/README.md +5 -1
  199. package/recipes/webhook-receiver/recipe.yaml +2 -1
  200. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  201. package/schemas/recipe.schema.json +4 -3
  202. package/schemas/urlcode.schema.json +14 -41
  203. package/skills/urlcode/SKILL.md +32 -11
  204. package/starters/default/AGENTS.md +18 -18
  205. package/starters/default/urlcode.yaml +0 -1
  206. package/dist/link-api.js +0 -136
  207. package/dist/link-cli.js +0 -141
  208. package/dist/link-events.js +0 -76
  209. package/dist/link-records.js +0 -31
  210. package/dist/link-store-worker.js +0 -150
  211. package/dist/link-store.js +0 -250
  212. package/dist/management-policy.js +0 -40
  213. package/dist/sqlite-version.js +0 -6
  214. package/dist/types/link-api.d.ts +0 -30
  215. package/dist/types/link-cli.d.ts +0 -37
  216. package/dist/types/link-events.d.ts +0 -27
  217. package/dist/types/link-records.d.ts +0 -11
  218. package/dist/types/link-store-worker.d.ts +0 -1
  219. package/dist/types/link-store.d.ts +0 -130
  220. package/dist/types/management-policy.d.ts +0 -8
  221. package/dist/types/sqlite-version.d.ts +0 -1
  222. package/docs/DYNAMIC-LINKS.md +0 -61
  223. package/docs/links/cli.md +0 -110
  224. package/docs/links/limits.md +0 -175
  225. package/docs/links/management-api.md +0 -80
  226. package/docs/links/pools.md +0 -75
  227. package/docs/links/setup.md +0 -135
  228. package/docs/yaml/links.md +0 -30
  229. package/examples/live-links/README.md +0 -11
  230. package/examples/live-links/example.yaml +0 -21
  231. package/examples/live-links/tests/requests.json +0 -6
  232. package/examples/live-links/urlcode.yaml +0 -16
@@ -1,5 +1,8 @@
1
1
  // Language redirect from Accept-Language, like Next.js i18n middleware. Only
2
2
  // languages listed in LOCALES are chosen; the native redirect is the default.
3
+ // `Response.redirect()`'s headers are immutable (a trusted route's real
4
+ // `Response` enforces the Fetch standard here, unlike the sandbox's guest
5
+ // API), so that branch builds its own `Response` with headers up front.
3
6
  export default async function locale(request, context, next) {
4
7
  const supported = (context.env.LOCALES || '').split(/\s+/).filter(Boolean);
5
8
  const ranked = (request.headers.get('accept-language') || '').split(',').map((part, index) => {
@@ -8,9 +11,10 @@ export default async function locale(request, context, next) {
8
11
  return {lang: tag.trim().toLowerCase().split('-')[0], q: q ? Number(q.slice(2)) || 0 : 1, index};
9
12
  }).filter(p => p.q > 0).sort((a, b) => b.q - a.q || a.index - b.index);
10
13
  const chosen = ranked.find(p => supported.includes(p.lang))?.lang;
11
- const response = chosen && chosen !== supported[0]
12
- ? Response.redirect(context.env.SITE + '/' + chosen + '/welcome', 302)
13
- : await next();
14
+ if (chosen && chosen !== supported[0]) {
15
+ return new Response(null, { status: 302, headers: { location: context.env.SITE + '/' + chosen + '/welcome', vary: 'accept-language' } });
16
+ }
17
+ const response = await next();
14
18
  response.headers.set('vary', 'accept-language');
15
19
  return response;
16
20
  }
@@ -3,7 +3,7 @@ description: Fourteen reusable middleware patterns around fifteen routes, mirror
3
3
  tags: [middleware, auth, bearer, basic, cors, tracing, request-id, errors, etag, caching, maintenance, negotiation, locale, referer, method-override, envelope, validation, debug, bucket, experiment]
4
4
  complexity: advanced
5
5
  capabilities: [bindings, download, enabled, function, methods, middleware, parameters, redirect, request.body, respond]
6
- targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
7
  routes: 15
8
8
  inputs:
9
9
  - name: credentials
@@ -3,7 +3,7 @@
3
3
  `/downloads/report` serves `files/report.txt` as an attachment, but only after
4
4
  the operator-installed `auth` extension authorizes the request (`auth: true`,
5
5
  the short form of `policies.extensions.auth`). The file is
6
- served natively: no guest code runs, and the response is forced to `no-store`.
6
+ served natively: no project code runs at all, and the response is forced to `no-store`.
7
7
 
8
8
  Like the `authenticated-json-api` recipe, this project declares the extension
9
9
  and needs an operator host file outside the project plus the canonical origin
@@ -3,7 +3,7 @@ description: Native attachment download that only the operator-installed auth ex
3
3
  tags: [download, attachment, protected, auth, authenticated, file, private, extension, content-disposition, "401"]
4
4
  complexity: advanced
5
5
  capabilities: [download, enabled, extension, methods, policies.extensions]
6
- targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: refused}
6
+ targets: {self-hosted: conditional, aws: conditional, vercel: conditional, cloudflare: refused, static: refused}
7
7
  routes: 1
8
8
  services:
9
9
  - name: auth extension
@@ -28,4 +28,4 @@ behavior:
28
28
  - GET /downloads/report without credentials answers 401
29
29
  - GET with the accepted credential answers 200, Content-Disposition attachment and Cache-Control no-store
30
30
  - HEAD mirrors both cases with an empty body
31
- - no guest code runs; the file is served natively and snapshotted at activation
31
+ - no project code runs; the file is served natively and snapshotted at activation
@@ -3,7 +3,7 @@ description: Permanent redirect that forwards only an allowlisted query key.
3
3
  tags: [redirect, permanent, "301", query, passthrough, documentation, native]
4
4
  complexity: starter
5
5
  capabilities: [enabled, methods, redirect]
6
- targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible}
6
+ targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible, static: compatible}
7
7
  routes: 1
8
8
  inputs:
9
9
  - name: destination
@@ -5,8 +5,8 @@ Run `urlcode validate --local --project .`, `urlcode test --project .` and
5
5
 
6
6
  `/` serves `public/index.html`, `/assets/*` serves everything under
7
7
  `public/assets` (with `index.html` for the directory itself), and `/api/info`
8
- is a sandboxed function returning JSON built from literal `args`. Pages and
9
- assets are native: no guest code runs for them, and they are snapshotted at
8
+ is a function returning JSON built from literal `args`. Pages and
9
+ assets are native: no project code runs for them at all, and they are snapshotted at
10
10
  activation, so new files need a reload. Edit the HTML, add files under
11
11
  `public/assets`, and change or extend `functions/info.mjs`.
12
12
 
@@ -3,6 +3,6 @@
3
3
  <head><meta charset="utf-8"><title>Example site</title><link rel="stylesheet" href="/assets/site.css"></head>
4
4
  <body>
5
5
  <h1>Example site</h1>
6
- <p>Static pages are served natively; <a href="/api/info">/api/info</a> is a sandboxed function.</p>
6
+ <p>Static pages are served natively; <a href="/api/info">/api/info</a> is a function route.</p>
7
7
  </body>
8
8
  </html>
@@ -1,9 +1,9 @@
1
1
  id: static-plus-api
2
- description: A native static site (home page and an assets directory) next to one sandboxed JSON endpoint.
2
+ description: A native static site (home page and an assets directory) next to one function-backed JSON endpoint.
3
3
  tags: [static, site, page, html, assets, css, api, json, function, cache-control, website]
4
4
  complexity: intermediate
5
5
  capabilities: [enabled, function, methods, page, response.headers, static]
6
- targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
7
  routes: 3
8
8
  inputs:
9
9
  - name: pages
@@ -12,7 +12,7 @@ routes:
12
12
  index: index.html
13
13
  cacheControl: public, max-age=3600
14
14
  /api/info:
15
- description: A small JSON endpoint next to the static site, computed in the sandbox.
15
+ description: A small JSON endpoint next to the static site, computed by a trusted function.
16
16
  function:
17
17
  source: functions/info.mjs
18
18
  args:
@@ -4,4 +4,5 @@ Run `urlcode build-typescript --project . --out ../hello-built` with a new outpu
4
4
  directory, then `urlcode validate --local --project ../hello-built` and
5
5
  `urlcode serve --project ../hello-built`. GET `/hello` returns JSON.
6
6
  TypeScript is transpiled ahead of time. The runtime executes only the emitted
7
- JavaScript in QuickJS/WASM. The build does not type-check or read tsconfig.json.
7
+ JavaScript, in QuickJS/WASM for a `sandbox: true` route and in-process for a
8
+ trusted one. The build does not type-check or read tsconfig.json.
@@ -1,14 +1,14 @@
1
1
  id: typescript
2
- description: Typed guest function transpiled ahead of time; only the emitted JavaScript runs in the sandbox.
3
- tags: [typescript, function, build, typed, compile, sandbox]
2
+ description: "Typed function transpiled ahead of time; only the emitted JavaScript runs: in-process for a trusted route, in QuickJS/WASM where a route declares `sandbox: true`."
3
+ tags: [typescript, function, build, typed, compile]
4
4
  complexity: intermediate
5
5
  capabilities: [enabled, function, methods]
6
- targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
7
  routes: 1
8
8
  inputs:
9
9
  - name: handler
10
10
  file: functions/hello.ts
11
- description: The typed guest; keep to the Request/Response sandbox API.
11
+ description: "The typed handler; keep to Request/Response, and to the sandbox subset if you add `sandbox: true`."
12
12
  files: [urlcode.yaml, functions/hello.ts, tests/requests.json, README.md]
13
13
  tests:
14
14
  fixtures: tests/requests.json
@@ -18,6 +18,6 @@ tests:
18
18
  - urlcode test --project ../hello-built
19
19
  - urlcode audit --project ../hello-built --expect-routes 1
20
20
  behavior:
21
- - after the build, GET /hello answers 200 JSON from the compiled guest
21
+ - after the build, GET /hello answers 200 JSON from the compiled module
22
22
  - the unbuilt project does not activate; .ts sources never run
23
23
  - POST answers 405
@@ -5,7 +5,11 @@ Run `urlcode validate --local --project .`, `urlcode test --project .` and
5
5
 
6
6
  `POST /webhook` accepts at most 64 KiB of `application/json`. The runtime
7
7
  rejects other methods (405), other content types (415), oversized bodies (413)
8
- and malformed JSON (400) before the sandbox runs. The function then requires an
8
+ and malformed JSON (400) before the sandbox runs. The route declares
9
+ `sandbox: true`: functions/middleware run trusted by default, but a
10
+ third-party sender's payload is exactly the input a project doesn't fully
11
+ trust, so this recipe isolates parsing it (docs/FUNCTION-SECURITY.md). The
12
+ function then requires an
9
13
  `X-Webhook-Event` header and a JSON object carrying a string `id`, and answers
10
14
  `202 {"received":true,...}`.
11
15
 
@@ -3,7 +3,7 @@ description: Accept a bounded JSON event that names its type in a header, check
3
3
  tags: [webhook, receiver, event, post, json, body, "202", callback, ingest, function]
4
4
  complexity: starter
5
5
  capabilities: [enabled, function, methods, request.body, response.headers]
6
- targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
7
  routes: 1
8
8
  inputs:
9
9
  - name: shape check
@@ -24,3 +24,4 @@ behavior:
24
24
  - a missing event header answers 400; a body that is not an object with a string id answers 422
25
25
  - other methods 405, other content types 415, oversized bodies 413 and malformed JSON 400 before the sandbox runs
26
26
  - the guest has no crypto or network API, so signatures are not verified here; put signed webhooks behind an operator extension or trusted host
27
+ - "the route declares sandbox: true, isolating the third-party payload from the trusted-by-default execution every other function/middleware route gets"
@@ -11,6 +11,13 @@ routes:
11
11
  format: json
12
12
  function:
13
13
  source: functions/receive.mjs
14
+ # A third-party sender's payload is exactly the kind of input this
15
+ # project does not fully trust (docs/AI-AUTHORING.md, "Deciding when a
16
+ # route needs sandbox: true"), even after body/content-type validation:
17
+ # isolate parsing it from the rest of the process.
18
+ sandbox: true
19
+ sandboxReason: >-
20
+ Third-party webhook payload; isolate parsing it even after body/content-type validation.
14
21
  response:
15
22
  headers:
16
23
  Cache-Control: no-store
@@ -45,12 +45,13 @@
45
45
  "description": "Derived: the capability preflight verdict per target. compatible means no issue; otherwise the strongest issue (refused, unknown, conditional).",
46
46
  "type": "object",
47
47
  "additionalProperties": false,
48
- "required": ["self-hosted", "aws", "vercel", "cloudflare"],
48
+ "required": ["self-hosted", "aws", "vercel", "cloudflare", "static"],
49
49
  "properties": {
50
50
  "self-hosted": {"$ref": "#/$defs/verdict"},
51
51
  "aws": {"$ref": "#/$defs/verdict"},
52
52
  "vercel": {"$ref": "#/$defs/verdict"},
53
- "cloudflare": {"$ref": "#/$defs/verdict"}
53
+ "cloudflare": {"$ref": "#/$defs/verdict"},
54
+ "static": {"$ref": "#/$defs/verdict"}
54
55
  }
55
56
  },
56
57
  "routes": {
@@ -81,7 +82,7 @@
81
82
  "additionalProperties": false,
82
83
  "required": ["kind", "description"],
83
84
  "properties": {
84
- "kind": {"enum": ["extension", "signals", "proxy", "env", "secret", "link-store", "origin"]},
85
+ "kind": {"enum": ["extension", "signals", "proxy", "env", "secret", "origin"]},
85
86
  "description": {"type": "string", "minLength": 1, "maxLength": 300}
86
87
  }
87
88
  }
@@ -22,11 +22,6 @@
22
22
  "uniqueItems": true,
23
23
  "maxItems": 256
24
24
  },
25
- "dynamicLinks": {
26
- "type": "boolean",
27
- "default": false,
28
- "description": "Entry-point opt-in for live stored-link routes. Does not enable guest storage or management access."
29
- },
30
25
  "policies": {
31
26
  "$ref": "#/$defs/policies"
32
27
  },
@@ -346,6 +341,16 @@
346
341
  "enabled": {
347
342
  "type": "boolean"
348
343
  },
344
+ "sandbox": {
345
+ "type": "boolean",
346
+ "default": false,
347
+ "description": "Run this route's function/middleware in the isolated QuickJS/WASM worker pool instead of the trusted, in-process default. false or absent (the default) means trusted, unsandboxed, direct host-process execution."
348
+ },
349
+ "sandboxReason": {
350
+ "type": "string",
351
+ "maxLength": 500,
352
+ "description": "Optional justification for this route's sandbox decision, whether sandbox is true or false: why it needs isolation, or why it is safe to trust. Never inferred or enforced; surfaced verbatim by explain/context/manifest."
353
+ },
349
354
  "expires": {
350
355
  "type": "string"
351
356
  },
@@ -882,37 +887,6 @@
882
887
  ]
883
888
  }
884
889
  },
885
- "link": {
886
- "type": "object",
887
- "properties": {
888
- "collection": {
889
- "type": "string",
890
- "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
891
- },
892
- "code": {
893
- "type": "object",
894
- "properties": {
895
- "from": {
896
- "const": "path"
897
- },
898
- "name": {
899
- "type": "string",
900
- "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
901
- }
902
- },
903
- "required": [
904
- "from",
905
- "name"
906
- ],
907
- "additionalProperties": false
908
- }
909
- },
910
- "required": [
911
- "collection",
912
- "code"
913
- ],
914
- "additionalProperties": false
915
- },
916
890
  "policies": {
917
891
  "$ref": "#/$defs/policies"
918
892
  },
@@ -964,6 +938,10 @@
964
938
  "$ref": "#/$defs/routeAuth"
965
939
  }
966
940
  ]
941
+ },
942
+ "cache": {
943
+ "description": "Short form for policies.cache: the same object, expanded to policies.cache before anything else reads the project. Refused alongside policies.cache; use one form.",
944
+ "$ref": "#/$defs/policyCache"
967
945
  }
968
946
  },
969
947
  "required": [],
@@ -999,11 +977,6 @@
999
977
  "respond"
1000
978
  ]
1001
979
  },
1002
- {
1003
- "required": [
1004
- "link"
1005
- ]
1006
- },
1007
980
  {
1008
981
  "required": [
1009
982
  "conditional"
@@ -1,15 +1,24 @@
1
1
  ---
2
2
  name: urlcode
3
- description: Work on a URLCode project, recognized by a urlcode.yaml file with version "1" and served by the @jimhoyd/urlcode runtime. Use this skill whenever a task touches urlcode.yaml, its included route files, functions or middleware under a URLCode project, or asks to add, change, test or deploy URL behavior (redirects, responses, pages, files, functions, live links, policies) in such a project. It teaches the authoring loop and how to retrieve the minimum reference from the installed runtime instead of guessing fields.
3
+ description: Work on a URLCode project, recognized by a urlcode.yaml file with version "1" and served by the @jimhoyd/urlcode runtime. Use this skill whenever a task touches urlcode.yaml, its included route files, functions or middleware under a URLCode project, or asks to add, change, test or deploy URL behavior (redirects, responses, pages, files, functions, policies) in such a project. It teaches the authoring loop and how to retrieve the minimum reference from the installed runtime instead of guessing fields.
4
4
  ---
5
5
 
6
6
  # URLCode authoring loop
7
7
 
8
8
  A URLCode project declares URL behavior in YAML; the installed runtime serves
9
- it. Your job is to change the declaration and the minimal guest code it names,
9
+ it. Your job is to change the declaration and the minimal application code it names,
10
10
  then prove the change with the runtime's own checks. Never reimplement what the
11
11
  runtime provides, and never invent fields.
12
12
 
13
+ ## Declarative-first default
14
+
15
+ > Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
16
+
17
+ Check the installed version's primitives, YAML configuration, policies, supported
18
+ extensions and recipes/templates before writing a custom function or middleware.
19
+ Keep necessary custom code focused and report the capability gap; never invent
20
+ fields or bypass target limits or operator grants. See `docs/PROJECT-DIRECTION.md` in the installed runtime.
21
+
13
22
  ## 1. Recognize the project
14
23
 
15
24
  - The root has `urlcode.yaml` with `version: "1"`. Included route files are
@@ -52,18 +61,30 @@ installed package also ships `docs/YAML-REFERENCE.md` (generated field
52
61
  inventory) and `schemas/urlcode.schema.json`; search them for the one key you
53
62
  need rather than reading them whole.
54
63
 
55
- ## 3. Prefer YAML, then a recipe, then a function
64
+ ## 3. Choose the highest-level supported abstraction
56
65
 
57
66
  1. If a native handler expresses the behavior (`redirect`, `respond`, `page`,
58
- `static`, `download`, `link`, `proxy`, `conditional`), write YAML only.
59
- 2. If a recipe from `recipes list` is close, `urlcode recipes add NAME --out DIR`
67
+ `static`, `download`, `proxy`, `conditional`), write YAML only.
68
+ 2. Check supported extensions and their configuration before custom code. If a
69
+ recipe from `recipes list` is close, `urlcode recipes add NAME --out DIR`
60
70
  and adapt the copy into the project's layout.
61
- 3. Only then write a function or middleware: one exported handler, relative
62
- ES-module imports inside the project only, inputs from validated `args`,
63
- output as a `Response`. The sandbox has no `fetch`, Node, filesystem or
64
- timers; a need for them is a `proxy` route, a binding, or a report.
65
- 4. Never build routing, validation, middleware chains, policies, static
66
- serving, caching, throttling or authentication in code. Declare them.
71
+ 3. Only then write a function or middleware: one exported handler, inputs from validated `args`,
72
+ output as a `Response`. `function`/`middleware` routes run trusted and
73
+ unsandboxed by default: full Node, npm, filesystem and `fetch` access, like
74
+ any other project code. Add `sandbox: true` only when that route's own code
75
+ warrants isolation (unreviewed or third-party code, a secret whose blast
76
+ radius matters, complex logic — never merely because it handles request
77
+ data, which is untrusted in both modes) — a `sandbox: true` route then has no
78
+ `fetch`, Node, npm, filesystem, WebSocket, streaming or crypto APIs (bounded timers are supported);
79
+ a need for those in a sandboxed route is a `proxy` route, a binding, or a
80
+ report.
81
+ 4. Declare routing, validation, middleware chains, policies, static serving,
82
+ caching, throttling and authentication wherever the runtime or a supported
83
+ extension provides them. Use custom code only for the unmet requirement.
84
+ Where a short form exists, it is the highest-level form: `auth: true` or
85
+ `auth: { role: admin }` on a route whose project declares an `auth`
86
+ extension, and `cache: { … }` for `policies.cache`. Each expands to the long
87
+ form; declaring both is refused.
67
88
 
68
89
  Keep every route you were not asked to change. Match the file organization the
69
90
  project already uses.
@@ -16,11 +16,11 @@ authentication; the runtime provides them. Read this file before changing anythi
16
16
  3. Run `urlcode recipes list` and `urlcode recipes show NAME` before writing a
17
17
  route from scratch. If a recipe covers the need, add it with
18
18
  `urlcode recipes add NAME --out DIR` and adapt the copy.
19
- 4. Prefer YAML over code. Prefer native handlers over functions.
19
+ 4. Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement. Check supported extensions and recipes first; explain any capability gap.
20
20
 
21
21
  ## Ask the runtime through MCP first
22
22
 
23
- `.mcp.json` registers the read-only `urlcode mcp` server. When it is
23
+ When present, `.mcp.json` registers the read-only `urlcode mcp` server. When it is
24
24
  available, prefer its tools over reading documents: `get_context`,
25
25
  `get_capability`, `get_schema`, `search_recipes`, `explain`, `get_manifest`.
26
26
  The CLI equivalents are the fallback: `urlcode context`, `urlcode capabilities NAME`,
@@ -29,24 +29,25 @@ The CLI equivalents are the fallback: `urlcode context`, `urlcode capabilities N
29
29
 
30
30
  ## What the runtime provides (this version)
31
31
 
32
- - Handlers, exactly one per route: `redirect`, `respond`, `page`, `static`, `download`, `function`, `link`, `proxy`, `conditional`, `extension`.
33
- - Ordered `middleware` around any handler, declared in YAML, run in the sandbox.
32
+ - Handlers, exactly one per route: `redirect`, `respond`, `page`, `static`, `download`, `function`, `proxy`, `conditional`.
33
+ - Ordered `middleware` around any handler, declared in YAML, trusted by default.
34
34
  - Validated inputs: `parameters`, `request.body` and `methods` on the route;
35
35
  functions receive validated `args`, never raw user input.
36
36
  - Policies, host-enforced and off by default: `agents`, `throttle`, `cache`, `security`, `compression`.
37
37
  - Site conventions under `site`, each generating one native route: `robots` (/robots.txt), `sitemap` (/sitemap.xml), `favicon` (/favicon.ico), `securityTxt` (/.well-known/security.txt), `llms` (/llms.txt).
38
38
  - Bindings: named `env` and `secrets` references resolved by the operator, never values in YAML.
39
39
 
40
- Never recreate any of these in a function. If a requirement seems to need one
41
- of them and it is missing, that is a report, not an invitation to reimplement.
40
+ Never recreate any of these in a function; a missing one is a report, not an
41
+ invitation to reimplement it.
42
42
 
43
- ## Functions and middleware are sandboxed
43
+ ## Functions and middleware are trusted by default; sandbox is opt-in
44
44
 
45
- Guest code runs in an isolated JavaScript engine with a fresh heap per call.
46
- It sees a text/JSON `Request`/`Response` subset, validated `args` and granted
47
- `env`. There is no `fetch`, no Node API, no filesystem, no timers and no
48
- imports outside the project. Do not write code that needs them; declare a
49
- `proxy` route or a binding instead and say why.
45
+ A route's `function`/`middleware` runs trusted, in-process, with full
46
+ Node/filesystem/`fetch` access, given only declared `args`/`env`/`secrets`. Add
47
+ `sandbox: true` when that code warrants isolation (unreviewed code, a sensitive
48
+ secret, complex logic) — not merely for untrusted input, which both modes share.
49
+ A `sandbox: true` route gets a text/JSON subset only: use `proxy`/a binding, and
50
+ say why in `sandboxReason`.
50
51
 
51
52
  ## Checks that count as evidence
52
53
 
@@ -56,10 +57,9 @@ urlcode test
56
57
  urlcode audit --expect-routes 2
57
58
  ```
58
59
 
59
- Run all three after every change. Update the expected route count deliberately
60
- when you add or remove a route, and add fixtures to `tests/requests.json` for
61
- every new route (positive and negative cases, every active method, HEAD).
62
- Without a global install, invoke `node /path/to/urlcode/src/cli.ts` instead of `urlcode`.
60
+ Run all three after every change, updating the route count deliberately and
61
+ adding `tests/requests.json` fixtures for every new route (positive/negative,
62
+ every active method, HEAD). No global install: use `node /path/to/urlcode/src/cli.ts`.
63
63
 
64
64
  ## Rules
65
65
 
@@ -69,8 +69,8 @@ Without a global install, invoke `node /path/to/urlcode/src/cli.ts` instead of `
69
69
  stop; the operator grants it outside this project, pinned to the revision.
70
70
  - Secrets stay out of the project: no keys, tokens or credentials in YAML,
71
71
  functions, fixtures, `.env` files that are not ignored, or commit messages.
72
- - Authentication is host processing: declare `auth` on the route, never build
73
- login forms, sessions or password checks in functions.
72
+ - Protect a route with `auth: true`/`auth: { role: admin }` where an `auth`
73
+ extension is declared; `cache` likewise expands to `policies.cache`.
74
74
  - Validation, tests and the audit are the evidence. Local checks are not a
75
75
  deployment, a soak test or a security review; do not claim otherwise.
76
76
 
@@ -1,7 +1,6 @@
1
1
  # yaml-language-server: $schema=https://raw.githubusercontent.com/jimhoyd-com/urlcode/669632fb4f188f293a0f622847f87ee019c0627f/schemas/urlcode.schema.json
2
2
  # Organize files however you like; list their paths from this project root.
3
3
  version: "1"
4
- dynamicLinks: false
5
4
  includes:
6
5
  - routes/functions.yaml
7
6
  - routes/marketing/links.yaml
package/dist/link-api.js DELETED
@@ -1,136 +0,0 @@
1
- import http from 'node:http';
2
- import {createHash,timingSafeEqual,randomUUID} from 'node:crypto';
3
- import {readFile,lstat} from 'node:fs/promises';
4
-
5
- import {outsideProject} from './link-store.js';
6
-
7
-
8
- import {linkCollection,linkCode} from './link-records.js';
9
- import {assert,HttpError} from './errors.js';
10
- import {createJsonLogger} from './logging.js';
11
-
12
- /** The store surface management needs: the pooled link store, or anything with the same contract. */
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
- export async function loadLinkToken(file ,project ) {
27
- const path=await outsideProject(file,project);const info=await lstat(path);
28
- assert(info.isFile()&&!info.isSymbolicLink()&&info.nlink===1&&info.size<=1024,'Invalid management token file');
29
- assert(process.platform==='win32'||(info.mode&0o077)===0,'Management token file must be private (mode 600)');
30
- const token=(await readFile(path,'utf8')).trim();
31
- assert(/^[A-Za-z0-9_-]{43,256}$/.test(token),'Management token must contain at least 43 base64url characters');return token;
32
- }
33
- function body(req ) {
34
- return new Promise((resolve,reject)=>{
35
- let bytes=0;const chunks =[];
36
- const cleanup=()=>{req.off('data',data);req.off('end',end);req.off('error',failed);req.off('aborted',failed);};
37
- const failed=()=>{cleanup();reject(new HttpError(400,'Incomplete management body'));};
38
- const data=(chunk )=>{
39
- bytes+=chunk.length;
40
- if(bytes>16384){cleanup();req.resume();reject(new HttpError(413,'Management body too large'));return;}
41
- chunks.push(chunk);
42
- };
43
- const end=()=>{
44
- cleanup();
45
- if(!bytes){reject(new HttpError(400,'JSON body required'));return;}
46
- try{resolve(JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(Buffer.concat(chunks))));}
47
- catch{reject(new HttpError(400,'Invalid JSON body'));}
48
- };
49
- req.on('data',data);req.once('end',end);req.once('error',failed);req.once('aborted',failed);
50
- });
51
- }
52
- const isRecord=(value ) =>value!==null && typeof value==='object' && !Array.isArray(value);
53
- const mutations ={POST:'create',PUT:'update',DELETE:'delete'};
54
- export async function startLinkApi({store,collection,token,authorize,host='127.0.0.1',port=3001,maxInFlightRequests=32,socketTimeoutMs=10000,log=createJsonLogger()} ) {
55
- linkCollection(collection);assert(['127.0.0.1','::1'].includes(host),'Management must bind a literal loopback address; use a private authenticated tunnel');
56
- assert(!authorize || store.atomicAudit===true,'Scoped management requires a store with atomic audit support');
57
- assert(authorize===undefined || typeof authorize==='function','Invalid management authorizer');
58
- assert(authorize || typeof token==='string'&&/^[A-Za-z0-9_-]{43,256}$/.test(token),'Invalid management token');
59
- assert(Number.isInteger(maxInFlightRequests)&&maxInFlightRequests>=1&&maxInFlightRequests<=64,'Management admission must be 1–64');
60
- assert(Number.isInteger(socketTimeoutMs)&&socketTimeoutMs>=100&&socketTimeoutMs<=60000,'Management socket timeout must be 100–60000 ms');
61
- let inFlight=0,shuttingDown=false,closing ;
62
- const digest=(value )=>createHash('sha256').update(value).digest();const expected=digest('Bearer '+token);
63
- const server=http.createServer({maxHeaderSize:8192,headersTimeout:5000,requestTimeout:10000,keepAliveTimeout:5000},async(req,res)=>{
64
- req.on('error',()=>{});res.on('error',()=>{});
65
- const requestId=randomUUID(),started=performance.now();let principal ;let authenticated=false,action='request',admitted=false,reported=false;
66
- const target=req.url??'',method=req.method??'';
67
- const report=()=>{
68
- if(reported)return;reported=true;if(admitted)inFlight--;
69
- try{log({event:'management_request',timestamp:new Date().toISOString(),requestId,collection,action,authenticated,principal:principal?.id,status:res.headersSent?res.statusCode:0,outcome:res.writableFinished?'finished':'aborted',durationMs:Math.round((performance.now()-started)*100)/100});}catch{/* Logging must not fail requests. */}
70
- };
71
- res.once('finish',report);res.once('close',report);
72
- const send=(status ,value ,headers ={})=>{const payload=value===undefined?undefined:JSON.stringify(value);res.writeHead(status,{'content-type':'application/json','cache-control':'no-store','x-content-type-options':'nosniff','x-request-id':requestId,...headers});res.end(payload);};
73
- try {
74
- if(shuttingDown || inFlight>=maxInFlightRequests)throw new HttpError(503,'Management capacity unavailable');
75
- inFlight++;admitted=true;
76
- let authCount=0;for(let i=0;i<req.rawHeaders.length;i+=2)if(req.rawHeaders[i]?.toLowerCase()==='authorization')authCount++;
77
- if(authCount===1){
78
- if(authorize){
79
- const bearer=/^Bearer ([A-Za-z0-9_-]{43,256})$/.exec(req.headers.authorization||'');
80
- if(bearer?.[1]!==undefined)principal=await authorize(bearer[1]);
81
- }else if(timingSafeEqual(expected,digest(req.headers.authorization||'')))principal={id:'legacy-shared',collections:[collection],actions:['get','list','create','update','delete']};
82
- }
83
- if(!principal) {req.resume();send(401,{error:'Unauthorized'},{'www-authenticate':'Bearer',connection:'close'});return;}
84
- authenticated=true;
85
- if(req.headers.origin!==undefined)throw new HttpError(403,'Browser-origin management requests are unsupported');
86
- if(!target.startsWith('/')||target.startsWith('//')||target.length>2048)throw new HttpError(400,'Invalid management target');
87
- const url=new URL(target,'http://localhost');
88
- if(target.split('?')[0]!==url.pathname)throw new HttpError(400,'Noncanonical management path');
89
- const match=/^\/v1\/links(?:\/([A-Za-z0-9_-]{1,128}))?$/.exec(url.pathname);
90
- if(!match)throw new HttpError(404,'Not found');
91
- const code=match[1];if(code)linkCode(code);
92
- if([...url.searchParams.keys()].some(k=>!['limit','after'].includes(k)) || (code && url.search))throw new HttpError(400,'Unsupported query');
93
- const allowed=code?['GET','PUT','DELETE']:['GET','POST'];
94
- if(!allowed.includes(method)){req.resume();send(405,{error:'Method not allowed'},{allow:allowed.join(', '),connection:'close'});return;}
95
- action=method==='GET'?(code?'get':'list'):mutations[method]??'request';
96
- if(!principal.collections.includes(collection) || !principal.actions.includes(action))throw new HttpError(403,'Management permission denied');
97
- const audit ={actor:principal.id,requestId};
98
- if(method==='GET'){
99
- req.resume();
100
- if(code){const value=await store.get(collection,code);if(!value)throw new HttpError(404,'Link not found');send(200,value,{etag:`"${value.version}"`});}
101
- else {
102
- if([...url.searchParams.keys()].some(k=>url.searchParams.getAll(k).length!==1))throw new HttpError(400,'Duplicate query');
103
- const raw=url.searchParams.get('limit')??'100';if(!/^\d{1,3}$/.test(raw))throw new HttpError(400,'Invalid limit');
104
- const items=await store.list(collection,{limit:Number(raw),after:url.searchParams.get('after')||''});
105
- send(200,{items,nextAfter:items.length===Number(raw)?items.at(-1)?.code??null:null});
106
- }
107
- return;
108
- }
109
- if(url.search)throw new HttpError(400,'Query unsupported for mutations');
110
- let expectedVersion ;
111
- if(method!=='POST'){
112
- const etag=req.headers['if-match'];if(!etag)throw new HttpError(428,'If-Match is required');
113
- if(!/^"[1-9]\d{0,15}"$/.test(etag))throw new HttpError(400,'Invalid If-Match');expectedVersion=Number(etag.slice(1,-1));
114
- }
115
- if(method==='DELETE'){req.resume();await store.delete(collection,code??'',expectedVersion,audit);send(204);return;}
116
- if(req.headers['content-encoding'] && req.headers['content-encoding']!=='identity')throw new HttpError(415,'Encoding unsupported');
117
- if(req.headers['content-type']?.split(';')[0]?.trim().toLowerCase()!=='application/json')throw new HttpError(415,'Expected application/json');
118
- const data=await body(req);
119
- if(!isRecord(data))throw new HttpError(400,'Invalid record');
120
- let value ;
121
- if(method==='POST'){const {code:assigned,...record}=data;value=await store.create(collection,record,assigned,audit);}
122
- else value=await store.update(collection,code??'',data,expectedVersion,audit);
123
- send(method==='POST'?201:200,value,{etag:`"${value.version}"`});
124
- }catch(e){req.resume();if(res.headersSent){res.destroy();return;}if(!res.destroyed){const status=e instanceof HttpError?e.status:503;send(status,{error:e instanceof HttpError?e.message:'Management service unavailable'},{connection:'close'});}}
125
- });
126
- server.setTimeout(socketTimeoutMs,socket=>socket.destroy());
127
- server.maxConnections=64;server.maxRequestsPerSocket=100;
128
- server.on('clientError',(_error,socket)=>{if(socket.writable)socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');});
129
- await new Promise ((resolve,reject)=>{server.once('error',reject);server.listen(port,host,()=>{server.off('error',reject);resolve();});});
130
- const address=server.address();
131
- assert(address!==null && typeof address==='object','Management server has no address');
132
- return {address,close(){
133
- if(closing)return closing;shuttingDown=true;
134
- closing=(async()=>{const timeout=setTimeout(()=>server.closeAllConnections(),10000);timeout.unref();try{await new Promise (resolve=>server.close(()=>resolve()));}finally{clearTimeout(timeout);}})();return closing;
135
- }};
136
- }