@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.1

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 (147) 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 +59 -64
  6. package/ROADMAP.md +66 -376
  7. package/dist/BUILD-MANIFEST.json +26 -25
  8. package/dist/agents-guide.js +6 -6
  9. package/dist/authoring.js +15 -1
  10. package/dist/build-static.js +2 -0
  11. package/dist/capability-query.js +0 -1
  12. package/dist/catalog.js +0 -1
  13. package/dist/cli.js +25 -9
  14. package/dist/config.js +1 -1
  15. package/dist/explain-cli.js +4 -2
  16. package/dist/explain.js +8 -2
  17. package/dist/extensions.js +1 -1
  18. package/dist/http-response.js +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/init-with.js +36 -11
  21. package/dist/manifest.js +8 -2
  22. package/dist/mcp-authoring.js +4 -4
  23. package/dist/mcp.js +1 -1
  24. package/dist/policies/cache.js +2 -2
  25. package/dist/policies.js +3 -1
  26. package/dist/prerender.js +4 -0
  27. package/dist/project-dependencies.js +305 -0
  28. package/dist/readiness.js +3 -0
  29. package/dist/route-diff.js +12 -5
  30. package/dist/runtime.js +1 -1
  31. package/dist/trusted-functions.js +4 -5
  32. package/dist/types/authoring.d.ts +9 -1
  33. package/dist/types/capability-query.d.ts +0 -1
  34. package/dist/types/catalog.d.ts +0 -4
  35. package/dist/types/config.d.ts +1 -9
  36. package/dist/types/explain.d.ts +6 -1
  37. package/dist/types/extensions.d.ts +1 -1
  38. package/dist/types/http-response.d.ts +0 -1
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/init-with.d.ts +7 -13
  41. package/dist/types/manifest.d.ts +5 -2
  42. package/dist/types/project-dependencies.d.ts +78 -0
  43. package/dist/types/readiness.d.ts +2 -0
  44. package/dist/types/trusted-functions.d.ts +1 -4
  45. package/dist/types/types.d.ts +8 -1
  46. package/dist/types.js +8 -1
  47. package/dist/typescript-authoring.js +18 -7
  48. package/docs/AI-AUTHORING.md +15 -7
  49. package/docs/ASSETS.md +2 -1
  50. package/docs/AUTH-BACKUP.md +32 -0
  51. package/docs/AWS.md +9 -0
  52. package/docs/BEST-PRACTICES.md +17 -9
  53. package/docs/CAPABILITIES.md +1 -1
  54. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  55. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  56. package/docs/CI.md +8 -3
  57. package/docs/CODEBASE-AUDIT-2026-09-20.md +284 -0
  58. package/docs/COMPOSING-A-SITE.md +278 -0
  59. package/docs/DEVELOPMENT-PIPELINE.md +270 -0
  60. package/docs/EXTENSIONS.md +51 -11
  61. package/docs/FRAMEWORK.md +67 -48
  62. package/docs/FUNCTION-SECURITY.md +44 -0
  63. package/docs/INSTALL.md +13 -8
  64. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  65. package/docs/MIDDLEWARE.md +10 -4
  66. package/docs/OPEN-DECISIONS.md +224 -212
  67. package/docs/OPERATIONAL-PROOF.md +3 -3
  68. package/docs/OPERATIONS.md +3 -3
  69. package/docs/POLICIES.md +13 -5
  70. package/docs/PRERENDER.md +23 -11
  71. package/docs/PROJECT-DIRECTION.md +3 -3
  72. package/docs/READINESS.md +10 -5
  73. package/docs/README.md +20 -44
  74. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  75. package/docs/RELEASE-0.4.1.md +73 -0
  76. package/docs/RELEASE-READINESS.md +6 -6
  77. package/docs/RELEASE-SECURITY.md +96 -181
  78. package/docs/RESILIENCE.md +3 -3
  79. package/docs/ROUTING.md +3 -4
  80. package/docs/SECURITY-AUDIT.md +2 -2
  81. package/docs/SPECIFICATION.md +12 -6
  82. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
  83. package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
  84. package/docs/SPIKE-CORE-LAYERING.md +49 -18
  85. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
  86. package/docs/STARTERS.md +17 -5
  87. package/docs/STATIC.md +14 -3
  88. package/docs/TOOLING.md +10 -7
  89. package/docs/TYPESCRIPT-AUTHORING.md +6 -1
  90. package/docs/VERCEL.md +10 -2
  91. package/docs/VERSION-ALIGNMENT.md +76 -201
  92. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  93. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
  94. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
  95. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
  96. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  97. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  98. package/docs/archive/2026-09-19/ROADMAP.md +387 -0
  99. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
  100. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
  101. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
  102. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
  103. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
  104. package/docs/archive/README.md +28 -0
  105. package/docs/policies/agents.md +1 -1
  106. package/docs/policies/compression.md +3 -2
  107. package/docs/policies/security.md +3 -2
  108. package/docs/yaml/functions.md +10 -2
  109. package/docs/yaml/middleware.md +5 -3
  110. package/examples/assets/example.yaml +1 -1
  111. package/examples/cookbook/middleware/envelope.mjs +4 -2
  112. package/examples/cookbook/route-index.json +1 -1
  113. package/examples/cookbook/routes/middleware.yaml +1 -1
  114. package/examples/prerender/README.md +14 -6
  115. package/examples/prerender/functions/page.mjs +4 -2
  116. package/examples/prerender/middleware/template.mjs +1 -1
  117. package/examples/prerender/prerender.mjs +1 -1
  118. package/examples/prerender/urlcode.yaml +8 -4
  119. package/llms-full.txt +503 -88
  120. package/llms.txt +6 -4
  121. package/package.json +27 -4
  122. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  123. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
  124. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
  125. package/recipes/authenticated-json-api/README.md +4 -3
  126. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  127. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  128. package/recipes/contact-form/functions/contact.mjs +2 -1
  129. package/recipes/contact-form/recipe.yaml +2 -2
  130. package/recipes/cors-api/README.md +2 -2
  131. package/recipes/cors-api/recipe.yaml +1 -1
  132. package/recipes/health-page/README.md +1 -1
  133. package/recipes/json-api/README.md +1 -1
  134. package/recipes/json-api/recipe.yaml +3 -3
  135. package/recipes/middleware/README.md +8 -4
  136. package/recipes/middleware/middleware/envelope.mjs +4 -2
  137. package/recipes/protected-download/README.md +1 -1
  138. package/recipes/protected-download/recipe.yaml +1 -1
  139. package/recipes/static-plus-api/README.md +2 -2
  140. package/recipes/static-plus-api/public/index.html +1 -1
  141. package/recipes/static-plus-api/recipe.yaml +1 -1
  142. package/recipes/static-plus-api/urlcode.yaml +1 -1
  143. package/recipes/typescript/recipe.yaml +4 -4
  144. package/skills/urlcode/SKILL.md +6 -6
  145. package/starters/default/AGENTS.md +6 -6
  146. package/docs/SPIKE-LAMBDA-COMPILE.md +0 -201
  147. package/docs/SPIKE-MONOREPO.md +0 -322
@@ -0,0 +1,102 @@
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
+ # Private management and durable mutation audit
12
+
13
+ > **Retired.** This page describes the management API and mutation audit of
14
+ > core's native link store. That store, its `urlcode links` CLI and this
15
+ > management listener were removed from core when `link` was extracted, and the
16
+ > `urlcode-dynamic-link` package that received them has since been retired and
17
+ > unpublished. Nothing in the current runtime exposes this surface. The page is
18
+ > kept because the security reasoning — loopback-only binding, operator-owned
19
+ > credential policy, transactional mutation audit — is referenced by
20
+ > [security audit](../../SECURITY-AUDIT.md) and applies to any component that
21
+ > reintroduces a management listener.
22
+
23
+ Management binds only `127.0.0.1` or `::1`. Use an authenticated private tunnel
24
+ (e.g. SSH/VPN with loopback forwarding); never publish its port through a public
25
+ proxy or container port mapping. This is operator management, not an end-user
26
+ account service. Browser Origin requests remain rejected.
27
+
28
+ ## Individual credentials
29
+
30
+ Use `urlcode links api --project ./my-links --store /operator/links.sqlite
31
+ --auth-file /operator/management.json --host 127.0.0.1 --port 3001` (one line).
32
+ The policy is operator-owned, outside the application, at most 64 KiB and mode
33
+ 600 on POSIX. Protect its parent directory and apply equivalent Windows ACLs.
34
+ It is not YAML and must never be placed in the application or Git.
35
+
36
+ Generate each token with `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`
37
+ on a trusted operator machine. Deliver it through your secret manager. Hash the
38
+ raw token using SHA-256 without a newline; the server expects `Authorization:
39
+ Bearer <token>`. Tokens must contain 43–256 base64url characters. Never use a
40
+ human password as a token. This synthetic policy illustrates the shape; replace
41
+ the hash with the real token hash and choose a short operational expiry:
42
+
43
+ ```json
44
+ {
45
+ "version": 1,
46
+ "credentials": [{
47
+ "id": "operator-alice",
48
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
49
+ "expires": "2026-10-01T00:00:00Z",
50
+ "collections": ["links"],
51
+ "actions": ["get", "list", "create", "update", "delete"]
52
+ }]
53
+ }
54
+ ```
55
+
56
+ At most 128 credentials; unique IDs and hashes; explicit collection and action
57
+ allowlists with no wildcards. Readers should receive only `get` and `list`.
58
+ An expired, removed or `"revoked": true` credential gets 401; a valid credential
59
+ outside its scope gets 403. The entire policy is validated on every authenticated
60
+ request. Malformed/unreadable policy fails closed with 503, without retaining an
61
+ old permissive copy. Write a replacement file with mode 600 and atomically rename
62
+ it over the policy. No restart is needed. Requests already authorized may finish;
63
+ revocation does not cancel an in-flight transaction. Credential administration is
64
+ an operator filesystem action, never exposed through this HTTP API.
65
+
66
+ Legacy `--token-file` remains available for local compatibility, with full access
67
+ to that listener's collection and actor `legacy-shared`. Prefer `--auth-file` for
68
+ attribution, expiry and revocation. They are mutually exclusive. These bearer
69
+ credentials do not establish verified human identity, MFA, SSO or session login.
70
+ Host administrators and custom embedding code remain trusted operators.
71
+
72
+ ## Audit durability and recovery
73
+
74
+ The built-in SQLite writer creates an additive `urlcode_link_audit` table when
75
+ opening a store. Every successful create/update/delete (including CLI and embedded
76
+ store calls) records revision, UTC timestamp, actor, request ID, collection,
77
+ action and SHA-256 of the short code in the **same transaction** as the mutation.
78
+ The default local CLI actor is `local-operator`. Scoped HTTP management requires
79
+ an adapter explicitly declaring atomic audit support; the built-in store provides it.
80
+
81
+ An audit insert failure rolls back both mutation and revision. Conditional-write
82
+ conflicts and rejected requests produce no successful-mutation row. HTTP request
83
+ logs still describe failed/aborted requests and remain best effort. A client timeout
84
+ may follow a committed transaction: reconcile the revision and request ID before
85
+ retrying. A lost HTTP reply is not evidence that the transaction was rolled back.
86
+
87
+ Inspect the journal with an operator-only SQLite client, for example:
88
+
89
+ ```sql
90
+ SELECT revision, timestamp, actor, request_id, collection, action, code_sha256
91
+ FROM urlcode_link_audit WHERE revision > 0 ORDER BY revision LIMIT 100;
92
+ ```
93
+
94
+ No token, URL, request body or raw short code is stored in the journal. Code hashes
95
+ are correlatable and guessable for short codes; restrict access accordingly.
96
+ This is a durable local transaction journal, **not tamper-evident external storage**.
97
+ An administrator with database access can change it. Export ordered revisions to
98
+ your protected collector/backup system and monitor lag, gaps and disk/WAL growth.
99
+ There is no automatic pruning: define retention and verify archival before any
100
+ operator purge. Keep audit records in backups. User_version remains 1 because
101
+ this table is additive; older readers work, but older writers do not emit audit
102
+ rows. Never roll a management writer back to an unaudited revision.
@@ -1,3 +1,13 @@
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
+
1
11
  # Next-phase implementation status
2
12
 
3
13
  Repository review and source implementation, 2026-09-17. Phase A established
@@ -10,7 +20,7 @@ Phase B–D features below. This describes the source additions after `0.3.0` th
10
20
  > The default changed in `0.4.0-alpha.2`: those routes now run trusted and
11
21
  > unsandboxed in the host process unless the route declares `sandbox: true`,
12
22
  > which gives it exactly the QuickJS/WASM execution described below. See
13
- > [SPIKE-DEFAULT-TRUST-MODEL.md](SPIKE-DEFAULT-TRUST-MODEL.md).
23
+ > [SPIKE-DEFAULT-TRUST-MODEL.md](../../SPIKE-DEFAULT-TRUST-MODEL.md).
14
24
 
15
25
  ## Implementation and evidence
16
26
 
@@ -28,10 +38,10 @@ Phase B–D features below. This describes the source additions after `0.3.0` th
28
38
  | D: TypeScript guests | Fixed trusted build-time compiler, bounded relative graph, rewritten JavaScript imports, referenced-asset snapshot and safe new output | Transpilation is not type checking; no tsconfig/plugins/package execution or dotenv copying; runtime remains QuickJS JavaScript only |
29
39
  | D: consolidated SDK/MCP | Inspection, semantic validation, path explanation, compatibility, conversion previews and recipe discovery; operator-rooted stdio MCP | Read-only tooling; no arbitrary path, credential, guest execution or write authority; not a remote authenticated service |
30
40
 
31
- See [interchange](INTERCHANGE.md), [provider evidence](PROVIDER-VERIFICATION.md),
32
- [egress](EGRESS.md), [conditions](CONDITIONS.md), [recipes](RECIPES.md),
33
- [bulk measurements](BULK.md), [TypeScript authoring](TYPESCRIPT-AUTHORING.md), and
34
- [tooling/MCP](TOOLING.md) for the executable interfaces and exact restrictions.
41
+ See [interchange](../../INTERCHANGE.md), [provider evidence](../../PROVIDER-VERIFICATION.md),
42
+ [egress](../../EGRESS.md), [conditions](../../CONDITIONS.md), [recipes](../../RECIPES.md),
43
+ [bulk measurements](../../BULK.md), [TypeScript authoring](../../TYPESCRIPT-AUTHORING.md), and
44
+ [tooling/MCP](../../TOOLING.md) for the executable interfaces and exact restrictions.
35
45
 
36
46
  ## Preserved architecture
37
47
 
@@ -1,10 +1,20 @@
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
+
1
11
  # Next steps: closing the gaps
2
12
 
3
13
  Status: plan written 2026-09-18 from the cleanup, the
4
14
  [usability review](USABILITY-REVIEW.md) and the AI-first framework brief.
5
15
  Each item says what it fixes, where the work is, how it is proven, and its
6
16
  size (S: a day, M: a week, L: more). Phases can overlap; order inside a phase
7
- is the recommended sequence. The [roadmap](../ROADMAP.md) owns what ships;
17
+ is the recommended sequence. The [roadmap](ROADMAP.md) owns what ships;
8
18
  this page owns how the gaps close.
9
19
 
10
20
  ## The rule everything below serves
@@ -16,7 +26,7 @@ policies, admin patterns and deployment glue on every project, and the person
16
26
  ends up owning it. URLCode's job is a small, deterministic, portable vocabulary
17
27
  in readable YAML, so generated code goes to the part that is the application.
18
28
  The agent describes what; the runtime owns how. The three tests that decide
19
- what gets built are in [project direction](PROJECT-DIRECTION.md#why-your-ai-should-build-your-application-not-your-framework):
29
+ what gets built are in [project direction](../../PROJECT-DIRECTION.md#why-your-ai-should-build-your-application-not-your-framework):
20
30
  the boundary test (do agents generate this across unrelated projects?), the
21
31
  feature test (does it reduce what the agent must know, generate, debug or
22
32
  maintain?) and the evidence test (measured repetition, not a feature list).
@@ -497,7 +507,9 @@ this repository and were not run here.
497
507
 
498
508
  Fixes: protecting a route today is `policies: { extensions: { auth: {} } }`,
499
509
  which is the mechanism, not the intent. The form an agent should write is
500
- `auth: { required: true, roles: [admin] }`.
510
+ `auth: { required: true, role: admin }`. (Delivered in `0.4.0-alpha.1` with
511
+ `role` singular; `routeAuth` sets `additionalProperties: false`, so the plural
512
+ `roles` this plan originally proposed is rejected by the validator.)
501
513
 
502
514
  - Work: a route-level `auth` key that expands to the `policies.extensions.auth`
503
515
  requirement the auth extension validates; `roles` maps to the extension's
@@ -0,0 +1,277 @@
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
+ # Open decisions
12
+
13
+ Status: written 2026-09-19 from a review of the repositories as they then
14
+ stood (nine; eight after `urlcode-docs` was deleted — see item 2). Every item
15
+ here is a maintainer decision that documentation cannot make. Each states what
16
+ is actually true today, what the options are, what it costs to leave open, and
17
+ a recommendation. Nothing here is committed scope; the
18
+ [roadmap](../../../ROADMAP.md) owns sequence and the
19
+ [readiness register](../../RELEASE-READINESS.md) owns what is proven.
20
+
21
+ This page exists because several documented positions have drifted apart from
22
+ each other and from the source. The drift is not the decisions themselves — it
23
+ is that they were never recorded in one place where the next contributor, human
24
+ or agent, would look.
25
+
26
+ ## 1. Publish `0.4.0-alpha.2` — decided
27
+
28
+ **Decided 2026-09-19: publish `alpha.2`.** The recommendation below was taken
29
+ rather than walking back a correct declaration. The alignment is prepared
30
+ across all nine repositories and the order is recorded in
31
+ [the version register](../../VERSION-ALIGNMENT.md); what remains is merging and tagging.
32
+
33
+ The second hazard was taken with it: `@jimhoyd/urlcode-auth` moves `latest`
34
+ up to `0.1.0-alpha.2` so that a plain `npm install` no longer resolves a build
35
+ older than the floor `@jimhoyd/urlcode-admin` declares. That is a registry
36
+ operation (`npm dist-tag add`), not a source change, and it is listed in the
37
+ release steps.
38
+
39
+ The original entry, for the record:
40
+
41
+ > **Today:** the repository source is `0.4.0-alpha.2`. The npm dist-tags are
42
+ > `latest = 0.3.0` and `alpha = 0.4.0-alpha.1`. `alpha.2` carries the
43
+ > trusted-by-default execution change ([the decision](../../SPIKE-DEFAULT-TRUST-MODEL.md)).
44
+ >
45
+ > **Why it blocks other things:** `@jimhoyd/urlcode-middleware@0.1.0-alpha.1` is
46
+ > published with `peerDependencies: { "@jimhoyd/urlcode": ">=0.4.0-alpha.2" }`.
47
+ > No published core version satisfies that range, so a registry install of that
48
+ > package cannot resolve its peer. The range is correct; the publication order
49
+ > was not.
50
+ >
51
+ > **Recommendation:** publish `alpha.2`. It is the only option that does not
52
+ > require walking back a correct declaration.
53
+
54
+ ## 2. Consolidate the repositories, or commit to the split
55
+
56
+ **Today (revised 2026-09-19):** eight repositories — `urlcode`,
57
+ `urlcode-auth`, `urlcode-admin`, `urlcode-ui`, `urlcode-middleware`,
58
+ `urlcode-template`, `urlcode-cloud` (private) and `homebrew-urlcode`. Five are
59
+ in scope for consolidation. [The monorepo spike](SPIKE-MONOREPO.md) is
60
+ complete, its layout is marked decided (option A), Changesets is decided, its
61
+ migration mechanics and sequencing are written, and it has three open questions
62
+ for the maintainer. It has been neither accepted nor rejected.
63
+
64
+ **What changed since this item was written:**
65
+
66
+ - The count fell rather than rose. `urlcode-dynamic-link`, `urlcode-short` and
67
+ `urlcode-docs` were deleted; the spike's in-scope set is **five, not six**.
68
+ The migration is smaller today than when this item claimed it only grows.
69
+ - **The spike's hard precondition is currently satisfied:** zero open pull
70
+ requests across all five in-scope repositories, and zero open issues outside
71
+ core. The issue-recreation step is a no-op. This is not a stable state.
72
+ - **The drift argument stopped being hypothetical.** `urlcode-auth`,
73
+ `urlcode-admin` and `urlcode-ui` all pin core at `d5e86017` and went 21
74
+ commits stale within a day of that pin being corrected by hand.
75
+ - **A new argument exists.** `check-trust-model-prose.ts` and
76
+ `check-guidance-claims.ts` now fail CI on stale or schema-contradicting
77
+ prose, but only within this checkout. The original observed failure —
78
+ `urlcode-auth/SECURITY.md` describing guest code as sandboxed — remains
79
+ unreachable by any check while `auth` is a separate repository. See
80
+ "What consolidation would newly enforce" in the spike.
81
+
82
+ **Cost of leaving it open:** the coordination work the spike describes —
83
+ pinning reviewed peer revisions by hand, chasing prose across repositories when
84
+ a contract changes — is paid again on every contract change, and the four
85
+ downstream repositories stay outside the enforcing checks that now protect this
86
+ one.
87
+
88
+ **Recommendation, sharpened:** decide before the next structural change, in
89
+ either direction; a recorded "no, and here is what we do instead about
90
+ cross-repo drift" closes this as well as a yes does. If the answer is yes, note
91
+ that the precondition is met now and will not stay met — the survey above is
92
+ the cheapest it will ever be. If the answer is no, the thing that needs
93
+ designing is how the trust-model and guidance checks reach the four downstream
94
+ repositories, because the pin drift shows the manual pass does not hold for a
95
+ day.
96
+
97
+ ## 3. One way to attach middleware, or two
98
+
99
+ **Today:** core has the native `routes.<path>.middleware[]` array.
100
+ `urlcode-middleware` provides the same capability through the extension seam
101
+ and states that core's native array "keeps working unchanged".
102
+ [The layering spike](../../SPIKE-CORE-LAYERING.md) proposed *extracting* middleware
103
+ to narrow core to YAML plus `function`; what exists is an addition alongside it.
104
+
105
+ **Why this is a principle question:** the
106
+ [extension model review](SPIKE-EXTENSION-MODEL.md) rejected an earlier design
107
+ for exactly this reason — two ways to protect a route would drift apart — and
108
+ resolved it by removing one. The same test applies here.
109
+
110
+ **Options:** deprecate the native array on a stated timeline and complete the
111
+ extraction; or keep both and document precisely when each is correct, treating
112
+ the extension as a bounded variant rather than a successor.
113
+
114
+ **Recommendation:** decide explicitly and write the answer into both
115
+ repositories. Either is defensible; leaving both undescribed is not.
116
+
117
+ ## 4. Where the backlog lives
118
+
119
+ **Today:** [next steps](NEXT-STEPS.md) holds roughly thirty tracked items
120
+ across nine phases. The repository has one open issue (#58). `AGENTS.md`
121
+ instructs contributors and agents to file what they find as issues on the
122
+ owning repository.
123
+
124
+ **The contradiction:** the instruction points at the issue tracker; the actual
125
+ plan is a Markdown file that no tracker reflects. A plan document also has no
126
+ state, so items stay written as future work after they ship — which is how
127
+ route-level `auth` came to be described as an invented field in merged agent
128
+ guidance (see item 7).
129
+
130
+ **Options:** move the phase items into issues and keep the document as the
131
+ narrative that links them; or keep the document as the system of record and
132
+ amend `AGENTS.md` to say so.
133
+
134
+ **Recommendation:** issues for items, document for sequence. It is the only
135
+ arrangement where "done" is recorded automatically.
136
+
137
+ ## 5. Gate the business suite on evidence
138
+
139
+ **Today:** [the business suite spike](../../SPIKE-BUSINESS-SUITE.md) proposes seven
140
+ applications. [Project direction](../../PROJECT-DIRECTION.md) states the evidence
141
+ test: the framework grows from measured repetition, not from a list of things
142
+ applications might need. Phase 6 of [next steps](NEXT-STEPS.md) says candidate
143
+ areas are built only when the repetition log shows them repeating. The
144
+ repetition log does not exist yet, and the Phase 0 agent benchmark has not been
145
+ run.
146
+
147
+ **Recommendation:** record on the spike itself that it is gated behind the
148
+ benchmark and the repetition log, or amend the evidence test. Holding both
149
+ positions unannotated makes the stated principle decorative.
150
+
151
+ ## 6. Where documentation is authored — decided
152
+
153
+ **Decided 2026-09-19: documentation is authored in this repository, in
154
+ `docs/`.** The instruction that sent new reader-facing pages to urlcode-docs is
155
+ removed from `AGENTS.md`, `CONTRIBUTING.md`, `README.md` and `docs/README.md`.
156
+ A behaviour change and its documentation now ship in the same pull request,
157
+ which is the arrangement where neither can land alone.
158
+
159
+ The original entry, for the record:
160
+
161
+ > **Today:** `AGENTS.md` states that urlcode-docs is the documentation home and
162
+ > the only place readers are sent, that new reader-facing pages go there, and
163
+ > that pages still under `docs/` here are being migrated. Roughly forty
164
+ > reader-facing pages remain in this repository, several duplicated in
165
+ > urlcode-docs.
166
+ >
167
+ > **Observed consequence:** the same fact drifts between copies. The public
168
+ > documentation site carried the correct trusted-by-default contract while
169
+ > `docs/OPERATIONS.md` in this repository still described functions as
170
+ > untrusted and isolated by default. <!-- trust-model-prose: historical -->
171
+
172
+ **The duplication is resolved by retirement, not by merging.** `urlcode-docs`
173
+ was deleted on 2026-09-19, along with `urlcode-short` and
174
+ `urlcode-dynamic-link`; all three GitHub repositories are gone, so links to
175
+ them 404 with no redirect. Rather than reconcile 51 drifted page pairs, the
176
+ content that was genuinely ahead in `urlcode-docs` was brought across before it
177
+ went away:
178
+
179
+ - Trusted-by-default corrections it carried and this repository did not, in
180
+ `POLICIES.md`, `policies/compression.md`, `BEST-PRACTICES.md`, `ASSETS.md`
181
+ and `PRERENDER.md`.
182
+ - Two pages that existed **only** there: `MANAGEMENT-SECURITY.md`, which
183
+ `SECURITY-AUDIT.md` already linked to twice from this repository and which
184
+ was therefore a live broken link, and `EXTENSION-IMPLEMENTATION.md`.
185
+
186
+ Its other pages were either behind this repository, or copies of pages owned by
187
+ `urlcode-auth`, `urlcode-admin`, `urlcode-ui` and `urlcode-dynamic-link`. Some
188
+ were actively stale: its `PRERENDER.md` and `VERCEL.md` still described the
189
+ `link` handler that `0.4.0-alpha.2` removed, so taking either wholesale would
190
+ have reintroduced a removed feature. Every page was judged individually.
191
+
192
+ **The lesson worth keeping:** the drift reached this size because the same page
193
+ existed in two places with no record of which side won. `AGENTS.md` now states
194
+ that documentation is authored here, so the second copy cannot reappear.
195
+
196
+ ## 7. A review window, and a check that does not need one
197
+
198
+ **Today:** [governance](../../../GOVERNANCE.md) records one maintainer, a required
199
+ approval count of zero, and that "an independent human review is not yet
200
+ guaranteed". Pull requests and CI are mandatory; a second pair of eyes is not.
201
+
202
+ **What that permitted, concretely:** on 2026-09-19, three coordinated pull
203
+ requests (urlcode#158, urlcode-template#6, and urlcode-docs#17 — the last no
204
+ longer resolvable, that repository having been deleted) opened and merged
205
+ within nine minutes. They recorded a genuine improvement — the declarative-first
206
+ principle, propagated to every copy in one change — and alongside it the
207
+ statement that agents must "never invent an `auth` field". Route-level `auth` is
208
+ implemented: it is defined in `schemas/urlcode.schema.json` as a short form
209
+ expanding to `policies.extensions.auth`, expanded in `src/config.ts`, typed in
210
+ `src/types.ts`, and asserted in `test/recipes.test.ts`. The guidance now
211
+ instructs agents away from a supported declarative short form and toward the
212
+ lower-level policy form — the opposite of the principle the same change
213
+ introduced — in the generated project guide, the bundled starter, both
214
+ authoring skills, the packaged plugin skill and the two generated `llms` files.
215
+
216
+ Nothing failed. Lint, typecheck, generated-resource checks, the package smoke
217
+ test and 501 tests all passed, because no check compares what the guidance
218
+ claims against what the schema implements.
219
+
220
+ **The check, now implemented.** `scripts/check-guidance-claims.ts` runs inside
221
+ `npm run check` and exits non-zero on a contradiction between agent-facing
222
+ guidance and the schema, so this class of error fails CI instead of depending on
223
+ a reviewer's attention:
224
+
225
+ - Inputs: the agent-facing surfaces — `src/agents-guide.ts`,
226
+ `starters/default/AGENTS.md`, `skills/urlcode/SKILL.md`,
227
+ `.claude/skills/*/SKILL.md`, `packaging/claude-plugin/skills/*/SKILL.md`,
228
+ `llms.txt`, `llms-full.txt`, `docs/AI-AUTHORING.md`.
229
+ - Assertion one: every YAML field named as valid in those files resolves in
230
+ `schemas/urlcode.schema.json`. A guidance file may not teach a field the
231
+ schema does not accept.
232
+ - Assertion two, the one that would have caught this: no field that the schema
233
+ *does* define is described as unsupported, invented or nonexistent. Match the
234
+ negative phrasings deliberately ("never invent", "does not exist",
235
+ "unsupported field") within a short window of a schema-resolvable field name.
236
+ - Assertion three: handlers listed as available match the capability catalog,
237
+ so a removed handler (`link`, extracted to `urlcode-dynamic-link` in
238
+ `f7dbe54`) cannot linger in generated guidance.
239
+ - Exits non-zero on violation, with a documented `<!-- guidance-claims: ignore -->`
240
+ marker for text that is deliberately about another version. Verified against
241
+ both regressions: reintroducing the "never invent an `auth` field" sentence
242
+ fails the check, and adding the removed `link` handler to the inventory line
243
+ fails it.
244
+
245
+ What remains a decision: whether `npm run check` membership is enough, or the
246
+ check should also be named in the repository's required status checks so it
247
+ cannot be bypassed.
248
+
249
+ **Recommendation:** the check is in; keep it required. It is the part that does
250
+ not depend on a person being available. Whether to
251
+ also raise the required approval count is a separate call, and
252
+ [governance](../../../GOVERNANCE.md) already states the condition — when the trusted
253
+ maintainer team grows.
254
+
255
+ ## 8. One publishing convention
256
+
257
+ **Today:** `urlcode-dynamic-link` and `urlcode-middleware` keep
258
+ `"private": true` on their main branch and drop it in the release commit;
259
+ `urlcode-auth`, `urlcode-admin` and `urlcode-ui` do not. All are published.
260
+ Both practices are defensible; having both means the flag no longer indicates
261
+ publication state.
262
+
263
+ **Recommendation:** pick one and state it where release process is documented.
264
+
265
+ ## 9. Adjudicate the vendored skill drift
266
+
267
+ **Today:** `npm run check:downstream-skills` reports that `urlcode-template`'s
268
+ vendored authoring and operations skills differ from core's current `main` by
269
+ 79 and 91 lines. The report is advisory by design and never fails, because a
270
+ downstream repository can correctly pin an older published core version
271
+ ([issue 155](https://github.com/jimhoyd-com/urlcode/issues/155)).
272
+
273
+ **What is missing:** the judgment the report defers. No one has read the two
274
+ diffs and recorded which side is right for the template's `0.4.0-alpha.1` pin.
275
+
276
+ **Recommendation:** review both diffs once, record the verdict, and re-run the
277
+ report when the template repins.
@@ -0,0 +1,186 @@
1
+ # Historical release process — superseded September 19, 2026
2
+
3
+ This is the prior process and incident record. Current instructions live in
4
+ [release security](../../RELEASE-SECURITY.md). Links below resolve to the current documentation unless otherwise noted.
5
+
6
+ # Candidate and release security process
7
+
8
+ URLCode 0.3.0 is the Apache-2.0 self-hosted baseline; licensing is defined
9
+ in [LICENSE](../../../LICENSE). Two workflows share one audited build path and differ
10
+ only in what they do with its output:
11
+
12
+ - `candidate.yml` is dispatched manually on main and retains a **signed build
13
+ candidate** for 30 days without publishing it. Use it to review a commit
14
+ before tagging it.
15
+ - `release.yml` runs on a `v*` tag whose commit is already on main, and publishes
16
+ a GitHub release. Publication to npm and GHCR is opt-in per repository variable
17
+ (`PUBLISH_NPM`, `PUBLISH_CONTAINER`), so a release can be artifacts-only.
18
+
19
+ Neither workflow is a statement that a release is production-ready; see
20
+ [release readiness](../../RELEASE-READINESS.md).
21
+
22
+ ## Publishing an alpha
23
+
24
+ An alpha such as `0.4.0-alpha.1` follows the same path: tag `v0.4.0-alpha.1`
25
+ on a commit that is already on main. The release workflow publishes to npm
26
+ only when the repository variable `PUBLISH_NPM` is `true` and the npm trusted
27
+ publisher for this repository and `release.yml` exists; otherwise the run is
28
+ artifacts-only (a GitHub release with the signed tarball). Publish order is
29
+ core → ui → auth → admin, because the extension packages declare
30
+ `@jimhoyd/urlcode >=0.4.0-alpha.1 <0.5.0` and must resolve the core alpha.
31
+
32
+ 1. Report vulnerabilities privately through [GitHub advisories](https://github.com/jimhoyd-com/urlcode/security/advisories/new).
33
+ The maintainer triages impact, confirms affected exact revisions, coordinates a
34
+ fix/retest privately, and publishes an advisory with upgrade guidance when safe.
35
+ Current reviewed main is the security support baseline; no guaranteed SLA/backports.
36
+ 2. Change dependencies through protected PRs. Review upstream provenance/advisories,
37
+ lockfile integrity and tests. Direct dependencies are exact, npm installs use
38
+ `npm ci`, CI actions use full commit SHAs, and container bases use SHA-256 digests.
39
+ Dependabot proposes updates; it does not authorize merging. Never silently
40
+ refresh dependencies during a candidate build.
41
+ 3. After protected main checks pass, a maintainer manually dispatches
42
+ `.github/workflows/candidate.yml` **on main**, or pushes a `v<version>` tag to
43
+ run `.github/workflows/release.yml`. The release workflow refuses a tag whose
44
+ commit is not an ancestor of main, refuses a tag that disagrees with
45
+ `package.json`, and refuses to build a private or unlicensed package. It reads and validates the digest-pinned Node image directly from Dockerfile,
46
+ installs without lifecycle scripts, verifies, package-tests and runs local drills.
47
+ It packs the runtime and creates a CycloneDX dependency SBOM plus a manifest
48
+ recording source commit, lockfile hash, engine versions and artifact hashes.
49
+ The package contains `dist/`, produced inside that build from the tagged
50
+ TypeScript sources by Node's type stripping (`scripts/build.ts`); `dist` is
51
+ never committed. The manifest records the Node version that stripped it, the
52
+ locked TypeScript version and a SHA-256 per emitted file
53
+ (`dist/BUILD-MANIFEST.json`), so a download can be verified by running
54
+ `npm run build` on the tagged commit with that Node version and comparing its
55
+ `dist/` file by file. The build strips types and rewrites specifier
56
+ extensions; it never bundles, minifies or transforms syntax, so every line of
57
+ `dist/x.js` is the corresponding line of `src/x.ts`. CI's `build-fidelity`
58
+ job builds twice and diffs the trees, so the transform is known to be
59
+ deterministic before a tag is cut.
60
+ 4. The pinned official `actions/attest` action signs provenance for the package,
61
+ SBOM and manifest using short-lived GitHub OIDC/Sigstore credentials. No long-lived
62
+ signing key is stored. Signing permissions exist only in this manual job; build
63
+ commands run in a container without passing GitHub tokens. Candidate files are
64
+ retained as GitHub Actions artifacts for 30 days. A release additionally attaches
65
+ them to the GitHub release, and publishes to npm and to GHCR when those
66
+ repository variables are enabled.
67
+
68
+ **npm publication holds no credential.** The registry is configured with a
69
+ trusted publisher naming this repository and `release.yml`, so the publish
70
+ step exchanges the job's OIDC identity for a credential that lives for the
71
+ length of one publish. There is no npm token in the repository's secrets to
72
+ leak, revoke or rotate, and a fork or another workflow cannot publish under
73
+ this package's name. Provenance is generated on that same identity, so
74
+ `--provenance` is not passed and its absence is not a downgrade.
75
+
76
+ A bearer token would silently take precedence over this exchange, so the
77
+ publish step must reference none; `test/release.test.ts` fails if one
78
+ reappears in it, and checks the npm and Node floors below which the exchange
79
+ is not attempted at all.
80
+ 5. Download the candidate for the intended commit and verify **each file**, e.g.
81
+ `gh attestation verify jimhoyd-urlcode-0.3.0.tgz --repo jimhoyd-com/urlcode --signer-workflow jimhoyd-com/urlcode/.github/workflows/candidate.yml --source-ref refs/heads/main --source-digest APPROVED_COMMIT_SHA`
82
+ (use `release.yml` as the signer workflow for a tagged release).
83
+ Check the verified provenance's source commit against the approved commit, and
84
+ compare package/SBOM hashes with the signed manifest. A signature establishes
85
+ provenance, not safety, reproducible bytes or production approval. See
86
+ [GitHub verification](https://cli.github.com/manual/gh_attestation_verify) and
87
+ [the official attestation action](https://github.com/actions/attest).
88
+ 6. Before registry publication, assign patch/release ownership and retention, and
89
+ document version/support policy. Before claiming hostile multi-tenant or
90
+ deployment-specific readiness, close independent-review and deployment gates. Keep last-good verified artifacts and compatible policies;
91
+ rehearse rollback. Never overwrite a published artifact/version or downgrade a
92
+ management writer past its audit/security baseline.
93
+
94
+ The SBOM describes npm dependencies; it is not a complete OS/container SBOM. Hosted
95
+ runners and action runtimes remain platform-controlled. Digest pins improve supply
96
+ chain integrity but do not prove byte-for-byte reproducibility or engine safety.
97
+ Signing verification must be demonstrated on a successful main workflow run before
98
+ claiming a candidate has been signed. Workflow definition alone is not that evidence.
99
+ `release.yml` ran successfully for the first time publishing `v0.4.0-alpha.1`
100
+ (2026-09-18), after the fixes below; treat any repository whose workflow has not
101
+ had a real successful tagged run the same way this one was treated before that.
102
+
103
+ ## What broke on every first release, and why
104
+
105
+ Four repositories (core, ui, auth, admin) each cut their first tagged release in
106
+ the same session. Every one hit a subset of the same bugs, because each
107
+ `release.yml` was written and reviewed but never actually run end-to-end against
108
+ a real tag before. None of this is repository-specific; check for all of it
109
+ before trusting an unexercised release workflow:
110
+
111
+ - **`npm ci` on the bare runner's root-owned npm.** A build that runs entirely
112
+ inside a pinned Docker image never calls `actions/setup-node`, so a later
113
+ `npm install --global npm@11.5.1` (needed for the trusted-publishing floor)
114
+ hits the runner's preinstalled, root-owned npm and fails `EACCES`. Add
115
+ `actions/setup-node` before any step that installs global npm packages, even
116
+ if the main build never touches the runner's own Node.
117
+ - **The floor check must run after the pin, not before.** A guard asserting
118
+ "npm ≥ 11.5.1" is useless directly after `setup-node` with `node-version: '22'`,
119
+ which bundles npm ~10.9.x — it can never pass. The floor only means something
120
+ once the publish step's own `npm install --global npm@11.5.1` has actually run.
121
+ - **`npm publish` refuses an unqualified prerelease.** `You must specify a tag
122
+ using --tag when publishing a prerelease version.` npm's safety default is
123
+ `latest`; a prerelease must derive an explicit dist-tag from its version
124
+ (`0.1.0-alpha.1` → `alpha`, anything without a `-` → `latest`) and pass
125
+ `--tag`. This path is only exercised by a package's *first* prerelease, so it
126
+ silently sat broken in every repository until each hit it for the first time.
127
+ - **`npm pack --pack-destination candidate` needs `candidate/` to exist first.**
128
+ npm does not create the destination directory; `mkdir -p candidate` first.
129
+ - **A private repository's unauthenticated `git fetch origin main` cannot work.**
130
+ If checkout uses `persist-credentials: false` (correct, for a step that
131
+ should not need write access) and the repo is private, `git fetch` fails
132
+ `could not read Username for 'https://github.com'` before ever reaching the
133
+ version check. Compare the tag against main through the GitHub API instead
134
+ (`gh api repos/OWNER/REPO/compare/main...SHA --jq .status`, expecting
135
+ `identical` or `behind`) — it needs no credentials and stays read-only. Public
136
+ repositories can keep the plain fetch; it works there.
137
+ - **`--conditions=development` in `npm run verify`'s test script resolves peers
138
+ to source that a real npm install never ships.** The regular CI job symlinks
139
+ sibling checkouts in place of `node_modules`, so `./src/*.ts` exists and the
140
+ flag is correct there. A release installs real published tarballs of its
141
+ peers, which only ever ship `dist/`, so the same flag makes every import of a
142
+ peer fail `ERR_MODULE_NOT_FOUND`. Drop the flag for the release-workflow test
143
+ invocation specifically (run `node scripts/check-sqlite.mjs` explicitly first,
144
+ since bypassing `npm test` skips that pretest hook), and audit any test file
145
+ that separately hardcodes the flag in a spawned child process — it has to be
146
+ fixed the same way, independently, wherever it appears.
147
+ - **A peer-install command with the wrong flag combination is a silent no-op.**
148
+ `npm install --no-save --no-package-lock --ignore-scripts --legacy-peer-deps
149
+ <peer>@<version>` installed *nothing*, with no error, when the target package
150
+ names already appear in `peerDependencies` — `npm ci` earlier reports "added N
151
+ packages" as if it worked. Confirm the install actually happened
152
+ (`ls node_modules/@scope/*/package.json` and print each version) rather than
153
+ trusting the exit code; `--no-save --ignore-scripts <specs>` (no
154
+ `--no-package-lock`, no `--legacy-peer-deps`) is the version that works, paired
155
+ with `git diff --exit-code -- package.json package-lock.json` to prove nothing
156
+ was recorded as a dependency.
157
+ - **A version published from an unbuilt checkout is burned forever.** npm never
158
+ allows a version to be replaced. `@jimhoyd/urlcode-auth@0.1.0-alpha.1` reached
159
+ the registry from something other than the CI workflow (a manual `npm
160
+ publish` run before `npm run build` had produced `dist/`), so the published
161
+ tarball contained only metadata files and no code. Every consumer's typecheck
162
+ failed with `Cannot find module '@jimhoyd/urlcode-auth'` — a real, correct
163
+ failure, not a bug in the consumer. The only fix is bumping to a new version
164
+ and publishing that instead; nothing can repair or unpublish the bad one.
165
+ **Never run `npm publish` by hand outside the release workflow** — the
166
+ workflow is the only place that reliably builds before packing.
167
+ - **`ENEEDAUTH` on `npm publish` under trusted publishing usually means the
168
+ registry-side configuration doesn't exist or doesn't permit direct publish
169
+ yet**, not a workflow bug. Trusted publishing needs an entry under the
170
+ package's npm settings ("Trusted Publisher") naming the exact GitHub
171
+ org/repo and workflow filename, with no environment set unless the workflow
172
+ declares one; recent npm UI changes default new configurations to
173
+ "stage publish" only; "allow npm publish" (direct publish, which is what
174
+ this project's workflows do) must be explicitly enabled too. A wrong
175
+ org/repo/workflow match tends to surface as a 404, not `ENEEDAUTH`;
176
+ `ENEEDAUTH` is the signature of no matching configuration existing at all.
177
+ - **Publish order matters and is easy to get backwards.** Extension packages
178
+ declare `@jimhoyd/urlcode >=X <Y` as a peer range; publish core before ui,
179
+ auth or admin, or their own release-workflow peer-install step has nothing
180
+ real to resolve against.
181
+
182
+ None of the above is exotic; all nine bugs were found by actually running each
183
+ workflow against a real tag, one release at a time, and reading the actual
184
+ failure rather than guessing from the workflow source. Treat "the workflow file
185
+ looks right" and "the workflow has actually published successfully once" as two
186
+ different, unrelated claims.