@jimhoyd/urlcode 0.4.0-alpha.2 → 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 (115) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +17 -19
  2. package/.claude/skills/urlcode-operations/SKILL.md +9 -9
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +44 -54
  6. package/ROADMAP.md +61 -376
  7. package/dist/BUILD-MANIFEST.json +15 -15
  8. package/dist/agents-guide.js +6 -6
  9. package/dist/build-static.js +2 -0
  10. package/dist/cli.js +3 -3
  11. package/dist/explain-cli.js +4 -2
  12. package/dist/explain.js +7 -1
  13. package/dist/extensions.js +1 -1
  14. package/dist/manifest.js +7 -1
  15. package/dist/mcp-authoring.js +2 -2
  16. package/dist/mcp.js +1 -1
  17. package/dist/policies.js +3 -1
  18. package/dist/prerender.js +4 -0
  19. package/dist/readiness.js +3 -0
  20. package/dist/route-diff.js +12 -5
  21. package/dist/types/explain.d.ts +6 -0
  22. package/dist/types/extensions.d.ts +1 -1
  23. package/dist/types/manifest.d.ts +5 -1
  24. package/dist/types/readiness.d.ts +2 -0
  25. package/dist/types/types.d.ts +8 -1
  26. package/dist/types.js +8 -1
  27. package/dist/typescript-authoring.js +18 -7
  28. package/docs/AI-AUTHORING.md +10 -6
  29. package/docs/ASSETS.md +2 -1
  30. package/docs/AUTH-BACKUP.md +32 -0
  31. package/docs/BEST-PRACTICES.md +17 -9
  32. package/docs/CAPABILITIES.md +1 -1
  33. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  34. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  35. package/docs/CI.md +8 -3
  36. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  37. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  38. package/docs/EXTENSIONS.md +15 -5
  39. package/docs/FRAMEWORK.md +22 -18
  40. package/docs/FUNCTION-SECURITY.md +44 -0
  41. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  42. package/docs/OPEN-DECISIONS.md +184 -212
  43. package/docs/OPERATIONAL-PROOF.md +3 -3
  44. package/docs/OPERATIONS.md +3 -3
  45. package/docs/POLICIES.md +13 -5
  46. package/docs/PRERENDER.md +23 -11
  47. package/docs/PROJECT-DIRECTION.md +3 -3
  48. package/docs/READINESS.md +6 -2
  49. package/docs/README.md +18 -41
  50. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  51. package/docs/RELEASE-READINESS.md +6 -6
  52. package/docs/RELEASE-SECURITY.md +68 -168
  53. package/docs/RESILIENCE.md +3 -3
  54. package/docs/ROUTING.md +3 -4
  55. package/docs/SECURITY-AUDIT.md +2 -2
  56. package/docs/SPECIFICATION.md +7 -5
  57. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
  58. package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
  59. package/docs/SPIKE-CORE-LAYERING.md +48 -17
  60. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
  61. package/docs/SPIKE-LAMBDA-COMPILE.md +8 -0
  62. package/docs/STATIC.md +14 -3
  63. package/docs/TOOLING.md +4 -3
  64. package/docs/TYPESCRIPT-AUTHORING.md +6 -1
  65. package/docs/VERSION-ALIGNMENT.md +46 -205
  66. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  67. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
  68. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
  69. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
  70. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  71. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  72. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  73. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +10 -0
  74. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
  75. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  76. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
  77. package/docs/archive/README.md +27 -0
  78. package/docs/policies/agents.md +1 -1
  79. package/docs/policies/compression.md +3 -2
  80. package/docs/policies/security.md +3 -2
  81. package/examples/assets/example.yaml +1 -1
  82. package/examples/cookbook/route-index.json +1 -1
  83. package/examples/cookbook/routes/middleware.yaml +1 -1
  84. package/examples/prerender/README.md +14 -6
  85. package/examples/prerender/functions/page.mjs +4 -2
  86. package/examples/prerender/middleware/template.mjs +1 -1
  87. package/examples/prerender/prerender.mjs +1 -1
  88. package/examples/prerender/urlcode.yaml +8 -4
  89. package/llms-full.txt +117 -45
  90. package/llms.txt +5 -4
  91. package/package.json +24 -4
  92. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  93. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
  94. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
  95. package/recipes/authenticated-json-api/README.md +4 -3
  96. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  97. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  98. package/recipes/contact-form/functions/contact.mjs +2 -1
  99. package/recipes/contact-form/recipe.yaml +2 -2
  100. package/recipes/cors-api/README.md +2 -2
  101. package/recipes/cors-api/recipe.yaml +1 -1
  102. package/recipes/health-page/README.md +1 -1
  103. package/recipes/json-api/README.md +1 -1
  104. package/recipes/json-api/recipe.yaml +3 -3
  105. package/recipes/middleware/README.md +8 -4
  106. package/recipes/protected-download/README.md +1 -1
  107. package/recipes/protected-download/recipe.yaml +1 -1
  108. package/recipes/static-plus-api/README.md +2 -2
  109. package/recipes/static-plus-api/public/index.html +1 -1
  110. package/recipes/static-plus-api/recipe.yaml +1 -1
  111. package/recipes/static-plus-api/urlcode.yaml +1 -1
  112. package/recipes/typescript/recipe.yaml +4 -4
  113. package/skills/urlcode/SKILL.md +6 -6
  114. package/starters/default/AGENTS.md +6 -6
  115. package/docs/SPIKE-MONOREPO.md +0 -322
@@ -1,17 +1,34 @@
1
1
  # Spike: layering `link` and `middleware` out of core
2
2
 
3
+ > Review update, 2026-09-19: Current baseline: **both extractions this spike
4
+ > proposes are over, and neither ended in a shipped separate package.** Link
5
+ > extraction completed and the resulting package was then retired. Middleware
6
+ > extraction never completed: core still implements native middleware, and the
7
+ > separate middleware extension — which added a different API with per-entry
8
+ > sandboxing — has itself been unpublished at `0.1.0-alpha.2` and its
9
+ > repository deleted. The extraction direction below is superseded, and it is
10
+ > not an instruction to remove core middleware: core's native `middleware:`
11
+ > array is now the only implementation of per-route middleware there is.
12
+ >
13
+ > An earlier version of this note said "monorepo work is starting now:
14
+ > middleware moves in as a separate package first, and folds into core
15
+ > afterward." Monorepo work is still starting, for core, auth, admin and UI —
16
+ > but the middleware clause is void. There is no package to move in and
17
+ > nothing to fold in afterward.
18
+
19
+
3
20
  Status: proposal, nothing implemented. No code in this repository does any of
4
21
  this, and nothing here is committed scope. This combines two extraction ideas
5
22
  into one spike because they share a mechanism and a sequence, not because
6
23
  either is blocked on the other.
7
24
 
8
- > **Update:** this spike's `link` extraction (Phase 1 below) has since been
9
- > implemented — core no longer has a native `link`/`LinkStore`/`dynamicLinks`
10
- > API; it now lives in the separate
11
- > [`urlcode-dynamic-link`](https://github.com/jimhoyd-com/urlcode-dynamic-link)
12
- > package. Present-tense references to `link` as a core feature elsewhere in
13
- > this document describe the pre-extraction state this spike was analyzing,
14
- > not current core.
25
+ > **Update:** this spike's `link` extraction (Phase 1 below) was implemented
26
+ > core no longer has a native `link`/`LinkStore`/`dynamicLinks` API — and the
27
+ > separate `urlcode-dynamic-link` package that received it has since been
28
+ > retired, unpublished from npm and its repository deleted (September 2026).
29
+ > Nothing owns stored links today. Present-tense references to `link` as a core
30
+ > feature elsewhere in this document describe the pre-extraction state this
31
+ > spike was analyzing, not current core.
15
32
 
16
33
  ## The shape this is aiming at
17
34
 
@@ -41,7 +58,8 @@ honest instead of inventing two different shapes:
41
58
  - **Mount ownership** (`route.extension`, `RuntimeExtension.activate` →
42
59
  `ExtensionInstance.handle`): an extension owns an exclusive `/prefix/*` and
43
60
  answers every request under it. This is the shape `link` needs — it already
44
- behaves like a self-contained mount (`docs/DYNAMIC-LINKS.md`), and
61
+ behaves like a self-contained mount (documented in the since-retired
62
+ `urlcode-dynamic-link` package), and
45
63
  `auth`/`admin` already use it for `/account/*` and `/admin/*`
46
64
  (`docs/FRAMEWORK.md:10-15`).
47
65
  - **Wrap without owning** (`ExtensionInstance.authorize`, called when a route
@@ -161,7 +179,15 @@ before anything here is treated as settled.
161
179
  need to exist or be attached before their Phase 2 work can be written or
162
180
  verified, matching the constraint already flagged for `link`.
163
181
 
164
- ## Repo governance for the two new repos (decided)
182
+ ## Repo governance for the two new repos (decided — both repos since deleted)
183
+
184
+ > **Historical, 2026-09-19.** Both repositories this section governs were
185
+ > created, released once, and then deleted; both packages are unpublished. The
186
+ > decisions below were applied while they existed and are kept as the recorded
187
+ > template for any future extension repository — not as a description of
188
+ > anything live. Note in particular that the "published public from the start"
189
+ > departure recorded below is the one whose consequences are worth reading
190
+ > back: both repos that took it are gone within days of their first release.
165
191
 
166
192
  Both `urlcode-dynamic-link` and `urlcode-middleware` follow `GOVERNANCE.md`
167
193
  and `AGENTS.md` as written, with one explicit decision recorded here per
@@ -176,7 +202,8 @@ AGENTS.md's "do not publish packages without an explicit decision":
176
202
  admins or automation, CODEOWNERS recording ownership. CI/release workflow
177
203
  shape copied from core's `release.yml` (candidate build → audit → pack →
178
204
  attest → publish via trusted publisher, no long-lived npm token), per the
179
- pattern `docs/NEXT-STEPS.md` §2.1 already used for `auth`/`admin`/`ui`.
205
+ pattern §2.1 of the archived plan (`docs/archive/2026-09-19/NEXT-STEPS.md`)
206
+ already used for `auth`/`admin`/`ui`.
180
207
  CodeQL required on main, secret scanning and push protection on, same as
181
208
  core.
182
209
  - **Published public from the start** — both the GitHub repo and the npm
@@ -225,9 +252,10 @@ system tax." The real costs are narrower and different for each:
225
252
  cost described above still applies, in full, to whichever specific
226
253
  `middleware:` wrap a developer explicitly opts into `sandbox: true` — and
227
254
  the same mitigation still matters there: native declarative primitives
228
- (`docs/NEXT-STEPS.md`'s Phase 4.2 `auth: { required: true, roles: [...] }`
229
- direction) reduce how much logic ever needs to reach for the sandboxed
230
- opt-in at all, for the cases where a developer does judge it warranted.
255
+ (the Phase 4.2 `auth: { required: true, role: ... }` direction in the
256
+ archived plan, `docs/archive/2026-09-19/NEXT-STEPS.md`) reduce how much logic
257
+ ever needs to reach for the sandboxed opt-in at all, for the cases where a
258
+ developer does judge it warranted.
231
259
 
232
260
  ## Other core pieces considered and set aside
233
261
 
@@ -253,14 +281,17 @@ part of the smallest complete product:
253
281
  them would shrink core below the "complete product on its own" bar rather
254
282
  than trim it.
255
283
  - **Management API / operator grants / credential policy** — foundation that
256
- extensions themselves depend on (`docs/MANAGEMENT-SECURITY.md`,
284
+ extensions themselves depend on (`docs/archive/2026-09-19/MANAGEMENT-SECURITY.md`,
257
285
  `docs/FUNCTION-SECURITY.md`); moving it out would mean extracting the thing
258
286
  the extraction pattern relies on.
259
287
 
260
288
  Nothing else in core matches the `link`/`middleware` shape today. If a third
261
- candidate is going to be found, `docs/REPETITION-LOG.md`'s discipline (`docs/NEXT-STEPS.md`
262
- Phase 6 — extract from observed repetition, not speculation) is the more
263
- defensible way to find it than continuing to eyeball the handler list.
289
+ candidate is going to be found, the repetition-log discipline of Phase 6 in the
290
+ archived plan (`docs/archive/2026-09-19/NEXT-STEPS.md`) — extract from observed
291
+ repetition, not speculation — is the more defensible way to find it than
292
+ continuing to eyeball the handler list. That plan records Phase 6 as never
293
+ started, and no `docs/REPETITION-LOG.md` was ever written, so the log itself
294
+ would have to be started before it could inform the decision.
264
295
 
265
296
  Recommendation: before either Phase 2 begins, attach `urlcode-auth`,
266
297
  `urlcode-admin` and `urlcode-ui` to a session and confirm (a) their actual
@@ -3,8 +3,7 @@
3
3
  Status: **decided by the maintainer and implemented in `0.4.0-alpha.2`**
4
4
  (`sandbox`/`sandboxReason` in `schemas/urlcode.schema.json`; see the
5
5
  `0.4.0-alpha.2` entry in [the roadmap](../ROADMAP.md) and the resulting contract
6
- in [function security](FUNCTION-SECURITY.md)). `0.4.0-alpha.2` is not yet
7
- published to npm. This overturns an
6
+ in [function security](FUNCTION-SECURITY.md)). The release is published; this file retains the decision rationale. This overturns an
8
7
  explicit, previously-stated project rule — see "What this reverses" below —
9
8
  so treat it as a deliberate, recorded policy change, not a code cleanup. The
10
9
  body below is preserved as written at decision time; where it calls downstream
@@ -153,11 +152,14 @@ with `add_repo` before treating it as settled.
153
152
  and `peers.json` in `auth`/`admin`/`ui` (`docs/SPIKE-CORE-LAYERING.md`'s
154
153
  §2.2 reference) should pin deliberately to a core version that includes
155
154
  this change, not inherit it silently on a routine bump.
156
- - **`urlcode-dynamic-link` (built and published since; was planned when this was written): unaffected.** It's a
155
+ - **`urlcode-dynamic-link` (built and published after this was written, then retired, unpublished and deleted in September 2026): moot.** It was a
157
156
  mount-based extension like `auth`, not a `function`/`middleware` consumer —
158
157
  nothing here changes its design.
159
- - **`urlcode-middleware` (built and published since; was planned when this
160
- was written): same rule applies — decided.** First-party middleware is trusted by default, exactly like
158
+ - **`urlcode-middleware` (built and published since, then retired, unpublished
159
+ and deleted in September 2026): the rule outlived the package — decided.**
160
+ The decision below was made for that package but is not about it: it governs
161
+ core's own native `middleware:` array, which is where per-route middleware
162
+ lives now that the extension package is gone. First-party middleware is trusted by default, exactly like
161
163
  `function`; `sandbox: true` is the same opt-in a developer reaches for
162
164
  when a specific `middleware:` wrap genuinely warrants it (e.g. it's
163
165
  processing input from a source the developer doesn't fully trust). This
@@ -1,5 +1,13 @@
1
1
  # Spike: compiling `function` routes into their own Lambdas
2
2
 
3
+ > Review update, 2026-09-19: Current baseline: trusted Node execution is the default, so fresh invocation
4
+ > state is a guarantee of `sandbox: true` only. AWS/Vercel still reject functions
5
+ > and middleware in `src/capabilities.ts`; changing the default did not implement
6
+ > a provider adapter for them. Re-evaluate the proposed per-route lowering against
7
+ > a single trusted Node deployment per project before choosing either. Removed
8
+ > link APIs in the analysis below are historical.
9
+
10
+
3
11
  Status: proposal, nothing implemented. No code in this repository does any of
4
12
  this, and nothing here is committed scope.
5
13
 
package/docs/STATIC.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Deploying to static hosting (S3 + CloudFront)
2
2
 
3
3
  The `static` target is the bottom rung of the [capability
4
- ladder](SPIKE-CORE-LAYERING.md#the-full-ladder-one-contract-one-vocabulary-per-level):
4
+ ladder](FRAMEWORK.md#the-ladder):
5
5
  the same `urlcode.yaml` a self-hosted server or a serverless adapter runs, with
6
6
  no server process at all. `urlcode build --target static` compiles a project
7
7
  ahead of time into plain files and two small JSON manifests meant for an S3
@@ -15,7 +15,7 @@ urlcode build --target static --project . --out dist --origin https://links.exam
15
15
 
16
16
  This target serves **redirects**, **declared responses** (`respond:`) and
17
17
  static files (`page`, `static`, `download`) — no path parameters, no query
18
- passthrough or mapping, GET/HEAD only, and no `enabled: false` or `expires`
18
+ passthrough or mapping, both GET and HEAD required, `respond.status: 200` only, and no `enabled: false` or `expires`
19
19
  (there is no server to answer a disabled or expired route with 404/410, so the
20
20
  build refuses one instead of silently serving it forever). Everything else is
21
21
  refused **at build time**, with the route pattern and the reason named:
@@ -33,7 +33,8 @@ refused **at build time**, with the route pattern and the reason named:
33
33
  | a redirect with a `{parameter}` in its path | S3's per-object redirect is keyed to one exact object, not a pattern |
34
34
  | a redirect with `query.pass`/`query.map` | S3's per-object redirect cannot compute a target per request |
35
35
  | a redirect with a `status` other than 301 | S3's per-object website redirect always answers 301 |
36
- | a route declaring methods other than GET/HEAD | static hosting only ever answers GET/HEAD |
36
+ | a route not admitting both GET and HEAD, or admitting other methods | the output cannot enforce a different method set |
37
+ | `respond.status` other than 200 | an uploaded response object cannot preserve a custom HTTP status |
37
38
  | a route with `enabled: false` or `expires` | no server to answer a disabled/expired route; remove the route instead |
38
39
 
39
40
  Run `urlcode capabilities --target static` for the full catalog.
@@ -92,3 +93,13 @@ object layout, the redirect manifest, and every refusal above. **It has not
92
93
  been deployed to S3 or fronted by CloudFront.** Bucket policy, CloudFront
93
94
  caching behavior, TLS/domain setup and the exact `aws s3` invocations above are
94
95
  unverified until a real deployment exercises them.
96
+
97
+
98
+ ## Building pages with middleware
99
+
100
+ Use [prerendering](PRERENDER.md) to execute functions and native middleware at
101
+ build time, then export the generated native page routes with this target.
102
+ Trusted Node execution is the build default; `sandbox: true` retains its
103
+ restricted imports and resource limits. Neither mode adds a request-time server
104
+ to the static output. Authentication, request-dependent headers and other
105
+ per-request middleware cannot be baked into a public file safely.
package/docs/TOOLING.md CHANGED
@@ -137,8 +137,8 @@ assistant file-write, guest-execution, deployment or network authority.
137
137
 
138
138
  `urlcode explain [/route] [--project DIR] [--target T] [--host-file F] [--json]`
139
139
  prints what `explainRoute` returns: one route in detail, or without a path a
140
- one-line-per-route table (methods, handler, state, middleware count, policies,
141
- cache outcome and target support). `--target` narrows the support columns to
140
+ one-line-per-route table (methods, handler, state, execution mode, middleware
141
+ count, policies, cache outcome and target support). `--target` narrows the support columns to
142
142
  one deployment target; `--host-file` supplies the operator registry so
143
143
  extension requirements show their provider. An unknown route exits 1 and names
144
144
  the nearest patterns. Everything comes from the compiled configuration: no
@@ -148,7 +148,8 @@ request is evaluated, no function runs and no binding is read.
148
148
  `schemaVersion`, the `urlcode` version, the entry file and its includes, the
149
149
  `revision` (the same digest `inspectExtensionRevision` returns, so an operator
150
150
  pin can be checked against it), the config `configVersion`, every route (path,
151
- methods, handler, state, middleware, inputs, policy names, extension
151
+ methods, handler, state, execution mode (`sandbox`, with `sandboxReason` when
152
+ the route declares one), middleware, inputs, policy names, extension
152
153
  requirements, cache outcome, binding names, egress origins, capabilities and
153
154
  per-target support), the union of capabilities used, extension declarations
154
155
  (version, configuration keys, mounts and protected routes), recipe provenance
@@ -48,7 +48,12 @@ refused, and the source graph is limited to 128 modules, 1 MiB per source and
48
48
  source parser and byte limits before publication. For a trusted route, none of
49
49
  that applies: bare/npm specifiers, dynamic `import()`, `import.meta` and
50
50
  import attributes pass through unchanged (resolved by Node at serve time, not
51
- by this build), and there is no module-count or size ceiling. In both modes,
51
+ by this build), and there is no module-count or aggregate-size ceiling only
52
+ a generous 16 MiB per-source read cap that bounds authoring-time memory. A
53
+ module may be shared by trusted and sandboxed routes: it is emitted once, and
54
+ every module reachable from a `sandbox: true` route is still validated under
55
+ the sandbox rules and budgets above, which does not disqualify the trusted
56
+ route that also imports it. In both modes,
52
57
  no import extension inference occurs — relative imports of project modules
53
58
  still need an explicit `.ts`/`.js`/`.mjs` extension to be rewritten and
54
59
  followed. This does not execute the modules or replace normal route, policy,
@@ -1,205 +1,46 @@
1
- # Core version alignment across repositories
2
-
3
- Which core version each downstream package supports, how it says so, and the
4
- order in which a core behavior change reaches downstream repositories. This
5
- page is a mechanism and an invariant, not a schedule: it states no release
6
- cadence, no LTS line and no support window. Those are not decided.
7
-
8
- It exists because "the current core" had come to mean five different things at
9
- once, and one of them did not resolve. The `0.4.0-alpha.2` release closes that;
10
- the table below is where every repository lands, and it is the register to
11
- change whenever a version changes anywhere.
12
-
13
- | Repository | How it names core | Value (read from its own `package.json`/`peers.json`) |
14
- |---|---|---|
15
- | `urlcode` | source version | `0.4.0-alpha.2` |
16
- | `urlcode-auth`, `urlcode-admin` | peer range plus a reviewed SHA | `>=0.4.0-alpha.1 <0.5.0`; `peers.json` `urlcode` = `d5e86017e93b96ec24bfdbf840692b95fc323151` in both |
17
- | `urlcode-dynamic-link` | peer range | `>=0.4.0-alpha.1 <0.5.0` |
18
- | `urlcode-middleware` | peer range | `>=0.4.0-alpha.2 <0.5.0` |
19
- | `urlcode-short`, `urlcode-template`, `urlcode-docs` | exact dependency pin | `0.4.0-alpha.2` |
20
-
21
- Only `urlcode-middleware` requires `0.4.0-alpha.2` specifically: it uses the
22
- `middleware()` extension hook, `ExtensionActivation.root` and
23
- `RuntimeExtension.cacheSensitive`, none of which exist in `0.4.0-alpha.1`. The
24
- other extension packages work against either alpha and keep the wider floor,
25
- which is what the supported-floor definition below requires of them.
26
-
27
- The npm dist-tags for `@jimhoyd/urlcode` are `latest` = `0.3.0` and `alpha` =
28
- `0.4.0-alpha.2`. `latest` deliberately stays on the `0.3.0` Apache-2.0
29
- self-hosted baseline: the `0.4.0` line is a prerelease and must not become the
30
- default install. Every release workflow derives its dist-tag from the version
31
- rather than defaulting, so a prerelease can only publish under `alpha`.
32
-
33
- The sibling packages are `@jimhoyd/urlcode-ui` `0.1.0-alpha.5`,
34
- `@jimhoyd/urlcode-auth` and `@jimhoyd/urlcode-admin` `0.1.0-alpha.3`, and
35
- `@jimhoyd/urlcode-dynamic-link`, `@jimhoyd/urlcode-middleware` and
36
- `@jimhoyd/urlcode-short` `0.1.0-alpha.2`. For the extension line, `latest` and
37
- `alpha` point at the same version — see the second invariant below.
38
-
39
- Every one of those is a new version in this release. Each package's previous
40
- release sat at the same version number as a source tree that had moved well
41
- past it — 43 merged commits in `urlcode-auth`, 40 in `urlcode-ui`, 28 in
42
- `urlcode-admin` so the published version number identified nothing. A
43
- version number that does not change when the source does is the same class of
44
- defect as a peer range that cannot resolve, and the rule is the same: change
45
- the version in the pull request that changes the source.
46
-
47
- ## The supported floor
48
-
49
- The supported core floor for an extension package is the **lowest core version
50
- published to npm that contains every core API the extension calls**. It is a
51
- property of the code, not of a calendar: raise it when the extension starts
52
- using a core API that older published cores do not have, and not otherwise.
53
-
54
- Everything below follows from that one definition.
55
-
56
- ## How a package expresses its core requirement
57
-
58
- Three forms are in use. They are not interchangeable.
59
-
60
- **Peer range (`peerDependencies`), for an extension package.** An extension is
61
- installed alongside core by the operator, so it must not carry its own copy;
62
- `peerDependencies` is the correct field. Write it as a floor plus the next
63
- breaking bound — `">=<floor> <0.5.0"` — where the floor is the supported floor
64
- above. `urlcode-auth` and `urlcode-admin` use this form.
65
-
66
- **Exact pin (`dependencies`), for an application or a starter.** A project that
67
- is deployed or cloned rather than composed — `urlcode-short`, `urlcode-template`,
68
- `urlcode-docs` — depends on one core version and pins it exactly. This is the
69
- right form when the repository's tests, generated files and documentation were
70
- all produced against one runtime and are only claimed to hold for that runtime.
71
-
72
- **Reviewed SHA (`peers.json`), for source CI on top of a range.** A reviewed
73
- commit is not a substitute for the peer range; it is an addition to it, used
74
- where the repository must build and test against an exact reviewed core
75
- checkout rather than whatever the registry resolves. `urlcode-auth` and
76
- `urlcode-admin` each keep one `peers.json` naming the reviewed core commit;
77
- each file states in its own `$comment` that published releases do not use those
78
- SHAs and resolve peers from the registry by the `package.json` range instead.
79
- Use a reviewed SHA when source CI needs reproducibility; do not use it to
80
- express what an installing operator will get.
81
-
82
- ## The invariant: a published peer range must be satisfiable
83
-
84
- **A package published to npm must never declare a peer range that no published
85
- core version satisfies.** At publish time, at least one version on the registry
86
- must fall inside the range. A range that points at an unpublished core is not a
87
- forward-looking declaration — it is an install failure for everyone who takes
88
- the package from the registry.
89
-
90
- The worked example came from this project. `@jimhoyd/urlcode-middleware`
91
- `0.1.0-alpha.1` was published declaring `peerDependencies`
92
- `{"@jimhoyd/urlcode": ">=0.4.0-alpha.2"}` at a time when the registry held only
93
- `0.3.0` and `0.4.0-alpha.1`, so nothing satisfied it. The range was correct —
94
- the package genuinely needs APIs that first appear in `0.4.0-alpha.2` — so the
95
- fix was never to widen the range. The publication order was wrong: the package
96
- was published before the core it requires, and it could be installed only from
97
- source against a vendored core tarball carried for exactly that reason.
98
-
99
- Publishing core `0.4.0-alpha.2` resolves it without any change to the already
100
- published package: the range becomes satisfiable the moment core is on the
101
- registry. The vendored tarball and the source-only install path go away with
102
- it.
103
-
104
- That is what the invariant prevents, and it is the only ordering rule that
105
- cannot be relaxed.
106
-
107
- ## The second invariant: `latest` must not fall below a sibling's floor
108
-
109
- **Where a package line publishes under a prerelease dist-tag, `latest` must
110
- still resolve to a version that satisfies every peer floor its siblings
111
- declare.** `latest` is what a plain `npm install <package>` resolves, so a
112
- `latest` left behind hands the installing operator a build that another
113
- package in the same install refuses.
114
-
115
- The worked example, again live in this project: `@jimhoyd/urlcode-auth`
116
- published `alpha` = `0.1.0-alpha.2` while leaving `latest` = `0.1.0-alpha.1`.
117
- `@jimhoyd/urlcode-admin` declares a peer floor of `>=0.1.0-alpha.2` on auth, so
118
- `npm install @jimhoyd/urlcode-auth` resolved a build below the floor admin
119
- requires. Nothing in the source is wrong; the dist-tag is.
120
-
121
- The fix is a registry operation rather than a source change:
122
-
123
- ```sh
124
- npm dist-tag add @jimhoyd/urlcode-auth@0.1.0-alpha.2 latest
125
- ```
126
-
127
- Core is the deliberate exception. Its `latest` stays on `0.3.0` because no
128
- sibling declares a floor above it — the extension packages name core through
129
- `peerDependencies`, which resolve by range and never by dist-tag.
130
-
131
- ## A deliberate older pin is a position, not drift
132
-
133
- `urlcode-template`, `urlcode-docs` and `urlcode-short` now all pin
134
- `0.4.0-alpha.2`, so no downstream repository is currently behind. An older pin
135
- remains a legitimate position, and the rule for it does not change: it is
136
- recorded where a reader will meet it. The repository's README says which core
137
- version it pins, and every statement about runtime behavior in that repository
138
- is read against that version. Where a statement is only true for the pinned version, it
139
- says so and names the version, rather than being silently corrected to match
140
- core's unreleased `main`.
141
-
142
- This matters most for the trust model. Core `0.4.0-alpha.2` runs `function` and
143
- `middleware` routes trusted and unsandboxed by default, with `sandbox: true` as
144
- a per-route opt-in ([decision record](SPIKE-DEFAULT-TRUST-MODEL.md)).
145
- `0.4.0-alpha.1` and earlier sandbox all such code unconditionally and have no
146
- `sandbox` field in the schema. This is why moving a pin to `0.4.0-alpha.2` is
147
- a behavior change even when no YAML changes: every `function`/`middleware`
148
- route that does not declare `sandbox` becomes trusted on upgrade. Review those
149
- routes before raising a pin, and add `sandbox: true` to the ones that handle
150
- input or code you would not trust with full Node/filesystem/network access.
151
- A repository still pinned to `0.4.0-alpha.1` or `0.3.0` that documents
152
- sandbox-by-default is **describing its pin correctly**. Its
153
- generated files — field references, schemas, scaffolding output, vendored agent
154
- skills — are likewise correct for that pin. Divergence from core's current
155
- `main` is not by itself staleness, and it is not a defect to be "fixed" by
156
- importing behavior the pinned runtime does not have. The same point is made
157
- about the advisory `npm run check:downstream-skills` report in
158
- [release readiness](RELEASE-READINESS.md).
159
-
160
- What is a defect: a repository that does not say which core version it pins, or
161
- that states pin-dependent behavior as an unconditional property of URLCode.
162
-
163
- ## Order of operations when core changes behavior
164
-
165
- A core behavior change reaches downstream repositories in this order. Each step
166
- depends on the one before it; skipping ahead is what produced the
167
- `urlcode-middleware` breakage above.
168
-
169
- 1. **Publish core.** The version carrying the change goes to npm first. Until it
170
- is on the registry, no downstream peer range may reference it (the invariant
171
- above).
172
- 2. **Raise downstream floors.** Extension packages move their peer range floor
173
- to the published version; applications and starters move their exact pin.
174
- A repository that chooses to stay on the older core does nothing here — that
175
- is a valid outcome, and step 3 does not apply to it.
176
- 3. **Regenerate downstream generated files.** Field references, schemas,
177
- scaffolding output and vendored agent skills are regenerated *after* the
178
- floor moves, against the version now pinned — never before, or they describe
179
- a runtime the repository does not depend on.
180
-
181
- Reviewed-SHA repositories take the same three steps, and additionally update
182
- `peers.json` to the reviewed commit for the new core when source CI is expected
183
- to build against it. The SHA and the published floor answer different questions
184
- and are updated independently.
185
-
186
- ## Open: two publishing conventions
187
-
188
- The repositories publish under two different conventions, and the maintainer has
189
- not settled which one the project uses. Both are recorded here neutrally; this
190
- page does not pick one.
191
-
192
- - **Private until release.** `urlcode-dynamic-link` and `urlcode-middleware`
193
- keep `"private": true` in their `package.json` on `main` and drop it in the
194
- release commit. Publication is an explicit, visible act in the release diff,
195
- and an accidental `npm publish` from `main` fails closed. Both packages are
196
- nonetheless published on npm, so the convention has been exercised.
197
- - **Publishable on main.** `urlcode-auth`, `urlcode-admin` and `urlcode-ui`
198
- carry no `private` field and declare `publishConfig.access = "public"`
199
- instead. The committed manifest is
200
- the manifest that publishes, so what is on the registry can be diffed against
201
- `main` without accounting for a release-only edit.
202
-
203
- The split is currently by repository, not by package kind, and nothing records
204
- why. Settling it is a maintainer decision; no `package.json` is changed on the
205
- strength of this page.
1
+ # Package and channel alignment
2
+
3
+ The package manifests and root lockfile are the version authority. Read live
4
+ registry and Git tag state with `npm run release:status`; do not maintain a
5
+ second table of changing version numbers in documentation.
6
+
7
+ | Package | Manifest | Version owner | Release tag |
8
+ | --- | --- | --- | --- |
9
+ | `@jimhoyd/urlcode` | `package.json` | Explicit core release PR | `v<version>` |
10
+ | `@jimhoyd/urlcode-ui` | `packages/ui/package.json` | Changesets | `@jimhoyd/urlcode-ui@<version>` |
11
+ | `@jimhoyd/urlcode-auth` | `packages/auth/package.json` | Changesets | `@jimhoyd/urlcode-auth@<version>` |
12
+ | `@jimhoyd/urlcode-admin` | `packages/admin/package.json` | Changesets | `@jimhoyd/urlcode-admin@<version>` |
13
+
14
+ Development uses workspace source. Auth and admin's `file:../..` development
15
+ links resolve core to this checkout, enforced by `check-workspace-links.ts`.
16
+ Core never imports extension packages. Release verification instead installs the
17
+ published lower bound of each declared peer range and checks resolution. A peer
18
+ floor rises when code requires a newly introduced API, not just because a sibling
19
+ published another version. Preserve the declared upper bound during Changesets
20
+ versioning; `.changeset/config.json` limits unnecessary peer rewrites.
21
+
22
+ Publishable workspace changes carry Changesets; the release PR applies them and
23
+ updates versions, changelogs and the lockfile together. Core stays an explicit
24
+ entry in that PR until a separately reviewed workspace migration. Its CLI banner
25
+ must match its manifest. `npm run release:check` rejects stale lockfile versions.
26
+ Unreleased source changes do not require moving a published tag or pretending a
27
+ new package has already shipped.
28
+
29
+ Alpha releases publish under `alpha`; they never automatically move npm
30
+ `latest`. Core's historical `latest` remains the stable 0.3.0 baseline until an
31
+ explicit stable release decision. Extensions have historical alpha versions on
32
+ `latest`; subsequent alpha publication does not keep that channel in lockstep.
33
+ Different channel values alone are not drift. Test the install combination you
34
+ recommend against peer ranges; a bare install may select an older channel.
35
+ `release:status` reports each declared peer floor and whether its current
36
+ `latest` and `alpha` satisfy the range.
37
+
38
+ The standalone `urlcode-template` is an external exact-version consumer: after a
39
+ runtime release, update its dependency and starter through its own reviewed PR.
40
+ It is not automatically released by the monorepo coordinator. The retired
41
+ `urlcode-docs`, `urlcode-middleware`, `urlcode-dynamic-link` and `urlcode-short`
42
+ repositories are not release targets.
43
+
44
+ See [the development pipeline](DEVELOPMENT-PIPELINE.md) for preparation,
45
+ publication order, immutable retries and credential scope, and
46
+ [release security](RELEASE-SECURITY.md) for provenance and limitations.
@@ -0,0 +1,68 @@
1
+ # Historical record
2
+
3
+ Archived 2026-09-19. This records an earlier implementation or proposal, not
4
+ current instructions. See the [current roadmap](../../../ROADMAP.md),
5
+ [current contract](../../SPECIFICATION.md) and [open decisions](../../OPEN-DECISIONS.md).
6
+ Remaining acceptance work is not declared complete by archiving this record.
7
+
8
+ <!-- trust-model-prose: historical-file -->
9
+ <!-- guidance-claims: ignore-file -->
10
+
11
+ # Extension implementation sequence
12
+
13
+ PR #54 records the auth, admin, UI and extension direction. Implementation is
14
+ incremental; the spikes are proposals, not the implemented project schema.
15
+ The implemented specification remains authoritative. This sequence incorporates
16
+ the architectural review rather than treating contradictory spike examples as
17
+ requirements.
18
+
19
+ ## Invariants
20
+
21
+ - Operators explicitly install trusted host extensions; project YAML never loads
22
+ host modules or chooses packages. Application code remains isolated in WASM.
23
+ - Authentication credentials stay in host processing. Guest Request headers and
24
+ header-derived parameters must not expose operator-declared credential headers,
25
+ including on public routes receiving a session cookie.
26
+ - Session and one-time-token changes have explicit concurrency semantics. Define
27
+ atomic operations before selecting a database abstraction; multi-record
28
+ operations cannot rely on a same-collection transaction promise.
29
+ - Passwords and bearer credentials use appropriate hashes. TOTP requires protected
30
+ recoverable key material and an operator key rotation/backup contract.
31
+ - Authentication forms and submission destinations belong to trusted extension
32
+ code. Project styling does not imply permission to replace security controls.
33
+ - Administrative authorization is enforced by the API, including subject-level
34
+ restrictions, self-promotion and last-administrator protections.
35
+ - Unsupported target capabilities fail explicitly. Live provider deployment
36
+ testing remains deferred and non-blocking; local tests do not prove deployment.
37
+
38
+ ## Delivery order
39
+
40
+ 1. **Credential boundary:** add an explicit host-plugin declaration for headers
41
+ withheld from guest requests and parameter resolution. Preserve originals for
42
+ host authentication, cache decisions and policy processing. Test functions,
43
+ middleware and routes without an authentication requirement.
44
+ 2. **Extension registration:** explicit operator registry, versioned configuration
45
+ validation, route ownership/collision rules and capability reporting. No
46
+ automatic project host-file execution. Define policy ordering before adding
47
+ an auth policy.
48
+ 3. **Session slice:** one supported login method, trusted login/logout forms,
49
+ protected page and JSON routes, session lookup/revocation, CSRF and no-store
50
+ behavior. Evaluate a maintained authentication implementation behind an
51
+ adapter before writing protocol engines. Define durable atomic store methods
52
+ and test concurrent redemption, expiry and interrupted operations.
53
+ 4. **Administrative API and CLI:** user lookup and session revocation first;
54
+ audit records and narrowly scoped permissions. Add role/recovery operations
55
+ only with object-level rules and concurrency tests.
56
+ 5. **UI and console:** extract components from real auth/admin flows; choose a
57
+ constrained renderer and asset ownership model; verify keyboard and assistive
58
+ technology behavior as well as automated accessibility checks.
59
+ 6. **Additional methods and targets:** passkeys, OIDC, MFA, recovery and imports
60
+ each receive state-machine, abuse-budget and portability tests. Preserve
61
+ verification of existing credential formats across target migrations.
62
+
63
+ ## Status
64
+
65
+ PR #54 is merged. The first implementation adds the operator-only
66
+ `Plugin.credentialHeaders` boundary, documented in [Plugins](../../PLUGINS.md).
67
+ The remaining items are pending; none of this document declares end-user auth,
68
+ admin routes, extension YAML, or a generic project store available today.