@jimhoyd/urlcode 0.4.8 → 0.5.5

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 (58) hide show
  1. package/README.md +18 -18
  2. package/dist/BUILD-MANIFEST.json +27 -23
  3. package/dist/agents-guide.js +8 -4
  4. package/dist/authoring.js +81 -7
  5. package/dist/build-cloudflare.js +1 -0
  6. package/dist/build-static.js +1 -0
  7. package/dist/cli.js +39 -14
  8. package/dist/config.js +7 -1
  9. package/dist/context.js +32 -4
  10. package/dist/ecosystem-cli.js +6 -0
  11. package/dist/explain-cli.js +1 -1
  12. package/dist/explain.js +2 -2
  13. package/dist/extension-artifacts.js +28 -34
  14. package/dist/extension-bundles.js +62 -0
  15. package/dist/extension-transport.js +41 -0
  16. package/dist/extensions.js +4 -0
  17. package/dist/feature-plan.js +99 -0
  18. package/dist/functions.js +2 -1
  19. package/dist/index.js +4 -2
  20. package/dist/init-with.js +55 -23
  21. package/dist/interchange.js +1 -1
  22. package/dist/match.js +23 -5
  23. package/dist/mcp.js +6 -2
  24. package/dist/readiness.js +2 -2
  25. package/dist/review.js +206 -0
  26. package/dist/router.js +35 -10
  27. package/dist/runtime.js +1 -0
  28. package/dist/tooling.js +4 -0
  29. package/dist/types/agents-guide.d.ts +6 -1
  30. package/dist/types/authoring.d.ts +3 -1
  31. package/dist/types/context.d.ts +12 -0
  32. package/dist/types/explain.d.ts +3 -0
  33. package/dist/types/extension-artifacts.d.ts +15 -4
  34. package/dist/types/extension-bundles.d.ts +49 -0
  35. package/dist/types/extension-transport.d.ts +31 -0
  36. package/dist/types/extensions.d.ts +4 -0
  37. package/dist/types/feature-plan.d.ts +67 -0
  38. package/dist/types/functions.d.ts +4 -0
  39. package/dist/types/index.d.ts +4 -2
  40. package/dist/types/init-with.d.ts +6 -1
  41. package/dist/types/match.d.ts +1 -0
  42. package/dist/types/review.d.ts +30 -0
  43. package/dist/types/tooling.d.ts +4 -0
  44. package/dist/types/types.d.ts +10 -1
  45. package/dist/types.js +10 -3
  46. package/docs/AI-AUTHORING.md +466 -0
  47. package/docs/FUNCTION-SECURITY.md +251 -0
  48. package/docs/README.md +96 -0
  49. package/docs/TOOLING.md +422 -0
  50. package/docs/YAML-REFERENCE.md +473 -0
  51. package/llms-full.txt +190 -83
  52. package/llms.txt +73 -128
  53. package/package.json +15 -4
  54. package/recipes/redirect/README.md +2 -2
  55. package/recipes/store-crud/README.md +9 -10
  56. package/recipes/store-crud/recipe.yaml +1 -1
  57. package/schemas/urlcode.schema.json +3 -0
  58. package/starters/default/AGENTS.md +2 -2
package/llms-full.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  <!-- Generated by scripts/build-llms-full.ts (npm run docs:llms). Do not edit; edit the source documents. -->
2
- <!-- Consolidated URLCode authoring reference: 16 documents, about 59,492 tokens (estimate: characters / 4). -->
2
+ <!-- Consolidated URLCode authoring reference: 16 documents, about 61,239 tokens (estimate: characters / 4). -->
3
3
 
4
4
  <!-- urlcode-current-version:start -->
5
5
  # URLCode
@@ -7,10 +7,11 @@
7
7
  > A portable runtime for programmable URL behavior, and the framework that grows
8
8
  > from it: routes in YAML, functions and middleware, then accounts, administration and
9
9
  > stored links as operator-installed extensions. Stable project format
10
- > `version: "1"`. Core is Apache-2.0; this revision is `0.4.8`. `function`/`middleware`
10
+ > `version: "1"`. Core is Apache-2.0; this revision is `0.5.5`. `function`/`middleware`
11
11
  > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
12
- > auth, admin and ui extension packages in this repository are versioned at the same
13
- > revision; confirm what is published with `npm run release:status`.
12
+ > auth, admin, ui and store workspace packages are bundle sources. New sites install
13
+ > core from npm and verified executable extensions from an immutable GitHub Release;
14
+ > see package and channel alignment for the supported release pair.
14
15
  <!-- urlcode-current-version:end -->
15
16
 
16
17
  Use the schema and docs from the runtime revision you run. Do not assume Node
@@ -18,9 +19,6 @@ or fetch inside a `sandbox: true` function, regex routes, database access,
18
19
  global middleware, YAML interpolation, or packages named in YAML. Secrets need external revision-pinned
19
20
  grants. Unsupported features fail with the route named; nothing degrades silently.
20
21
 
21
- Agents that explicitly want the complete consolidated reference in one fetch should read
22
- [llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt), generated from the documents below (about 50k tokens, estimated).
23
-
24
22
  ## Contents
25
23
 
26
24
  1. [The URLCode framework](#the-urlcode-framework)
@@ -46,12 +44,12 @@ Agents that explicitly want the complete consolidated reference in one fetch sho
46
44
 
47
45
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md
48
46
 
49
- One page for people and AI agents. It says what the four packages are, how a
47
+ One page for people and AI agents. It says what the six workspace packages are, how a
50
48
  project grows from a handful of redirects into an application with accounts
51
49
  and an administration console, and which facts an agent must not guess. Every
52
50
  claim here is implemented in the linked repository; nothing is roadmap.
53
51
 
54
- ### Four packages, one project shape
52
+ ### Six workspace packages, one project shape
55
53
 
56
54
  | Package | Source | What it adds | How a project declares it |
57
55
  |---|---|---|---|
@@ -59,12 +57,17 @@ claim here is implemented in the linked repository; nothing is roadmap.
59
57
  | `@jimhoyd/urlcode-ui` | [`packages/ui`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
60
58
  | `@jimhoyd/urlcode-auth` | [`packages/auth`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
61
59
  | `@jimhoyd/urlcode-admin` | [`packages/admin`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
62
-
63
- All four are Apache-2.0. Each package's npm `latest` tag identifies its stable
64
- version. The current publisher records the exact four-package stack tested
65
- together on each new GitHub release and attaches the same information in signed
66
- `train.json` metadata.
67
- A stable npm channel is not an independent assessment: review, deployment
60
+ | `@jimhoyd/urlcode-store` | [`packages/store`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/store) | Durable bounded JSON collections exposed as a typed CRUD API | `extensions.store` plus a protected collection mount |
61
+ | `@jimhoyd/urlcode-forms` | [`packages/forms`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/forms) | Bounded server-rendered form flows: escaped controls, admission, CSRF, validation and a fixed confirmation | `extensions.forms` plus a `GET, HEAD, POST` form mount; it composes with `ui` and optional `auth` |
62
+
63
+ All six are Apache-2.0. Core is published through npm, GitHub Releases and
64
+ Homebrew. The first-party executable extensions are published as signed,
65
+ immutable GitHub Release bundles; their source remains in these workspace
66
+ packages, but new sites do not install them from npm. The forms package is an
67
+ unreleased bundle source and is not implied by the currently recorded bundle
68
+ release. The legacy extension npm
69
+ packages are deprecated migration artifacts. A release channel is not an
70
+ independent assessment: review, deployment
68
71
  evidence and an accessibility assessment are still pending
69
72
  ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
70
73
  files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/IMPLEMENTATION-STATUS.md),
@@ -104,33 +107,40 @@ Each rung's YAML is valid on every rung above it.
104
107
  6. **Your own look.** A shared `presentation` (catalogue and theme variables)
105
108
  restyles auth and admin together; the `ui` extension adds the template kit,
106
109
  project copy, template and stylesheet overrides for kit-rendered pages.
110
+ 7. **Bounded data and forms.** The `store` extension supplies declared durable
111
+ collections; the `forms` extension supplies declared browser form flows over
112
+ the shared UI kit. Both are trusted operator extensions, not core YAML
113
+ handlers. Add `auth: true` where a flow or collection is per-account.
107
114
 
108
115
  Stored short links previously sat here as a native `link` route; that handler
109
116
  was removed from core. A `urlcode-dynamic-link` package owned them the same way
110
117
  `auth`/`admin` own their mounts, but it has been retired and unpublished; no
111
118
  package occupies this rung today.
112
119
 
113
- Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
114
- packages installed from npm and a Node host with a
115
- patched SQLite build; see each package's README ([auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/README.md),
116
- [admin](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/README.md), [ui](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/README.md)) for the
120
+ Rungs 1 to 3 need only the core package. Rungs 4 to 7 need a verified extension
121
+ bundle installed into an explicit operator host, once its source package appears
122
+ in a selected catalog. Auth and admin additionally need the Node/SQLite runtime
123
+ their packages document; forms declares Node, AWS and Vercel targets, while store
124
+ is currently Node-only. See each package's README ([auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/README.md),
125
+ [admin](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/README.md), [ui](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/README.md),
126
+ [store](https://github.com/jimhoyd-com/urlcode/blob/main/packages/store/README.md), [forms](https://github.com/jimhoyd-com/urlcode/blob/main/packages/forms/README.md)) for the
117
127
  exact requirement.
118
128
 
119
129
  ### The composition contract
120
130
 
121
- An extended project starts from the current stable packages and one command.
122
- `--save-exact` records the concrete versions selected from the independent
123
- `latest` channels:
131
+ An extended project starts from core and an immutable bundle release. The
132
+ release is an explicit operator choice; use the current verified tag from
133
+ [package and channel alignment](https://github.com/jimhoyd-com/urlcode/blob/main/docs/VERSION-ALIGNMENT.md):
124
134
 
125
135
  ```sh
126
- npm install --save-exact @jimhoyd/urlcode@latest @jimhoyd/urlcode-ui@latest @jimhoyd/urlcode-auth@latest @jimhoyd/urlcode-admin@latest
127
- urlcode init my-site --with ui,auth,admin
136
+ npm install @jimhoyd/urlcode
137
+ npx urlcode init my-site --with ui,auth,admin --bundle-release extension-bundles@v…
128
138
  ```
129
139
 
130
- Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
131
- local tarballs from a reviewed checkout for operators who install only source
132
- they have read one revision now covers core and every extension. Three files make an extended
133
- project. Nothing else is discovered by convention.
140
+ This produces a manifest with core only and a bundle lockfile for extensions.
141
+ `scripts/pack-sources.mjs` remains available to review reproducible source
142
+ inputs. Three files make an extended project. Nothing else is discovered by
143
+ convention.
134
144
 
135
145
  ```
136
146
  site/
@@ -202,34 +212,32 @@ package behavior.
202
212
  A signed declarative artifact is a separate, optional authoring input, not a
203
213
  fifth way to compose executable behavior. A project may lock an attested
204
214
  schema/example bundle and expose it through MCP `get_extension_artifacts` and
205
- `get_extension_artifact`; the npm package and operator host remain the only
206
- executable extension path. See [signed declarative artifacts](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#signed-declarative-artifacts).
215
+ `get_extension_artifact`; the verified executable bundle and explicit operator
216
+ host remain the executable extension path. See [signed declarative artifacts](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#signed-declarative-artifacts).
207
217
 
208
218
  ```sh
209
219
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
210
220
  ```
211
221
 
212
- `urlcode init <dir> --with ui,auth,admin` writes this layout in one step: it
213
- resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
214
- calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
215
- one `host.mjs` and one `README.md`, refusing before writing anything when a
216
- package is missing or two fragments collide (the contract is documented under
222
+ `urlcode init <dir> --with ui,auth,admin --bundle-release
223
+ extension-bundles@v…` writes this layout in one step. It verifies and locks the
224
+ named GitHub Release bundles, calls each verified module's `scaffold` export,
225
+ and merges fragments into `app/urlcode.yaml`, one
226
+ explicit `host.mjs` and one `README.md`, refusing before writing a site when a
227
+ bundle is missing or two fragments collide (the contract is documented under
217
228
  [scaffolding](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
218
229
  and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
219
230
  administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
220
231
  the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
221
232
  or mounts changes the revision and needs an explicit operator reapproval.
222
233
 
223
- The presentation tooling composes the same way, by naming packages rather than
224
- depending on them. `urlcode-ui` is the kit alone until
225
- `--extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin` names the packages
226
- that ship the other namespaces: each is resolved from `--project` with Node
227
- package resolution and imported for the namespace it exports, so `list`,
234
+ The presentation tooling composes the same way, by naming logical extensions
235
+ rather than npm dependencies. The UI bundle is the kit until the locked auth
236
+ and admin bundles add their namespaces, so `list`,
228
237
  `doctor`, `eject`, `preview` and `copy --missing` cover the `auth/*` and
229
238
  `admin/*` templates and copy the host registers, and a project override of an
230
- extension template is checked against the shipped view model. A package that is
231
- not installed is skipped. `urlcode init --with` writes the commands with the
232
- flag already set; `@jimhoyd/urlcode-ui` still depends on neither peer.
239
+ extension template is checked against the shipped view model. `urlcode init
240
+ --with --bundle-release` writes the release pin into the generated README.
233
241
 
234
242
  ### Rules an agent must follow
235
243
 
@@ -314,6 +322,13 @@ needed schema, example or README. Context is a summary with the
314
322
  constraints and exact commands, not a schema dump, and it never hides a
315
323
  capability limit: ask `capabilities NAME` before promising a feature.
316
324
 
325
+ When a goal spans routes, persistence or extensions, the next bounded query can
326
+ be `urlcode plan-feature "goal" --project DIR --json` (MCP `plan_feature`). It
327
+ matches only the current compiled project, capability catalog, bundled recipes,
328
+ locked inert artifacts and registrations already available to the session. Read
329
+ its operator prerequisites and explicit gaps as constraints, not as permission
330
+ to select packages, storage, keys or grants in project YAML.
331
+
317
332
  The complete catalogs (`urlcode capabilities`, `recipes list`), the compact
318
333
  [llms.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms.txt) index and the generated [llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt)
319
334
  stay available as deliberate fallback and reference, not as the opening move.
@@ -508,12 +523,12 @@ this project's own redirects — cheaper than this table or the recipe catalog.
508
523
  |---|---|---|
509
524
  | Fixed redirect (301/302/303/307/308, 302 default) | `redirect: {url, status}` | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
510
525
  | Parameterized path redirect (`/users/{id}` to `/profiles/{id}`) | `{name}` placeholder in `redirect.url` naming a declared path parameter | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
511
- | Fixed-depth suffix redirect (`/legacy/a/b` to `/modern/a/b`) | one route per depth, one placeholder per segment | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
526
+ | Root-relative redirect (`/users/{id}` to `/profiles/{id}`) | `redirect.url: /profiles/{id}`: one leading slash, path only, `{name}` placeholders | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
527
+ | Wildcard/suffix redirect (`/legacy/**` to `/modern/{**}`, any depth) | terminal `/**` route key with a literal prefix, `{**}` in the destination path; redirect only, not static or Cloudflare | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
512
528
  | Redirect that preserves query keys | `redirect.query.pass` (explicit allowlist) or `query.map` | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
513
529
  | Redirect that keeps the method/body (POST) | `methods` plus `status: 307` or `308` | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
514
530
  | 404 for unmatched paths | `site.notFound` (a project-relative `.html` file) | [site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SITE.md) |
515
- | Wildcard/suffix redirect (`/legacy/*` to `/modern/*`, any depth) — **gap** | not expressible; terminal `/*` and `{rest...}` are refused on `redirect` | [open decision](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md) |
516
- | Host-based, scheme-based or relative-URL redirect — **gap** | not expressible; destination must be a literal absolute `https://host/path` | [open decision](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md) |
531
+ | Host-based or scheme-based redirect — **gap** | not expressible; destination is a literal absolute `https://host/path` or a root-relative path | [open decision](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md) |
517
532
  | Security headers (CSP, HSTS, frame and referrer policy) | `policies.security: {headers: oshp}` or `policies.profile: hardened` | [security](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/security.md) |
518
533
  | Cache headers on a page, download or static mount | `cacheControl`: `no-cache` (default), `no-store`, `public, max-age=3600` or `public, max-age=31536000, immutable`; nothing else validates | [assets](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/assets.md) |
519
534
  | A cache strategy on any route | `policies.cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) |
@@ -537,8 +552,9 @@ Which handler serves the response:
537
552
  Data persistence has no native handler. The operator-installed `store` extension
538
553
  serves declared collections as a CRUD API, and `urlcode recipes search "crud store
539
554
  persist"` finds the `store-crud` recipe. It needs the operator to install
540
- `@jimhoyd/urlcode-store` (on npm) and a host file. `init --with
541
- ui,auth,store` scaffolds one from the published packages; a no-auth
555
+ an attested executable bundle and a host file. `init --with
556
+ ui,auth,store --bundle-release extension-bundles@v…` scaffolds the supported
557
+ npm-free form; a no-auth
542
558
  `--with store` needs `--ack store:public-write`, which only a core release after the
543
559
  store's first publication has, so say so rather than promising it. Report anything beyond that recipe (filtering, sorting, per-record
544
560
  ownership, a database) as a gap. `urlcode context` lists the same built-ins so
@@ -757,7 +773,7 @@ programmatic compatibility analysis and provider verification limits.
757
773
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md
758
774
 
759
775
  <!-- urlcode-current-version:start -->
760
- This guide targets URLCode 0.4.8. Start with the function example below,
776
+ This guide targets URLCode 0.5.5. Start with the function example below,
761
777
  then add only the fields your route needs. The authoritative machine-readable
762
778
  shape is [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json); semantic rules are in the
763
779
  [specification](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md). Unsupported fields fail validation.
@@ -898,6 +914,7 @@ schema-valid combinations activate successfully.
898
914
  | `routes.*.env.* (option 1).value` | string | yes | — |
899
915
  | `routes.*.env.* (option 2)` | object | no | unknown keys rejected |
900
916
  | `routes.*.env.* (option 2).env` | string | yes | pattern: "^[A-Za-z_][A-Za-z0-9_]*$" |
917
+ | `routes.*.env.* (option 2).default` | string | no | — |
901
918
  | `routes.*.secrets` | object | no | — |
902
919
  | `routes.*.secrets.*` | object | no | unknown keys rejected |
903
920
  | `routes.*.secrets.*.secret` | string | yes | pattern: "^[A-Za-z_][A-Za-z0-9_]*$" |
@@ -1327,7 +1344,8 @@ Keys are absolute case-sensitive paths. Trailing slashes are significant.
1327
1344
  Parameters occupy whole segments, e.g. `/p/{id}`, with distinct identifier names.
1328
1345
  Each parameter matches exactly one nonempty segment, never across `/`; it is not
1329
1346
  greedy. No regex paths, client-controlled host dispatch or dot segments. Only static directory mounts
1330
- support a terminal `/*` wildcard with an otherwise literal path. Route keys cannot contain
1347
+ support a terminal `/*` wildcard with an otherwise literal path; a `redirect` alone supports a
1348
+ terminal `/**` (one or more remaining segments, at least a one-segment literal prefix, see [route matching](https://github.com/jimhoyd-com/urlcode/blob/main/docs/ROUTING.md)). Route keys cannot contain
1331
1349
  percent encoding, spaces, backslashes or query strings. Path length is limited
1332
1350
  to 2,048 characters and 32 segments. `/_urlcode` is reserved.
1333
1351
 
@@ -1466,7 +1484,9 @@ Unknown query keys are ignored unless explicitly passed by a redirect.
1466
1484
  ### Redirects
1467
1485
 
1468
1486
  `redirect.url` is an absolute HTTP(S) URL with literal scheme/host and no embedded
1469
- credentials or whitespace/control characters. `{pathInput}` placeholders are
1487
+ credentials or whitespace/control characters, or a root-relative path (one leading `/`, never
1488
+ `//`, no dot segments) that answers a path-only `Location`. On a `/**` route `{**}` is the captured
1489
+ suffix, once, each segment encoded. `{pathInput}` placeholders are
1470
1490
  allowed only in the destination pathname and encoded as single components.
1471
1491
  No environment/secret interpolation. Status defaults to 302; allowed values are
1472
1492
  301, 302, 303, 307 and 308.
@@ -1590,12 +1610,17 @@ parity for a `sandbox: true` route; a trusted route has no such restriction.
1590
1610
 
1591
1611
  Context contains `inputs.path/query/header`, `args`, `env`, `secrets`. Arguments
1592
1612
  may be scalar literals, input references, `{env: alias}` or `{secret: alias}`.
1593
- Bindings use `{value: "literal"}`, `{env: EXTERNAL_NAME}` or `{secret: logical_name}`.
1594
- Literal non-secret values need no grant. Every external environment or secret
1595
- binding is denied unless an operator policy grants that exact name to the route
1596
- and matches the SHA-256 of the current configuration/source snapshot.
1597
- A project cannot grant itself capabilities. See [policy setup](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md).
1598
- Missing bindings also reject activation. Inspection parses source without running it.
1613
+ Bindings use `{value: "literal"}`, `{env: EXTERNAL_NAME}`,
1614
+ `{value: "default", env: EXTERNAL_NAME}` or `{secret: logical_name}`. The
1615
+ combined form resolves to the process environment variable when set and
1616
+ non-empty, else `value`; it needs the same operator grant as `{env: ...}`
1617
+ alone. Literal non-secret values need no grant. Every external environment or
1618
+ secret binding is denied unless an operator policy grants that exact name to
1619
+ the route and matches the SHA-256 of the current configuration/source
1620
+ snapshot. A project cannot grant itself capabilities. See
1621
+ [policy setup](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md). Missing bindings reject activation
1622
+ (the combined form always resolves, via its default). Inspection parses
1623
+ source without running it.
1599
1624
 
1600
1625
  Development may read `.env.local`; process values win. Serving never reads it.
1601
1626
  Dotenv supports single-line NAME=value, paired single/double quotes, blank lines
@@ -1709,6 +1734,7 @@ not part of the route key.
1709
1734
  | `/go` | `/go`, `/go?campaign=spring` | `/Go`, `/go/`, `/go/extra` |
1710
1735
  | `/r/{code}` | `/r/abc`, `/r/123` | `/r/`, `/r/abc/extra` |
1711
1736
  | `/r/{code}/details` | `/r/abc/details` | `/r/abc/other/details` |
1737
+ | `/legacy/**` with a `redirect` handler | `/legacy/a`, `/legacy/a/b/c` | `/legacy`, `/legacy/`, `/legacy/a//b` |
1712
1738
  | `/assets/*` with a `static` handler | Files under `/assets/`, including `/assets/css/site.css` | `/assets`, `/assets-other/site.css` |
1713
1739
 
1714
1740
  A `{parameter}` captures exactly one nonempty path segment. It is **not greedy**:
@@ -1721,8 +1747,16 @@ path. It covers the remaining nested file path; it is not a named capture or a
1721
1747
  regex operator. Matching a mount does not guarantee a response file exists:
1722
1748
  missing files return 404. It is not a catch-all for functions or redirects.
1723
1749
 
1750
+ A `redirect` handler alone supports a terminal `/**` after a literal prefix (never bare `/**`, never
1751
+ with a `{parameter}`). It matches one or more remaining segments, and `{**}` in `redirect.url` is
1752
+ those segments, each percent-encoded and joined by `/`, usable once and only in the destination
1753
+ path. Empty segments, `.`/`..`, encoded slashes and captures over 1,024 characters do not match.
1754
+ Exact and `{parameter}` routes always win over it, so `/legacy/keep/{id}` can carve an exception out
1755
+ of `/legacy/**`. It is refused on static hosting (S3 redirects match one path) and on Cloudflare
1756
+ until the Worker table supports suffix matching, and it cannot share a prefix with a `static` mount.
1757
+
1724
1758
  No regex routes, greedy parameters, optional segments, partial-segment parameters,
1725
- `**` globs, or regex constraints inside `{code}` are implemented. Characters such
1759
+ other `**` globs, or regex constraints inside `{code}` are implemented. Characters such
1726
1760
  as `.` and `+` have no regex meaning in a literal path. Do not paste a regex into
1727
1761
  a route key: some regex-looking text is legal literal text, while unsupported
1728
1762
  syntax may fail validation. Parameter-schema `pattern` is also unsupported.
@@ -2821,7 +2855,7 @@ Extensions are trusted operator modules, separate from a project's own
2821
2855
  `function`/`middleware` code. Auth
2822
2856
  and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
2823
2857
  supplies only the generic integration contract. No project file can import a host
2824
- extension or choose its npm package.
2858
+ extension, choose a bundle release, or choose an npm package.
2825
2859
 
2826
2860
  Stored short links moved out of core this way too: a `urlcode-dynamic-link`
2827
2861
  package (mount-based, like `auth`/`admin`) owned the durable link store, its CLI
@@ -2833,6 +2867,13 @@ The `store` extension is the data-owning counterpart: it serves declared,
2833
2867
  bounded collections as a CRUD API from an operator-owned directory. See
2834
2868
  [data store](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STORE.md).
2835
2869
 
2870
+ The `forms` extension is the browser-flow counterpart: it renders bounded
2871
+ declared fields through the `ui` kit, validates URL-encoded submissions with
2872
+ its host-supplied CSRF secret, and redirects a successful submission to a
2873
+ fixed confirmation page. It is a trusted operator extension, needs `ui`, and
2874
+ may be mounted with `auth: true`; its optional `onSubmit` hook is trusted
2875
+ project code rather than a sandbox bridge. See the [forms package](https://github.com/jimhoyd-com/urlcode/blob/main/packages/forms/README.md).
2876
+
2836
2877
  A project declares versioned configuration and exclusive route mounts:
2837
2878
 
2838
2879
  ```yaml
@@ -3217,29 +3258,24 @@ Assembly rules, in the resolved order:
3217
3258
 
3218
3259
  #### Recorded versions
3219
3260
 
3220
- `init --with` also writes `<directory>/package.json`: a private manifest that
3221
- pins, at exactly the version that was resolved, the running runtime, every
3222
- extension named in `--with`, and every package those extensions declare in
3223
- `peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
3224
- although nobody named it). Before anything is written, the whole set is checked
3225
- against every declared peer range; an incompatible combination or a missing
3226
- required peer refuses and names it, leaving no directory behind.
3261
+ `init --with --bundle-release extension-bundles@v…` writes a private
3262
+ `<directory>/package.json` pinning the running core and an extension bundle
3263
+ lockfile naming the verified archives. It does not add extension npm
3264
+ dependencies. Before anything is written, the selected catalog checks every
3265
+ required extension and core compatibility; a missing requirement or incompatible
3266
+ bundle refuses and names it, leaving no directory behind.
3227
3267
 
3228
3268
  Nothing is installed. The generated site has no `node_modules` and no
3229
- `package-lock.json` until you run `npm install` in it yourself, which the
3230
- command and the generated README both state as the next step. Reproducibility
3231
- comes from that install, not from generation.
3269
+ `package-lock.json` until you run `npm install` in it yourself for core, which
3270
+ the command and generated README both state as the next step. Bundle
3271
+ reproducibility comes from the committed lockfile and frozen cache, not npm.
3232
3272
 
3233
3273
  - `--no-manifest` generates the site without a `package.json`, for a site whose
3234
3274
  dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
3235
3275
  still writes no manifest; add `--manifest` to pin the runtime for a
3236
3276
  route-only project too.
3237
- - `--pin <package>=<specifier>` records a specifier instead of the resolved
3238
- version, for local tarball or offline development
3239
- (`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
3240
- package installed from a local path or tarball is detected from npm's own
3241
- install record and pinned by that path without any flag; the README says so,
3242
- because such a pin only reproduces where that path exists.
3277
+ - `--pin <package>=<specifier>` is only for reviewed local source development.
3278
+ New first-party extension installs use the signed bundle release instead.
3243
3279
 
3244
3280
  There is no upgrade command. Moving a generated project to newer versions today
3245
3281
  means editing its `package.json` and re-running `npm install` yourself; nothing
@@ -3260,9 +3296,8 @@ extension artifact for tooling that understands its declared format. It is not
3260
3296
  a Node module and cannot activate an extension, run a hook, replace a trusted
3261
3297
  operator host, or grant a route any authority.
3262
3298
 
3263
- Install an artifact only from its immutable `extensions@v…` GitHub Release.
3264
- For example, after the first release is published, its inert store configuration
3265
- schema snapshot can be installed with:
3299
+ Install an artifact only from its immutable `extensions@v…` GitHub Release. The
3300
+ published inert store configuration schema snapshot can be installed with:
3266
3301
 
3267
3302
  ```sh
3268
3303
  urlcode extension-artifacts install store-schema --artifact-release extensions@v1.0.0 --project app
@@ -3310,6 +3345,71 @@ not the `@jimhoyd/urlcode-store` implementation. Installing it does not install
3310
3345
  or activate that package. Its README names the separate executable and operator
3311
3346
  requirements.
3312
3347
 
3348
+ ### Signed executable extension bundles
3349
+
3350
+ Official executable extensions are delivered through a separate, immutable
3351
+ `extension-bundles@v…` GitHub Release namespace;
3352
+ it is intentionally disjoint from the permanently data-only `extensions@v…`
3353
+ artifact channel above. A bundle is a bounded, frozen Node module tree produced
3354
+ from reviewed first-party source, not a general extension marketplace and not
3355
+ a project dependency resolver.
3356
+
3357
+ An operator explicitly installs one named bundle from an immutable release:
3358
+
3359
+ ```sh
3360
+ urlcode extension-bundles install store \
3361
+ --bundle-release extension-bundles@v1.0.0 --project app
3362
+ ```
3363
+
3364
+ For a new composed site, `init --with` can perform that verified installation
3365
+ before it writes the route project. This is the npm-free extension path: the
3366
+ generated `package.json`, when requested, pins URLCode core only; the generated
3367
+ host loads only the names recorded in the bundle lockfile.
3368
+
3369
+ ```sh
3370
+ urlcode init site --with ui,auth,admin \
3371
+ --bundle-release extension-bundles@v1.0.0
3372
+ ```
3373
+
3374
+ `init` verifies each requested bundle in a temporary operator staging root,
3375
+ obtains each scaffold from that verified module tree, then writes the cache and
3376
+ `urlcode.extension-bundles.lock.json` into the new site. It never resolves an
3377
+ extension package from npm in this mode. A failed verification or scaffold
3378
+ refusal leaves no site directory behind. The release tag is still an explicit
3379
+ operator choice; YAML cannot supply it.
3380
+
3381
+ The command verifies attestations for both the catalog and selected archive
3382
+ against the requested tag and dedicated workflow, rejects self-hosted runners,
3383
+ checks the catalog's commit, filename and SHA-256, and extracts only regular
3384
+ files in the signed module tree. It writes
3385
+ `urlcode.extension-bundles.lock.json` and keeps the frozen bytes under
3386
+ `app/.urlcode/extension-bundles/<sha256>/`. There is no automatic discovery,
3387
+ installation, update, or fallback to npm. `inspect` reads the committed lock;
3388
+ a modified cache or an incompatible core version refuses before import.
3389
+
3390
+ Executable bundles are **trusted operator code**, exactly like a hand-written
3391
+ operator host module. Project YAML cannot choose a bundle, name a release,
3392
+ trigger a download, or grant a bundle authority. An operator host explicitly
3393
+ loads a locked entry by name, then chooses which returned registration to pass
3394
+ to `createRuntime`:
3395
+
3396
+ ```js
3397
+ import { loadExtensionBundle } from '@jimhoyd/urlcode/extension-bundles';
3398
+
3399
+ const { storeExtension } = await loadExtensionBundle('/absolute/site/app', 'store');
3400
+ export default { extensions: [storeExtension({ directory: '/srv/site-data', projectSha256: process.env.PROJECT_SHA256 })] };
3401
+ ```
3402
+
3403
+ This does not make bundle code sandboxed and does not alter a route that
3404
+ declares `sandbox: true`; those remain distinct execution modes. The signed
3405
+ bundle path is the supported distribution for first-party executable
3406
+ extensions. The legacy `@jimhoyd/urlcode-ui`, `@jimhoyd/urlcode-auth`,
3407
+ `@jimhoyd/urlcode-admin`, and `@jimhoyd/urlcode-store` npm packages are
3408
+ deprecated migration artifacts: existing projects may retain their locked
3409
+ copies, but new projects must use a verified bundle release. Their npm
3410
+ retention status is not a promise that they are available or supported for new
3411
+ installs.
3412
+
3313
3413
  ---
3314
3414
 
3315
3415
  # Composing a site from ui, auth and admin
@@ -3320,10 +3420,16 @@ One command produces a site that already has accounts, an administration
3320
3420
  console and a presentation kit wired together:
3321
3421
 
3322
3422
  ```sh
3323
- npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
3324
- urlcode init site --with ui,auth,admin
3423
+ npm install @jimhoyd/urlcode
3424
+ npx urlcode init site --with ui,auth,admin --bundle-release extension-bundles@v…
3325
3425
  ```
3326
3426
 
3427
+ Use the immutable bundle release recorded in [package and channel
3428
+ alignment](https://github.com/jimhoyd-com/urlcode/blob/main/docs/VERSION-ALIGNMENT.md). This creates a site whose npm manifest pins
3429
+ core only; UI, auth and admin are verified, locked GitHub Release bundles.
3430
+ The legacy extension npm packages are deprecated migration artifacts. New sites
3431
+ obtain extensions from the verified bundle release.
3432
+
3327
3433
  This page is the map of what you may then change, and with which tool. It
3328
3434
  covers three different activities that are easy to confuse:
3329
3435
 
@@ -3501,9 +3607,10 @@ not run it. With the packages named:
3501
3607
  and those translations do not currently reach the console
3502
3608
  ([#227](https://github.com/jimhoyd-com/urlcode/issues/227)).
3503
3609
 
3504
- `urlcode init <directory> --with ui,auth,admin` writes these commands into the
3505
- generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
3506
- neither peer; the operator names them.
3610
+ `urlcode init <directory> --with ui,auth,admin --bundle-release
3611
+ extension-bundles@v…` writes these commands into the generated README with the
3612
+ verified bundle release pinned. The operator names the logical extensions; it
3613
+ does not add extension npm dependencies.
3507
3614
 
3508
3615
  Run the extension's published `fastChecks` while editing. Theme and copy changes
3509
3616
  need no framework build. Template and CSS checks load only the UI kit and named