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

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 (191) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +24 -8
  2. package/.claude/skills/urlcode-operations/SKILL.md +10 -16
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/README.md +75 -62
  5. package/ROADMAP.md +61 -16
  6. package/SECURITY.md +23 -8
  7. package/dist/BUILD-MANIFEST.json +38 -43
  8. package/dist/agents-guide.js +18 -18
  9. package/dist/build-static.js +134 -0
  10. package/dist/capabilities.js +85 -31
  11. package/dist/capability-query.js +0 -1
  12. package/dist/cli.js +23 -31
  13. package/dist/compliance-rules/baseline.js +2 -10
  14. package/dist/compliance-rules/privacy.js +5 -16
  15. package/dist/compliance-rules/shared.js +0 -2
  16. package/dist/compliance.js +6 -8
  17. package/dist/config.js +15 -8
  18. package/dist/context.js +9 -10
  19. package/dist/examples.js +2 -2
  20. package/dist/explain-cli.js +3 -3
  21. package/dist/explain.js +5 -6
  22. package/dist/extensions.js +61 -2
  23. package/dist/function-sources.js +34 -2
  24. package/dist/function-worker.js +3 -1
  25. package/dist/functions.js +84 -13
  26. package/dist/guest-api.js +29 -3
  27. package/dist/index.js +3 -6
  28. package/dist/manifest.js +5 -7
  29. package/dist/mcp.js +1 -1
  30. package/dist/observability.js +1 -21
  31. package/dist/policies/cache.js +8 -3
  32. package/dist/policy.js +28 -9
  33. package/dist/project-tests.js +3 -3
  34. package/dist/readiness.js +32 -9
  35. package/dist/router.js +5 -7
  36. package/dist/runtime.js +77 -58
  37. package/dist/sandbox.js +48 -0
  38. package/dist/scaffold.js +0 -0
  39. package/dist/scripts/operational-drills.js +12 -54
  40. package/dist/server.js +3 -29
  41. package/dist/tooling.js +1 -1
  42. package/dist/trusted-functions.js +210 -0
  43. package/dist/types/build-static.d.ts +43 -0
  44. package/dist/types/capabilities.d.ts +14 -5
  45. package/dist/types/compliance-rules/shared.d.ts +0 -2
  46. package/dist/types/compliance.d.ts +0 -3
  47. package/dist/types/config.d.ts +2 -1
  48. package/dist/types/context.d.ts +2 -1
  49. package/dist/types/examples.d.ts +1 -1
  50. package/dist/types/extensions.d.ts +55 -0
  51. package/dist/types/function-sources.d.ts +4 -0
  52. package/dist/types/functions.d.ts +48 -5
  53. package/dist/types/guest-api.d.ts +1 -0
  54. package/dist/types/index.d.ts +3 -6
  55. package/dist/types/manifest.d.ts +1 -3
  56. package/dist/types/observability.d.ts +1 -14
  57. package/dist/types/project-tests.d.ts +1 -2
  58. package/dist/types/readiness.d.ts +10 -3
  59. package/dist/types/router.d.ts +2 -1
  60. package/dist/types/runtime.d.ts +0 -27
  61. package/dist/types/sandbox.d.ts +12 -0
  62. package/dist/types/scaffold.d.ts +0 -2
  63. package/dist/types/server.d.ts +1 -4
  64. package/dist/types/tooling.d.ts +3 -3
  65. package/dist/types/trusted-functions.d.ts +29 -0
  66. package/dist/types/types.d.ts +14 -7
  67. package/dist/types/verify-deployment.d.ts +2 -2
  68. package/dist/types.js +17 -4
  69. package/dist/typescript-authoring.js +55 -17
  70. package/dist/verify-deployment.js +3 -3
  71. package/docs/AI-AUTHORING.md +104 -9
  72. package/docs/AWS.md +4 -4
  73. package/docs/BEST-PRACTICES.md +3 -2
  74. package/docs/CAPABILITIES.md +29 -16
  75. package/docs/CAPACITY.md +128 -31
  76. package/docs/CLOUDFLARE.md +1 -2
  77. package/docs/COMPLIANCE.md +6 -9
  78. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  79. package/docs/EXTENSIONS.md +183 -11
  80. package/docs/FRAMEWORK.md +35 -19
  81. package/docs/FUNCTION-SECURITY.md +129 -32
  82. package/docs/INSTALL.md +0 -5
  83. package/docs/LOAD-TESTING.md +4 -4
  84. package/docs/MIDDLEWARE.md +27 -16
  85. package/docs/MONITORING.md +2 -19
  86. package/docs/NEXT-PHASE-PLAN.md +12 -4
  87. package/docs/NEXT-STEPS.md +259 -40
  88. package/docs/OBSERVABILITY.md +7 -16
  89. package/docs/OPEN-DECISIONS.md +212 -0
  90. package/docs/OPERATIONAL-PROOF.md +26 -30
  91. package/docs/OPERATIONS.md +23 -32
  92. package/docs/POLICIES.md +15 -4
  93. package/docs/PRERENDER.md +2 -2
  94. package/docs/PROJECT-DIRECTION.md +34 -9
  95. package/docs/README.md +13 -11
  96. package/docs/RELEASE-READINESS.md +49 -29
  97. package/docs/RELEASE-SECURITY.md +89 -3
  98. package/docs/RESILIENCE.md +16 -15
  99. package/docs/ROUTING.md +8 -10
  100. package/docs/SANDBOX-REVIEW.md +19 -6
  101. package/docs/SCAFFOLDING.md +0 -2
  102. package/docs/SECURITY-AUDIT.md +40 -0
  103. package/docs/SPECIFICATION.md +60 -26
  104. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  105. package/docs/SPIKE-BUSINESS-SUITE.md +8 -0
  106. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  107. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  108. package/docs/SPIKE-EXTENSION-MODEL.md +2 -2
  109. package/docs/SPIKE-EXTENSIONS.md +5 -0
  110. package/docs/SPIKE-LAMBDA-COMPILE.md +4 -2
  111. package/docs/SPIKE-MONOREPO.md +322 -0
  112. package/docs/STARTERS.md +1 -1
  113. package/docs/STATIC.md +94 -0
  114. package/docs/TOOLING.md +13 -9
  115. package/docs/TUNNELS.md +0 -3
  116. package/docs/TYPESCRIPT-AUTHORING.md +27 -12
  117. package/docs/TYPESCRIPT.md +25 -4
  118. package/docs/USABILITY-REVIEW.md +7 -1
  119. package/docs/VERCEL.md +4 -5
  120. package/docs/VERSION-ALIGNMENT.md +205 -0
  121. package/docs/YAML-GUIDE.md +0 -3
  122. package/docs/YAML-REFERENCE.md +16 -6
  123. package/docs/policies/cache.md +13 -0
  124. package/examples/assets/example.yaml +1 -1
  125. package/examples/aws/example.yaml +1 -1
  126. package/examples/cloudflare/example.yaml +1 -1
  127. package/examples/conditions/example.yaml +1 -1
  128. package/examples/cookbook/example.yaml +1 -1
  129. package/examples/cookbook/middleware/bucket.mjs +12 -2
  130. package/examples/cookbook/middleware/locale.mjs +7 -3
  131. package/examples/egress/example.yaml +1 -1
  132. package/examples/extensions/example.yaml +1 -1
  133. package/examples/prerender/README.md +2 -2
  134. package/examples/prerender/example.yaml +1 -1
  135. package/examples/provider-conformance/example.yaml +1 -1
  136. package/examples/vercel/example.yaml +1 -1
  137. package/llms-full.txt +594 -219
  138. package/llms.txt +23 -12
  139. package/package.json +10 -3
  140. package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
  141. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +24 -8
  142. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +10 -16
  143. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  144. package/recipes/contact-form/recipe.yaml +1 -1
  145. package/recipes/cors-api/recipe.yaml +1 -1
  146. package/recipes/health-page/recipe.yaml +1 -1
  147. package/recipes/json-api/recipe.yaml +1 -1
  148. package/recipes/middleware/middleware/bucket.mjs +12 -2
  149. package/recipes/middleware/middleware/locale.mjs +7 -3
  150. package/recipes/middleware/recipe.yaml +1 -1
  151. package/recipes/protected-download/recipe.yaml +1 -1
  152. package/recipes/redirect/recipe.yaml +1 -1
  153. package/recipes/static-plus-api/recipe.yaml +1 -1
  154. package/recipes/typescript/README.md +2 -1
  155. package/recipes/typescript/recipe.yaml +1 -1
  156. package/recipes/webhook-receiver/README.md +5 -1
  157. package/recipes/webhook-receiver/recipe.yaml +2 -1
  158. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  159. package/schemas/recipe.schema.json +4 -3
  160. package/schemas/urlcode.schema.json +14 -41
  161. package/skills/urlcode/SKILL.md +29 -8
  162. package/starters/default/AGENTS.md +18 -18
  163. package/starters/default/urlcode.yaml +0 -1
  164. package/dist/link-api.js +0 -136
  165. package/dist/link-cli.js +0 -141
  166. package/dist/link-events.js +0 -76
  167. package/dist/link-records.js +0 -31
  168. package/dist/link-store-worker.js +0 -150
  169. package/dist/link-store.js +0 -250
  170. package/dist/management-policy.js +0 -40
  171. package/dist/sqlite-version.js +0 -6
  172. package/dist/types/link-api.d.ts +0 -30
  173. package/dist/types/link-cli.d.ts +0 -37
  174. package/dist/types/link-events.d.ts +0 -27
  175. package/dist/types/link-records.d.ts +0 -11
  176. package/dist/types/link-store-worker.d.ts +0 -1
  177. package/dist/types/link-store.d.ts +0 -130
  178. package/dist/types/management-policy.d.ts +0 -8
  179. package/dist/types/sqlite-version.d.ts +0 -1
  180. package/docs/DYNAMIC-LINKS.md +0 -61
  181. package/docs/MANAGEMENT-SECURITY.md +0 -82
  182. package/docs/links/cli.md +0 -110
  183. package/docs/links/limits.md +0 -175
  184. package/docs/links/management-api.md +0 -80
  185. package/docs/links/pools.md +0 -75
  186. package/docs/links/setup.md +0 -135
  187. package/docs/yaml/links.md +0 -30
  188. package/examples/live-links/README.md +0 -11
  189. package/examples/live-links/example.yaml +0 -21
  190. package/examples/live-links/tests/requests.json +0 -6
  191. package/examples/live-links/urlcode.yaml +0 -16
@@ -33,16 +33,12 @@ named.
33
33
  | `reload` | `status` `ok`/`rejected`; `version` string and `routes` integer on `ok` | `app.reload()` or the development watcher swapped, or refused to swap, the snapshot. |
34
34
  | `watch` | `status` `failed` | The development watcher could not fingerprint the project. |
35
35
  | `function_worker` | `status` `started`/`restarting`, `slot` integer; `attempt` and `delayMs` integers on `restarting` | A function worker became ready or is scheduled for replacement. |
36
- | `link_store_worker` | `status` `started`/`restarting`, `readOnly` boolean; `attempt` and `delayMs` on `restarting` | The same for a link-store connection. |
37
36
  | `signal` | `outcome` (`accepted`, `delivered`, `failed`, `dropped`), positive `count` | Best-effort webhook totals; no destination, request data or secrets. |
38
- | `link_observer` | `status` `failed`/`dropped`/`closed`; `reason` `timeout`/`error` on `failed`; `dropped` integer on `dropped`; `queued`, `delivered`, `dropped`, `failed`, `timedOut`, `closed` on `closed` | Only with `linkEvents`: the link collector failed, the queue shed events, or the channel drained at shutdown. |
39
- | `link_request` | `requestId`, `collection`, `route`, `code` (only with `includeCode`), `method`, `status`, `outcome`, `durationMs` | Delivered to the `linkEvents.observe` function only, after the response is over. It never reaches the log or observers; its `outcome` is counted in the metrics. |
40
37
  | `logs_dropped` | `count` integer | The JSON logger shed records because stdout was not writable. Written by the logger itself, so observers do not see it. |
41
38
  | `observer` | `status` `failed`, `name` string | An observer hook threw or rejected. Written to the default log only, never to observers. |
42
39
  | `throttle` | `route`, `outcome` `allowed`/`exceeded`, `remaining` integer | A throttle decision. `allowed` is logged only in `mode: report`; enforce mode logs refusals. |
43
40
  | `agents` | `route`, `list` string, `outcome` `denied`/`reported` | A User-Agent matched a list. The list name is logged, never the header. |
44
41
  | `cache` | `route`, `outcome` `hit`/`stale`/`miss`/`store` | A cache lookup or store. |
45
- | `management_request` | `timestamp`, `requestId`, `collection`, `action`, `authenticated`, `principal`, `status`, `outcome`, `durationMs` | The link-management API (`startLinkApi`), which has its own logger; `principal` is the operator-configured principal id. |
46
42
  | `listening` | `address`, `port`, `mode`, `origin` | Printed once by the CLI at startup, not emitted by the server. |
47
43
 
48
44
  Every event carries `event` (its name). Numbers are JSON numbers, never
@@ -51,11 +47,10 @@ strings.
51
47
  ### Privacy guarantees
52
48
 
53
49
  No event, snapshot or exposition carries a request URL, path, query string,
54
- header, body, client address, User-Agent string, binding, secret, stored link
55
- destination or user exception text. `route` is always a configured pattern
50
+ header, body, client address, User-Agent string, binding, secret or user
51
+ exception text. `route` is always a configured pattern
56
52
  from reviewed YAML. `requestId` is server-generated unless
57
- `--trust-request-id` accepts one from a trusted proxy. `code` in
58
- `link_request` is redacted unless the operator sets `includeCode`. An observer
53
+ `--trust-request-id` accepts one from a trusted proxy. An observer
59
54
  that logs should keep the same rule; nothing in an event lets it break it.
60
55
 
61
56
  ## Observers
@@ -98,12 +93,12 @@ call. A hook that throws or returns a rejecting promise is isolated: the
98
93
  request is unaffected, the next observer still runs, `observers.errors` in
99
94
  the snapshot increments and one `observer` record goes to the default log.
100
95
  Nothing is retried; an observer that needs delivery guarantees owns its own
101
- queue, like the [link event channel](MONITORING.md#the-link-event-channel).
96
+ queue.
102
97
 
103
98
  `onMetrics` receives a fresh snapshot every `metricsIntervalMs`
104
99
  (`startServer` option, `0` off by default, 1 s to 1 h) and once at `close()`.
105
- `onClose` runs in reverse order after the runtime and the link channel have
106
- drained. `app.observers` lists the `{ name, version }` pairs.
100
+ `onClose` runs in reverse order after the runtime has closed.
101
+ `app.observers` lists the `{ name, version }` pairs.
107
102
 
108
103
  `createRuntime(project, { observers })` takes the same array for embedding
109
104
  without the server: the runtime's own sink and counters are then yours, and
@@ -124,7 +119,7 @@ and capped at 10 000 keys.
124
119
 
125
120
  | Field | Type | Meaning |
126
121
  |---|---|---|
127
- | `version` | gauge | Snapshot shape version, currently `1`. |
122
+ | `version` | gauge | Snapshot shape version, currently `2`. |
128
123
  | `uptimeSeconds`, `rssBytes` | gauge | Process facts. |
129
124
  | `requests.total`, `requests.byStatusClass.{2xx,3xx,4xx,5xx}` | counter | Application responses, including shed 503s. |
130
125
  | `requests.inFlight` | gauge | Requests holding application admission now. |
@@ -135,13 +130,10 @@ and capped at 10 000 keys.
135
130
  | `watch.failed` | counter | Development watcher failures. |
136
131
  | `functionWorkers.started`, `functionWorkers.restarts` | counter | Worker starts and scheduled replacements. |
137
132
  | `functionWorkers.healthySlots`, `functionWorkers.slots` | gauge | Ready slots and configured slots of the serving runtime. |
138
- | `linkStoreWorkers.started`, `linkStoreWorkers.restarts` | counter | The same for link-store connections. |
139
133
  | `policies.throttle.{allowed,exceeded}` | counter | Throttle decisions (see the catalogue for what enforce mode logs). |
140
134
  | `policies.agents.{denied,reported}` | counter | Agents decisions. |
141
135
  | `policies.cache.{hit,stale,miss,store}` | counter | Cache outcomes. |
142
- | `linkRequests.{completed,aborted,missing,disabled,expired,invalid_code,invalid_record,unavailable}` | counter | Dynamic link outcomes, counted whether or not `linkEvents` is set. |
143
136
  | `signals.{accepted,delivered,failed,dropped}` | counter | Best-effort webhook outcomes; exposed as `signals_total` with outcome labels. |
144
- | `linkObserver.failed`, `linkObserver.dropped` | counter | Link collector failures and dropped events. |
145
137
  | `logsDropped` | counter | Records the JSON logger shed. |
146
138
  | `observers.errors` | counter | Observer hooks that threw or rejected. |
147
139
 
@@ -171,7 +163,6 @@ urlcode_function_worker_restarts_total 0
171
163
  urlcode_function_worker_healthy_slots 2
172
164
  urlcode_throttle_total{outcome="exceeded"} 14
173
165
  urlcode_cache_total{outcome="hit"} 511
174
- urlcode_link_requests_total{outcome="completed"} 0
175
166
  urlcode_logs_dropped_total 0
176
167
  urlcode_observer_errors_total 0
177
168
  urlcode_uptime_seconds 86400
@@ -0,0 +1,212 @@
1
+ # Open decisions
2
+
3
+ Status: written 2026-09-19 from a review of the nine repositories. Every item
4
+ here is a maintainer decision that documentation cannot make. Each states what
5
+ is actually true today, what the options are, what it costs to leave open, and
6
+ a recommendation. Nothing here is committed scope; the
7
+ [roadmap](../ROADMAP.md) owns sequence and the
8
+ [readiness register](RELEASE-READINESS.md) owns what is proven.
9
+
10
+ This page exists because several documented positions have drifted apart from
11
+ each other and from the source. The drift is not the decisions themselves — it
12
+ is that they were never recorded in one place where the next contributor, human
13
+ or agent, would look.
14
+
15
+ ## 1. Publish `0.4.0-alpha.2` — decided
16
+
17
+ **Decided 2026-09-19: publish `alpha.2`.** The recommendation below was taken
18
+ rather than walking back a correct declaration. The alignment is prepared
19
+ across all nine repositories and the order is recorded in
20
+ [the version register](VERSIONS.md); what remains is merging and tagging.
21
+
22
+ The second hazard was taken with it: `@jimhoyd/urlcode-auth` moves `latest`
23
+ up to `0.1.0-alpha.2` so that a plain `npm install` no longer resolves a build
24
+ older than the floor `@jimhoyd/urlcode-admin` declares. That is a registry
25
+ operation (`npm dist-tag add`), not a source change, and it is listed in the
26
+ release steps.
27
+
28
+ The original entry, for the record:
29
+
30
+ > **Today:** the repository source is `0.4.0-alpha.2`. The npm dist-tags are
31
+ > `latest = 0.3.0` and `alpha = 0.4.0-alpha.1`. `alpha.2` carries the
32
+ > trusted-by-default execution change ([the decision](SPIKE-DEFAULT-TRUST-MODEL.md)).
33
+ >
34
+ > **Why it blocks other things:** `@jimhoyd/urlcode-middleware@0.1.0-alpha.1` is
35
+ > published with `peerDependencies: { "@jimhoyd/urlcode": ">=0.4.0-alpha.2" }`.
36
+ > No published core version satisfies that range, so a registry install of that
37
+ > package cannot resolve its peer. The range is correct; the publication order
38
+ > was not.
39
+ >
40
+ > **Recommendation:** publish `alpha.2`. It is the only option that does not
41
+ > require walking back a correct declaration.
42
+
43
+ ## 2. Consolidate the repositories, or commit to the split
44
+
45
+ **Today:** nine repositories. [The monorepo spike](SPIKE-MONOREPO.md) is
46
+ complete, its layout is marked decided (option A), its migration mechanics and
47
+ sequencing are written, and it has three open questions for the maintainer. It
48
+ has been neither accepted nor rejected. It was drafted for four repositories;
49
+ `urlcode-dynamic-link`, `urlcode-middleware` and `urlcode-short` have appeared
50
+ since.
51
+
52
+ **Cost of leaving it open:** the migration gets larger with every repository
53
+ added, and the coordination work the spike describes — pinning reviewed peer
54
+ revisions by hand, chasing prose across repositories when a contract changes —
55
+ is paid again on every contract change in the meantime.
56
+
57
+ **Recommendation:** decide before the next structural change, in either
58
+ direction. A recorded "no, and here is what we do instead about cross-repo
59
+ drift" closes this as well as a yes does.
60
+
61
+ ## 3. One way to attach middleware, or two
62
+
63
+ **Today:** core has the native `routes.<path>.middleware[]` array.
64
+ `urlcode-middleware` provides the same capability through the extension seam
65
+ and states that core's native array "keeps working unchanged".
66
+ [The layering spike](SPIKE-CORE-LAYERING.md) proposed *extracting* middleware
67
+ to narrow core to YAML plus `function`; what exists is an addition alongside it.
68
+
69
+ **Why this is a principle question:** the
70
+ [extension model review](SPIKE-EXTENSION-MODEL.md) rejected an earlier design
71
+ for exactly this reason — two ways to protect a route would drift apart — and
72
+ resolved it by removing one. The same test applies here.
73
+
74
+ **Options:** deprecate the native array on a stated timeline and complete the
75
+ extraction; or keep both and document precisely when each is correct, treating
76
+ the extension as a bounded variant rather than a successor.
77
+
78
+ **Recommendation:** decide explicitly and write the answer into both
79
+ repositories. Either is defensible; leaving both undescribed is not.
80
+
81
+ ## 4. Where the backlog lives
82
+
83
+ **Today:** [next steps](NEXT-STEPS.md) holds roughly thirty tracked items
84
+ across nine phases. The repository has one open issue (#58). `AGENTS.md`
85
+ instructs contributors and agents to file what they find as issues on the
86
+ owning repository.
87
+
88
+ **The contradiction:** the instruction points at the issue tracker; the actual
89
+ plan is a Markdown file that no tracker reflects. A plan document also has no
90
+ state, so items stay written as future work after they ship — which is how
91
+ route-level `auth` came to be described as an invented field in merged agent
92
+ guidance (see item 7).
93
+
94
+ **Options:** move the phase items into issues and keep the document as the
95
+ narrative that links them; or keep the document as the system of record and
96
+ amend `AGENTS.md` to say so.
97
+
98
+ **Recommendation:** issues for items, document for sequence. It is the only
99
+ arrangement where "done" is recorded automatically.
100
+
101
+ ## 5. Gate the business suite on evidence
102
+
103
+ **Today:** [the business suite spike](SPIKE-BUSINESS-SUITE.md) proposes seven
104
+ applications. [Project direction](PROJECT-DIRECTION.md) states the evidence
105
+ test: the framework grows from measured repetition, not from a list of things
106
+ applications might need. Phase 6 of [next steps](NEXT-STEPS.md) says candidate
107
+ areas are built only when the repetition log shows them repeating. The
108
+ repetition log does not exist yet, and the Phase 0 agent benchmark has not been
109
+ run.
110
+
111
+ **Recommendation:** record on the spike itself that it is gated behind the
112
+ benchmark and the repetition log, or amend the evidence test. Holding both
113
+ positions unannotated makes the stated principle decorative.
114
+
115
+ ## 6. Finish the documentation move, or describe the split
116
+
117
+ **Today:** `AGENTS.md` states that urlcode-docs is the documentation home and
118
+ the only place readers are sent, that new reader-facing pages go there, and
119
+ that pages still under `docs/` here are being migrated. Roughly forty
120
+ reader-facing pages remain in this repository, several duplicated in
121
+ urlcode-docs.
122
+
123
+ **Observed consequence:** the same fact drifts between copies. The public
124
+ documentation site carried the correct trusted-by-default contract while
125
+ `docs/OPERATIONS.md` in this repository still described functions as untrusted
126
+ and isolated by default. <!-- trust-model-prose: historical -->
127
+
128
+ **Options:** finish the migration against a dated list; or state which pages
129
+ stay here permanently and why, and add a check that fails when a new
130
+ reader-facing page appears beside them.
131
+
132
+ ## 7. A review window, and a check that does not need one
133
+
134
+ **Today:** [governance](../GOVERNANCE.md) records one maintainer, a required
135
+ approval count of zero, and that "an independent human review is not yet
136
+ guaranteed". Pull requests and CI are mandatory; a second pair of eyes is not.
137
+
138
+ **What that permitted, concretely:** on 2026-09-19, three coordinated pull
139
+ requests (urlcode#158, urlcode-template#6, urlcode-docs#17) opened and merged
140
+ within nine minutes. They recorded a genuine improvement — the declarative-first
141
+ principle, propagated to every copy in one change — and alongside it the
142
+ statement that agents must "never invent an `auth` field". Route-level `auth` is
143
+ implemented: it is defined in `schemas/urlcode.schema.json` as a short form
144
+ expanding to `policies.extensions.auth`, expanded in `src/config.ts`, typed in
145
+ `src/types.ts`, and asserted in `test/recipes.test.ts`. The guidance now
146
+ instructs agents away from a supported declarative short form and toward the
147
+ lower-level policy form — the opposite of the principle the same change
148
+ introduced — in the generated project guide, the bundled starter, both
149
+ authoring skills, the packaged plugin skill and the two generated `llms` files.
150
+
151
+ Nothing failed. Lint, typecheck, generated-resource checks, the package smoke
152
+ test and 501 tests all passed, because no check compares what the guidance
153
+ claims against what the schema implements.
154
+
155
+ **The check, now implemented.** `scripts/check-guidance-claims.ts` runs inside
156
+ `npm run check` and exits non-zero on a contradiction between agent-facing
157
+ guidance and the schema, so this class of error fails CI instead of depending on
158
+ a reviewer's attention:
159
+
160
+ - Inputs: the agent-facing surfaces — `src/agents-guide.ts`,
161
+ `starters/default/AGENTS.md`, `skills/urlcode/SKILL.md`,
162
+ `.claude/skills/*/SKILL.md`, `packaging/claude-plugin/skills/*/SKILL.md`,
163
+ `llms.txt`, `llms-full.txt`, `docs/AI-AUTHORING.md`.
164
+ - Assertion one: every YAML field named as valid in those files resolves in
165
+ `schemas/urlcode.schema.json`. A guidance file may not teach a field the
166
+ schema does not accept.
167
+ - Assertion two, the one that would have caught this: no field that the schema
168
+ *does* define is described as unsupported, invented or nonexistent. Match the
169
+ negative phrasings deliberately ("never invent", "does not exist",
170
+ "unsupported field") within a short window of a schema-resolvable field name.
171
+ - Assertion three: handlers listed as available match the capability catalog,
172
+ so a removed handler (`link`, extracted to `urlcode-dynamic-link` in
173
+ `f7dbe54`) cannot linger in generated guidance.
174
+ - Exits non-zero on violation, with a documented `<!-- guidance-claims: ignore -->`
175
+ marker for text that is deliberately about another version. Verified against
176
+ both regressions: reintroducing the "never invent an `auth` field" sentence
177
+ fails the check, and adding the removed `link` handler to the inventory line
178
+ fails it.
179
+
180
+ What remains a decision: whether `npm run check` membership is enough, or the
181
+ check should also be named in the repository's required status checks so it
182
+ cannot be bypassed.
183
+
184
+ **Recommendation:** the check is in; keep it required. It is the part that does
185
+ not depend on a person being available. Whether to
186
+ also raise the required approval count is a separate call, and
187
+ [governance](../GOVERNANCE.md) already states the condition — when the trusted
188
+ maintainer team grows.
189
+
190
+ ## 8. One publishing convention
191
+
192
+ **Today:** `urlcode-dynamic-link` and `urlcode-middleware` keep
193
+ `"private": true` on their main branch and drop it in the release commit;
194
+ `urlcode-auth`, `urlcode-admin` and `urlcode-ui` do not. All are published.
195
+ Both practices are defensible; having both means the flag no longer indicates
196
+ publication state.
197
+
198
+ **Recommendation:** pick one and state it where release process is documented.
199
+
200
+ ## 9. Adjudicate the vendored skill drift
201
+
202
+ **Today:** `npm run check:downstream-skills` reports that `urlcode-template`'s
203
+ vendored authoring and operations skills differ from core's current `main` by
204
+ 79 and 91 lines. The report is advisory by design and never fails, because a
205
+ downstream repository can correctly pin an older published core version
206
+ ([issue 155](https://github.com/jimhoyd-com/urlcode/issues/155)).
207
+
208
+ **What is missing:** the judgment the report defers. No one has read the two
209
+ diffs and recorded which side is right for the template's `0.4.0-alpha.1` pin.
210
+
211
+ **Recommendation:** review both diffs once, record the verdict, and re-run the
212
+ report when the template repins.
@@ -1,22 +1,21 @@
1
1
  # Operational evidence and deployment acceptance
2
2
 
3
- `node scripts/operational-drills.ts` creates and deletes its own temporary project
4
- and store. It runs mixed real HTTP requests against native redirects, isolated
5
- functions and live links while updating records; asserts responses; rejects a bad
6
- reload; activates and rolls back a configuration; then closes all users of the
7
- store, explicitly verifies a successful WAL checkpoint, and restores a copied database. It checks integrity, record/version and
8
- latest durable audit revision. `URLCODE_SOAK_SECONDS=60` selects a longer run
9
- (1–3600 seconds, default 5). Output is JSON with request count, batch p99 duration,
10
- RSS and restore time. Batch latency is not per-request p99 or a capacity promise.
11
- CI runs the short drill on all six supported Node/OS combinations.
3
+ `node scripts/operational-drills.ts` creates and deletes its own temporary
4
+ project. It runs mixed real HTTP requests against native redirects and
5
+ `function` routes; asserts responses; rejects a bad reload; and activates and
6
+ rolls back a configuration. `URLCODE_SOAK_SECONDS=60` selects a longer run
7
+ (1–3600 seconds, default 5). Output is JSON with request count, batch p99
8
+ duration and RSS. Batch latency is not per-request p99 or a capacity promise.
9
+ CI's `verify` job runs the short drill across a 3-Node (22/24/26) × 3-OS
10
+ (Linux/macOS/Windows) matrix, nine combinations, but only on a push to `main`.
11
+ A pull request runs the same drill across all three Node versions on Linux
12
+ only (3 of the 9 combinations); the macOS/Windows legs only run once a PR
13
+ merges, per `.github/workflows/ci.yml`'s matrix.
12
14
 
13
- The container job additionally uses `--disk-full-dir /state` on a disposable
14
- 16 MiB tmpfs. It reserves recovery space, writes until a real volume exhaustion
15
- failure, checks available disk space, releases the reserve, proves a subsequent
16
- mutation succeeds, and checks integrity and equal committed link/audit counts.
17
- Never point this option at production storage: it intentionally consumes up to
18
- 84 MiB of writes in a newly created temporary child directory. The CI mount is
19
- nonpersistent. An I/O device failure or power cut is a different failure mode.
15
+ Core has no durable store of its own, so this drill has no backup/restore or
16
+ disk-exhaustion exercise: a future extension package owning durable state
17
+ (such as the planned `urlcode-dynamic-link`) is responsible for its own
18
+ persistence proof once it exists.
20
19
 
21
20
  ## Required proof on the intended deployment
22
21
 
@@ -27,19 +26,16 @@ substitute for it. The deployment owner must record:
27
26
 
28
27
  | Exercise | Acceptance evidence |
29
28
  |---|---|
30
- | Soak | At least 24 hours at expected peak and burst load through actual TLS/ingress; native/function/live-link mix, slow clients, response correctness, p50/p95/p99, throughput, error/rejection rate, RSS plateau, CPU, FD and disk/WAL growth. Define numerical SLOs before starting |
31
- | Restore | Restore a consistent backup onto a separate host/volume. Reconcile collection counts, latest committed revisions and audit journal; measure RPO/RTO against agreed targets. Test encrypted backup access and credential recovery |
32
- | Logical export/restore | Run `links export` against a store under write load, restore it with `links import` onto a separate store, and reconcile record counts and every field. Prove the export's digest verifies, that a truncated stream is rejected, and that stale management ETags are discarded because the restore reassigns versions. An export carries no audit journal, so pair it with a file backup |
33
- | Kill/restart | Kill server and writer with outstanding reads/writes. Reconcile uncertain mutations by version/request ID, run integrity checks, restore readiness, and prove no duplicate successful conditional writes |
34
- | Disk/log exhaustion | Fill the actual disposable staging storage type and log destination. Verify bounded 503s, audit/mutation atomicity, alert delivery through an independent sink, reserve-space recovery and readiness after recovery |
35
- | Rollback | Deploy candidate by exact digest beside last-good, run route assertions, switch ingress, drain, then switch back. Verify configuration/policy compatibility and audit continuity; never downgrade to an unaudited writer |
36
- | Monitoring | Deliver test alerts for missing logs, sustained errors, pool rejection/failure, low disk, restart storms, backup age and failed readiness to a named on-call owner |
29
+ | Soak | At least 24 hours at expected peak and burst load through actual TLS/ingress; native/function mix, slow clients, response correctness, p50/p95/p99, throughput, error/rejection rate, RSS plateau, CPU and FD growth. Define numerical SLOs before starting |
30
+ | Kill/restart | Kill the server with outstanding requests. Reconcile uncertain mutations by request ID, restore readiness, and prove no duplicate side effects from application code |
31
+ | Rollback | Deploy candidate by exact digest beside last-good, run route assertions, switch ingress, drain, then switch back. Verify configuration/policy compatibility |
32
+ | Monitoring | Deliver test alerts for missing logs, sustained errors, pool rejection/failure, low disk, restart storms and failed readiness to a named on-call owner |
33
+
34
+ If your deployment adds a durable-state extension, add that extension's own
35
+ backup/restore, logical export/import and disk/log exhaustion exercises to
36
+ this table; core's proof above does not cover them.
37
37
 
38
38
  Record date, operator, source/app/policy/image digests, topology, hardware/limits,
39
- commands, duration, synthetic dataset size, raw metrics/log locations, result,
40
- RPO/RTO and unresolved findings. A reviewer signs the acceptance record; a blank
41
- record is not a pass. Do not copy only the main file of a live WAL database.
42
- The executable local drill uses a fully quiesced, explicitly checkpointed database intentionally. A last
43
- read-only connection can leave WAL frames even after every connection closes;
44
- closing alone is insufficient proof that the main database file is a full backup. For a
45
- live-backup system, prove its SQLite-consistent snapshot method separately.
39
+ commands, duration, synthetic dataset size, raw metrics/log locations, result
40
+ and unresolved findings. A reviewer signs the acceptance record; a blank
41
+ record is not a pass.
@@ -45,8 +45,10 @@ docker run --rm --name my-links \
45
45
  ```
46
46
 
47
47
  Replace the example mount with your app. The image uses the unprivileged `node`
48
- user; ensure mounted config/functions are readable by it. Only operator-owned components such as the optional link store can use writable
49
- mounts. Sandboxed application functions cannot access mounted files or installed
48
+ user; ensure mounted config/functions are readable by it. Core has no writable
49
+ mount of its own; a future mount-based extension (like `auth`/`admin`, see
50
+ [extensions](EXTENSIONS.md)) is the place for operator-owned writable state.
51
+ Sandboxed application functions cannot access mounted files or installed
50
52
  Node packages. The resource values above illustrate
51
53
  container limits, not a sizing recommendation; large configuration compilation
52
54
  can need more memory. Measure your workload. Tag/redeploy immutable image digests
@@ -63,7 +65,10 @@ at the proxy; they are unauthenticated and reveal route count/config digest.
63
65
 
64
66
  If functions perform sensitive actions, implement authentication and authorization
65
67
  in the application. A short URL is not automatically an access-control mechanism.
66
- Functions are untrusted and isolated in WASM by default. Keep separate deployment
68
+ Functions and middleware run trusted and unsandboxed by default, in the host
69
+ process with full Node, filesystem and network access; a route that declares
70
+ `sandbox: true` runs isolated in QuickJS/WebAssembly instead (see
71
+ [function security](FUNCTION-SECURITY.md)). Keep separate deployment
67
72
  processes/containers and narrowly scoped credentials as additional boundaries.
68
73
  Do not expose a public code-upload/multi-tenant service on the basis of the self-hosted release alone
69
74
  without separate security review and stronger service-level containment.
@@ -89,14 +94,14 @@ production does not watch or refresh secret values automatically.
89
94
 
90
95
  - `GET /_urlcode/health`: process liveness.
91
96
  - `GET /_urlcode/ready`: 200 when the active snapshot and all function workers
92
- are available and configured link-store readers are healthy; 503 while a worker/store
97
+ are available; 503 while a worker
93
98
  is unavailable. Busy workers alone do not
94
- mark readiness down. A failed worker or store connection is replaced with
99
+ mark readiness down. A failed worker is replaced with
95
100
  exponential backoff (250 ms doubling to a 30-second ceiling) and readiness
96
101
  reports 503 until every slot is serving again. Replacement does not stop, so a
97
102
  request-triggered deadline cannot disable functions until an operator restarts;
98
103
  a cause that keeps recurring keeps the instance shedding load and needs an
99
- operator. Alert on sustained `function_worker`/`link_store_worker` restart events.
104
+ operator. Alert on sustained `function_worker` restart events.
100
105
  - Probes are answered from their own admission budget (16 by default,
101
106
  `--max-in-flight-health`), so they stay available while the application is
102
107
  saturated without being an unmetered endpoint. They are unauthenticated and
@@ -125,11 +130,15 @@ production does not watch or refresh secret values automatically.
125
130
  admitted through response completion; excess requests receive 503. Health probes
126
131
  remain available under admission saturation. A 15-second socket inactivity
127
132
  timeout closes stalled readers/writers. Proxy timeouts/rate limits still matter.
128
- - Functions: 2 concurrent workers (`--workers`), no queue, 5-second deadline
129
- (`--function-timeout-ms`), 1 MiB buffered response (`--max-response-bytes`) and
130
- 16 KiB response headers. Saturation 503; timeout 504; error 502.
133
+ - Functions: a `sandbox: true` route gets 2 concurrent workers (`--workers`),
134
+ no queue and a 5-second deadline (`--function-timeout-ms`); a trusted route
135
+ (`sandbox` false or absent, the default) shares the in-flight admission cap
136
+ instead of a worker pool and races the same deadline. Either mode buffers
137
+ 1 MiB of response (`--max-response-bytes`) and 16 KiB response headers.
138
+ Saturation 503; timeout 504; error 502.
131
139
  QuickJS guests have a 32 MiB heap and 512 KiB stack budget and no network or
132
- host capabilities. Outer workers have additional V8 limits. Total process/WASM
140
+ host capabilities; a trusted route has neither budget and full Node access.
141
+ Outer workers have additional V8 limits. Total process/WASM
133
142
  memory still needs deployment-level limits; do not equate guest budget with RSS.
134
143
 
135
144
  `urlcode serve`/`dev` and the JavaScript server API both configure workers,
@@ -140,24 +149,14 @@ deployment controls, not portable route behavior. Horizontal replicas
140
149
  must use identical application/config versions and secret bindings. In-memory
141
150
  function state is reset after every invocation, not durable/shared application state.
142
151
  General application storage needs a future explicit capability broker; no
143
- storage/network access is exposed to the guest. The optional native
144
- [link store](DYNAMIC-LINKS.md) supports live short-link records on one host.
152
+ storage/network access is exposed to the guest. Stored short links are moving
153
+ to a future `urlcode-dynamic-link` extension package, not yet published; core
154
+ no longer has a native link store.
145
155
 
146
156
  The health version combines route-definition and asset-representation digests;
147
157
  it does not identify the complete function/runtime release. Record runtime commit,
148
158
  application commit, dependency locks and image digest in your deployment system.
149
159
 
150
- ## Optional dynamic-link deployment
151
-
152
- Keep SQLite and management tokens outside the application, in a private durable
153
- local directory. Initialize through `links init/create`, bind public serving with
154
- `--link-store`, and expose management on a separate private listener. Restrict
155
- its token to your trusted backend; apply ingress limits and backups. Public
156
- serving opens read-only pools; management has a separate writer and read pool.
157
- Budget connections across processes and monitor writer health separately. See
158
- [dynamic-link operations](DYNAMIC-LINKS.md). Multiple host replicas must not share
159
- this file over a network filesystem; no distributed adapter is included yet.
160
-
161
160
  ## Deployment and rollback procedure
162
161
 
163
162
  1. Build a candidate from pinned runtime/application revisions and lockfiles.
@@ -174,7 +173,7 @@ this file over a network filesystem; no distributed adapter is included yet.
174
173
  This is an operator procedure, not an implemented deployment control plane.
175
174
  Rollback cannot undo a function's external side effects or migrate an app's
176
175
  state automatically. Plan those independently. Keep Git definitions backed up;
177
- back up any app-owned persistent state separately. YAML routes require no database; dynamic link records require separate backups.
176
+ back up any app-owned persistent state separately. YAML routes require no database.
178
177
 
179
178
  ## Capacity and incident planning
180
179
 
@@ -200,11 +199,3 @@ adapter is made by the current release.
200
199
  The [2026-09-16 internal audit](SECURITY-AUDIT.md) records fixes, regression evidence
201
200
  and remaining security/operational gates. This is not an independent assessment.
202
201
 
203
- ## Management hardening baseline
204
-
205
- Management is now restricted to literal loopback addresses. Prefer `--auth-file`
206
- for individual expiring, revocable credentials with collection/action scopes.
207
- Every successful built-in store mutation has an atomic, durable SQLite audit row;
208
- HTTP request logs remain best effort. See [management security](MANAGEMENT-SECURITY.md)
209
- for policy examples, compatibility, archival and rollback requirements, and
210
- [operational proof](OPERATIONAL-PROOF.md) for executable recovery drills.
package/docs/POLICIES.md CHANGED
@@ -40,11 +40,22 @@ routes:
40
40
  ```
41
41
 
42
42
  `policies` at the top level sets project defaults; `routes.<path>.policies`
43
- adjusts them for one route. One route-level short form exists: `auth`, which
43
+ adjusts them for one route. Two route-level short forms exist. `auth`
44
44
  expands to `policies.extensions.auth` when the project declares an auth
45
- [extension](EXTENSIONS.md). The pattern is reserved for `cache`: a route-level
46
- `cache: {strategy, maxAge}` may later expand to `policies.cache` the same way,
47
- but it is not implemented, and `policies.cache` is the only cache form today. Both accept the same keys: `profile` plus one entry
45
+ [extension](EXTENSIONS.md). `cache: {strategy, maxAge, ...}` expands to
46
+ `policies.cache` the same way the compiler merges it into that route's
47
+ `policies` before anything else reads the project, so `routes`, `audit` and
48
+ `explain` see only the expanded long form, and it is refused alongside a
49
+ direct `policies.cache` on the same route (use one form):
50
+
51
+ ```yaml
52
+ routes:
53
+ /feed:
54
+ function: { source: functions/feed.mjs }
55
+ cache: { strategy: swr, maxAge: 30 } # expands to policies: { cache: {...} }
56
+ ```
57
+
58
+ Both accept the same keys: `profile` plus one entry
48
59
  per policy, each either an object or `false`. Unknown keys fail validation, as
49
60
  everywhere in the project format. The
50
61
  [field reference](YAML-REFERENCE.md) lists every accepted field with its
package/docs/PRERENDER.md CHANGED
@@ -14,7 +14,7 @@ and no capability.
14
14
  ## Why render ahead of time
15
15
 
16
16
  A function response body is readable by middleware through `text()`. A native
17
- `page`, `static`, `download`, `redirect`, `respond` or `link` body is not — that
17
+ `page`, `static`, `download`, `redirect` or `respond` body is not — that
18
18
  opacity is a deliberate contract, not a gap, and it is what keeps native file
19
19
  serving free of guest code. See [middleware](MIDDLEWARE.md).
20
20
 
@@ -165,7 +165,7 @@ service and redirect; provider asset adapters are not implemented.
165
165
 
166
166
  The first two rows above are the sandbox's snapshot budgets: at most 127 guest
167
167
  modules and 4 MiB of module source in one snapshot. They are deliberate — part
168
- of what [function security](FUNCTION-SECURITY.md) promises about untrusted guest
168
+ of what [function security](FUNCTION-SECURITY.md) promises about sandboxed guest
169
169
  code — and the render step does not relax them for trusted generated content.
170
170
  Serving a project that crosses either still fails at startup, naming the module
171
171
  that crossed it:
@@ -6,12 +6,31 @@ under. It describes intent and
6
6
  boundaries, not a delivery schedule; the [roadmap](../ROADMAP.md) owns sequence
7
7
  and the [readiness register](RELEASE-READINESS.md) owns what is proven.
8
8
 
9
+ ## Design principle: declarative-first
10
+
11
+ > Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
12
+
13
+ This is the default for human authors, coding agents, examples and framework
14
+ changes. Before writing application code, check the installed version's declarative
15
+ primitives, YAML configuration, policies, supported extensions and reusable recipes
16
+ or templates. Prefer the highest-level supported abstraction that satisfies the
17
+ requirement; do not recreate framework behavior in a lower-level handler.
18
+
19
+ Custom functions or middleware remain valid for application-specific behavior the
20
+ framework cannot express. Keep that code focused, explain the missing capability,
21
+ and report reusable gaps to the owning repository. Never invent YAML fields or
22
+ bypass target limits, sandbox isolation or operator grants to avoid custom code.
23
+
9
24
  ## What URLCode is
10
25
 
11
26
  A portable runtime for programmable URL behavior. A project declares its public
12
- URL surface in YAML, adds isolated JavaScript only where declarative handlers are
27
+ URL surface in YAML, adds JavaScript only where declarative handlers are
13
28
  not enough, and runs the same definition locally, in a container, or on operator
14
- infrastructure. The project format is deliberately bounded so a runtime can
29
+ infrastructure. That JavaScript runs trusted, in the host process, like any
30
+ other project code, unless the project isolates a specific route with
31
+ `sandbox: true` (docs/SPIKE-DEFAULT-TRUST-MODEL.md) — a judgment call the
32
+ project makes per route, not a default the runtime imposes on all guest code.
33
+ The project format is deliberately bounded so a runtime can
15
34
  validate it, inspect it, test it and eventually carry it across hosting providers.
16
35
 
17
36
  Git owns route definitions and code. Operators own credentials, storage and
@@ -19,12 +38,18 @@ capability grants. Application data stays in the operator's systems.
19
38
 
20
39
  ## What URLCode is not
21
40
 
22
- - **Not a URL shortener.** Short links are one handler beside redirects,
23
- validated responses, request functions, middleware, pages, static assets and
24
- downloads. The [live-link store](DYNAMIC-LINKS.md) is optional and single-host.
25
- - **Not a general Node web framework.** Guest code runs inside WASM isolation
26
- with no ambient filesystem, network or Node APIs. Behavior that cannot be
27
- expressed in the bounded contract is rejected rather than emulated.
41
+ - **Not a URL shortener.** Core is redirects, validated responses, request
42
+ functions, middleware, pages, static assets and downloads. Stored short
43
+ links are moving to a future `urlcode-dynamic-link` extension package
44
+ (mount-based, like `auth`/`admin`, not yet published), not a core handler.
45
+ - **Not a general Node web framework.** There is no framework code to write
46
+ for routing, validation, middleware wiring or policies those are declared
47
+ in YAML and enforced by the runtime. Function/middleware code that needs
48
+ isolation from the host (untrusted input, an unreviewed contribution, a
49
+ particularly sensitive secret) opts into `sandbox: true`, which runs it
50
+ inside WASM isolation with no ambient filesystem, network or Node APIs.
51
+ Behavior that cannot be expressed in the bounded contract of a sandboxed
52
+ route is rejected rather than emulated.
28
53
  - **Not a hosting account system.** There is no end-user identity, billing or
29
54
  public account surface. Management is a private operator API.
30
55
  - **Not a provider configuration format.** Provider infrastructure settings do
@@ -54,7 +79,7 @@ and the database owns the machinery. One level up, a route should read
54
79
 
55
80
  ```yaml
56
81
  /admin:
57
- auth: { required: true, roles: [admin] }
82
+ auth: { role: admin }
58
83
  function: { source: functions/admin.mjs }
59
84
  ```
60
85