@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.
- package/.claude/skills/urlcode-authoring/SKILL.md +17 -19
- package/.claude/skills/urlcode-operations/SKILL.md +9 -9
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +20 -16
- package/README.md +59 -64
- package/ROADMAP.md +66 -376
- package/dist/BUILD-MANIFEST.json +26 -25
- package/dist/agents-guide.js +6 -6
- package/dist/authoring.js +15 -1
- package/dist/build-static.js +2 -0
- package/dist/capability-query.js +0 -1
- package/dist/catalog.js +0 -1
- package/dist/cli.js +25 -9
- package/dist/config.js +1 -1
- package/dist/explain-cli.js +4 -2
- package/dist/explain.js +8 -2
- package/dist/extensions.js +1 -1
- package/dist/http-response.js +1 -1
- package/dist/index.js +1 -0
- package/dist/init-with.js +36 -11
- package/dist/manifest.js +8 -2
- package/dist/mcp-authoring.js +4 -4
- package/dist/mcp.js +1 -1
- package/dist/policies/cache.js +2 -2
- package/dist/policies.js +3 -1
- package/dist/prerender.js +4 -0
- package/dist/project-dependencies.js +305 -0
- package/dist/readiness.js +3 -0
- package/dist/route-diff.js +12 -5
- package/dist/runtime.js +1 -1
- package/dist/trusted-functions.js +4 -5
- package/dist/types/authoring.d.ts +9 -1
- package/dist/types/capability-query.d.ts +0 -1
- package/dist/types/catalog.d.ts +0 -4
- package/dist/types/config.d.ts +1 -9
- package/dist/types/explain.d.ts +6 -1
- package/dist/types/extensions.d.ts +1 -1
- package/dist/types/http-response.d.ts +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/init-with.d.ts +7 -13
- package/dist/types/manifest.d.ts +5 -2
- package/dist/types/project-dependencies.d.ts +78 -0
- package/dist/types/readiness.d.ts +2 -0
- package/dist/types/trusted-functions.d.ts +1 -4
- package/dist/types/types.d.ts +8 -1
- package/dist/types.js +8 -1
- package/dist/typescript-authoring.js +18 -7
- package/docs/AI-AUTHORING.md +15 -7
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/AWS.md +9 -0
- package/docs/BEST-PRACTICES.md +17 -9
- package/docs/CAPABILITIES.md +1 -1
- package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
- package/docs/CI.md +8 -3
- package/docs/CODEBASE-AUDIT-2026-09-20.md +284 -0
- package/docs/COMPOSING-A-SITE.md +278 -0
- package/docs/DEVELOPMENT-PIPELINE.md +270 -0
- package/docs/EXTENSIONS.md +51 -11
- package/docs/FRAMEWORK.md +67 -48
- package/docs/FUNCTION-SECURITY.md +44 -0
- package/docs/INSTALL.md +13 -8
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/MIDDLEWARE.md +10 -4
- package/docs/OPEN-DECISIONS.md +224 -212
- package/docs/OPERATIONAL-PROOF.md +3 -3
- package/docs/OPERATIONS.md +3 -3
- package/docs/POLICIES.md +13 -5
- package/docs/PRERENDER.md +23 -11
- package/docs/PROJECT-DIRECTION.md +3 -3
- package/docs/READINESS.md +10 -5
- package/docs/README.md +20 -44
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-0.4.1.md +73 -0
- package/docs/RELEASE-READINESS.md +6 -6
- package/docs/RELEASE-SECURITY.md +96 -181
- package/docs/RESILIENCE.md +3 -3
- package/docs/ROUTING.md +3 -4
- package/docs/SECURITY-AUDIT.md +2 -2
- package/docs/SPECIFICATION.md +12 -6
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
- package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
- package/docs/SPIKE-CORE-LAYERING.md +49 -18
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
- package/docs/STARTERS.md +17 -5
- package/docs/STATIC.md +14 -3
- package/docs/TOOLING.md +10 -7
- package/docs/TYPESCRIPT-AUTHORING.md +6 -1
- package/docs/VERCEL.md +10 -2
- package/docs/VERSION-ALIGNMENT.md +76 -201
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
- package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
- package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
- package/docs/archive/2026-09-19/ROADMAP.md +387 -0
- package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
- package/docs/archive/README.md +28 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/docs/yaml/functions.md +10 -2
- package/docs/yaml/middleware.md +5 -3
- package/examples/assets/example.yaml +1 -1
- package/examples/cookbook/middleware/envelope.mjs +4 -2
- package/examples/cookbook/route-index.json +1 -1
- package/examples/cookbook/routes/middleware.yaml +1 -1
- package/examples/prerender/README.md +14 -6
- package/examples/prerender/functions/page.mjs +4 -2
- package/examples/prerender/middleware/template.mjs +1 -1
- package/examples/prerender/prerender.mjs +1 -1
- package/examples/prerender/urlcode.yaml +8 -4
- package/llms-full.txt +503 -88
- package/llms.txt +6 -4
- package/package.json +27 -4
- package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
- package/recipes/authenticated-json-api/README.md +4 -3
- package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
- package/recipes/authenticated-json-api/recipe.yaml +1 -1
- package/recipes/contact-form/functions/contact.mjs +2 -1
- package/recipes/contact-form/recipe.yaml +2 -2
- package/recipes/cors-api/README.md +2 -2
- package/recipes/cors-api/recipe.yaml +1 -1
- package/recipes/health-page/README.md +1 -1
- package/recipes/json-api/README.md +1 -1
- package/recipes/json-api/recipe.yaml +3 -3
- package/recipes/middleware/README.md +8 -4
- package/recipes/middleware/middleware/envelope.mjs +4 -2
- package/recipes/protected-download/README.md +1 -1
- package/recipes/protected-download/recipe.yaml +1 -1
- package/recipes/static-plus-api/README.md +2 -2
- package/recipes/static-plus-api/public/index.html +1 -1
- package/recipes/static-plus-api/recipe.yaml +1 -1
- package/recipes/static-plus-api/urlcode.yaml +1 -1
- package/recipes/typescript/recipe.yaml +4 -4
- package/skills/urlcode/SKILL.md +6 -6
- package/starters/default/AGENTS.md +6 -6
- package/docs/SPIKE-LAMBDA-COMPILE.md +0 -201
- 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](
|
|
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,
|
|
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.
|