@jimhoyd/urlcode 0.4.8 → 0.5.0

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/dist/types.js CHANGED
@@ -127,7 +127,7 @@
127
127
 
128
128
 
129
129
 
130
-
130
+
131
131
 
132
132
 
133
133
 
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 60,557 tokens (estimate: characters / 4). -->
3
3
 
4
4
  <!-- urlcode-current-version:start -->
5
5
  # URLCode
@@ -7,7 +7,7 @@
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.0`. `function`/`middleware`
11
11
  > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
12
12
  > auth, admin and ui extension packages in this repository are versioned at the same
13
13
  > revision; confirm what is published with `npm run release:status`.
@@ -18,9 +18,6 @@ or fetch inside a `sandbox: true` function, regex routes, database access,
18
18
  global middleware, YAML interpolation, or packages named in YAML. Secrets need external revision-pinned
19
19
  grants. Unsupported features fail with the route named; nothing degrades silently.
20
20
 
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
21
  ## Contents
25
22
 
26
23
  1. [The URLCode framework](#the-urlcode-framework)
@@ -209,11 +206,13 @@ executable extension path. See [signed declarative artifacts](https://github.com
209
206
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
210
207
  ```
211
208
 
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
209
+ `urlcode init <dir> --with ui,auth,admin` writes this layout in one step: by
210
+ default it resolves each installed `@jimhoyd/urlcode-<name>` from the current
211
+ directory; with `--bundle-release extension-bundles@v…` it verifies and locks
212
+ the named GitHub Release bundles instead. In either mode it calls the verified
213
+ module's `scaffold` export and merges fragments into `app/urlcode.yaml`, one
214
+ explicit `host.mjs` and one `README.md`, refusing before writing a site when a
215
+ package/bundle is missing or two fragments collide (the contract is documented under
217
216
  [scaffolding](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
218
217
  and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
219
218
  administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
@@ -508,12 +507,12 @@ this project's own redirects — cheaper than this table or the recipe catalog.
508
507
  |---|---|---|
509
508
  | 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
509
  | 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) |
510
+ | 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) |
511
+ | 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
512
  | 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
513
  | 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
514
  | 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) |
515
+ | 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
516
  | 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
517
  | 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
518
  | A cache strategy on any route | `policies.cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) |
@@ -537,8 +536,9 @@ Which handler serves the response:
537
536
  Data persistence has no native handler. The operator-installed `store` extension
538
537
  serves declared collections as a CRUD API, and `urlcode recipes search "crud store
539
538
  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
539
+ `@jimhoyd/urlcode-store` package or an attested executable bundle and a host
540
+ file. `init --with ui,auth,store --bundle-release extension-bundles@v…`
541
+ scaffolds the npm-free bundle form; a no-auth
542
542
  `--with store` needs `--ack store:public-write`, which only a core release after the
543
543
  store's first publication has, so say so rather than promising it. Report anything beyond that recipe (filtering, sorting, per-record
544
544
  ownership, a database) as a gap. `urlcode context` lists the same built-ins so
@@ -757,7 +757,7 @@ programmatic compatibility analysis and provider verification limits.
757
757
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md
758
758
 
759
759
  <!-- urlcode-current-version:start -->
760
- This guide targets URLCode 0.4.8. Start with the function example below,
760
+ This guide targets URLCode 0.5.0. Start with the function example below,
761
761
  then add only the fields your route needs. The authoritative machine-readable
762
762
  shape is [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json); semantic rules are in the
763
763
  [specification](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md). Unsupported fields fail validation.
@@ -1327,7 +1327,8 @@ Keys are absolute case-sensitive paths. Trailing slashes are significant.
1327
1327
  Parameters occupy whole segments, e.g. `/p/{id}`, with distinct identifier names.
1328
1328
  Each parameter matches exactly one nonempty segment, never across `/`; it is not
1329
1329
  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
1330
+ support a terminal `/*` wildcard with an otherwise literal path; a `redirect` alone supports a
1331
+ 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
1332
  percent encoding, spaces, backslashes or query strings. Path length is limited
1332
1333
  to 2,048 characters and 32 segments. `/_urlcode` is reserved.
1333
1334
 
@@ -1466,7 +1467,9 @@ Unknown query keys are ignored unless explicitly passed by a redirect.
1466
1467
  ### Redirects
1467
1468
 
1468
1469
  `redirect.url` is an absolute HTTP(S) URL with literal scheme/host and no embedded
1469
- credentials or whitespace/control characters. `{pathInput}` placeholders are
1470
+ credentials or whitespace/control characters, or a root-relative path (one leading `/`, never
1471
+ `//`, no dot segments) that answers a path-only `Location`. On a `/**` route `{**}` is the captured
1472
+ suffix, once, each segment encoded. `{pathInput}` placeholders are
1470
1473
  allowed only in the destination pathname and encoded as single components.
1471
1474
  No environment/secret interpolation. Status defaults to 302; allowed values are
1472
1475
  301, 302, 303, 307 and 308.
@@ -1709,6 +1712,7 @@ not part of the route key.
1709
1712
  | `/go` | `/go`, `/go?campaign=spring` | `/Go`, `/go/`, `/go/extra` |
1710
1713
  | `/r/{code}` | `/r/abc`, `/r/123` | `/r/`, `/r/abc/extra` |
1711
1714
  | `/r/{code}/details` | `/r/abc/details` | `/r/abc/other/details` |
1715
+ | `/legacy/**` with a `redirect` handler | `/legacy/a`, `/legacy/a/b/c` | `/legacy`, `/legacy/`, `/legacy/a//b` |
1712
1716
  | `/assets/*` with a `static` handler | Files under `/assets/`, including `/assets/css/site.css` | `/assets`, `/assets-other/site.css` |
1713
1717
 
1714
1718
  A `{parameter}` captures exactly one nonempty path segment. It is **not greedy**:
@@ -1721,8 +1725,16 @@ path. It covers the remaining nested file path; it is not a named capture or a
1721
1725
  regex operator. Matching a mount does not guarantee a response file exists:
1722
1726
  missing files return 404. It is not a catch-all for functions or redirects.
1723
1727
 
1728
+ A `redirect` handler alone supports a terminal `/**` after a literal prefix (never bare `/**`, never
1729
+ with a `{parameter}`). It matches one or more remaining segments, and `{**}` in `redirect.url` is
1730
+ those segments, each percent-encoded and joined by `/`, usable once and only in the destination
1731
+ path. Empty segments, `.`/`..`, encoded slashes and captures over 1,024 characters do not match.
1732
+ Exact and `{parameter}` routes always win over it, so `/legacy/keep/{id}` can carve an exception out
1733
+ of `/legacy/**`. It is refused on static hosting (S3 redirects match one path) and on Cloudflare
1734
+ until the Worker table supports suffix matching, and it cannot share a prefix with a `static` mount.
1735
+
1724
1736
  No regex routes, greedy parameters, optional segments, partial-segment parameters,
1725
- `**` globs, or regex constraints inside `{code}` are implemented. Characters such
1737
+ other `**` globs, or regex constraints inside `{code}` are implemented. Characters such
1726
1738
  as `.` and `+` have no regex meaning in a literal path. Do not paste a regex into
1727
1739
  a route key: some regex-looking text is legal literal text, while unsupported
1728
1740
  syntax may fail validation. Parameter-schema `pattern` is also unsupported.
@@ -3260,9 +3272,8 @@ extension artifact for tooling that understands its declared format. It is not
3260
3272
  a Node module and cannot activate an extension, run a hook, replace a trusted
3261
3273
  operator host, or grant a route any authority.
3262
3274
 
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:
3275
+ Install an artifact only from its immutable `extensions@v…` GitHub Release. The
3276
+ published inert store configuration schema snapshot can be installed with:
3266
3277
 
3267
3278
  ```sh
3268
3279
  urlcode extension-artifacts install store-schema --artifact-release extensions@v1.0.0 --project app
@@ -3310,6 +3321,67 @@ not the `@jimhoyd/urlcode-store` implementation. Installing it does not install
3310
3321
  or activate that package. Its README names the separate executable and operator
3311
3322
  requirements.
3312
3323
 
3324
+ ### Signed executable extension bundles
3325
+
3326
+ Official executable extensions are migrating away from consumer npm installs.
3327
+ They use a separate, immutable `extension-bundles@v…` GitHub Release namespace;
3328
+ it is intentionally disjoint from the permanently data-only `extensions@v…`
3329
+ artifact channel above. A bundle is a bounded, frozen Node module tree produced
3330
+ from reviewed first-party source, not a general extension marketplace and not
3331
+ a project dependency resolver.
3332
+
3333
+ An operator explicitly installs one named bundle from an immutable release:
3334
+
3335
+ ```sh
3336
+ urlcode extension-bundles install store \
3337
+ --bundle-release extension-bundles@v1.0.0 --project app
3338
+ ```
3339
+
3340
+ For a new composed site, `init --with` can perform that verified installation
3341
+ before it writes the route project. This is the npm-free extension path: the
3342
+ generated `package.json`, when requested, pins URLCode core only; the generated
3343
+ host loads only the names recorded in the bundle lockfile.
3344
+
3345
+ ```sh
3346
+ urlcode init site --with ui,auth,admin \
3347
+ --bundle-release extension-bundles@v1.0.0
3348
+ ```
3349
+
3350
+ `init` verifies each requested bundle in a temporary operator staging root,
3351
+ obtains each scaffold from that verified module tree, then writes the cache and
3352
+ `urlcode.extension-bundles.lock.json` into the new site. It never resolves an
3353
+ extension package from npm in this mode. A failed verification or scaffold
3354
+ refusal leaves no site directory behind. The release tag is still an explicit
3355
+ operator choice; YAML cannot supply it.
3356
+
3357
+ The command verifies attestations for both the catalog and selected archive
3358
+ against the requested tag and dedicated workflow, rejects self-hosted runners,
3359
+ checks the catalog's commit, filename and SHA-256, and extracts only regular
3360
+ files in the signed module tree. It writes
3361
+ `urlcode.extension-bundles.lock.json` and keeps the frozen bytes under
3362
+ `app/.urlcode/extension-bundles/<sha256>/`. There is no automatic discovery,
3363
+ installation, update, or fallback to npm. `inspect` reads the committed lock;
3364
+ a modified cache or an incompatible core version refuses before import.
3365
+
3366
+ Executable bundles are **trusted operator code**, exactly like a hand-written
3367
+ operator host module. Project YAML cannot choose a bundle, name a release,
3368
+ trigger a download, or grant a bundle authority. An operator host explicitly
3369
+ loads a locked entry by name, then chooses which returned registration to pass
3370
+ to `createRuntime`:
3371
+
3372
+ ```js
3373
+ import { loadExtensionBundle } from '@jimhoyd/urlcode/extension-bundles';
3374
+
3375
+ const { storeExtension } = await loadExtensionBundle('/absolute/site/app', 'store');
3376
+ export default { extensions: [storeExtension({ directory: '/srv/site-data', projectSha256: process.env.PROJECT_SHA256 })] };
3377
+ ```
3378
+
3379
+ This does not make bundle code sandboxed and does not alter a route that
3380
+ declares `sandbox: true`; those remain distinct execution modes. npm packages
3381
+ remain the migration fallback until the first signed bundle release and the
3382
+ fresh composed consumer flow have been released and proven. Do not unpublish a
3383
+ package merely because its data-only artifact exists.
3384
+
3313
3385
  ---
3314
3386
 
3315
3387
  # Composing a site from ui, auth and admin
package/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
  > A portable runtime for programmable URL behavior, and the framework that grows
5
5
  > from it: routes in YAML, functions and middleware, then accounts, administration and
6
6
  > stored links as operator-installed extensions. Stable project format
7
- > `version: "1"`. Core is Apache-2.0; this revision is `0.4.8`. `function`/`middleware`
7
+ > `version: "1"`. Core is Apache-2.0; this revision is `0.5.0`. `function`/`middleware`
8
8
  > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
9
9
  > auth, admin and ui extension packages in this repository are versioned at the same
10
10
  > revision; confirm what is published with `npm run release:status`.
@@ -15,132 +15,68 @@ or fetch inside a `sandbox: true` function, regex routes, database access,
15
15
  global middleware, YAML interpolation, or packages named in YAML. Secrets need external revision-pinned
16
16
  grants. Unsupported features fail with the route named; nothing degrades silently.
17
17
 
18
- Agents that explicitly want the complete consolidated reference in one fetch should read
19
- [llms-full.txt](llms-full.txt), generated from the documents below (about 50k tokens, estimated).
18
+ ## Start here (read only this section for a first attempt)
20
19
 
21
- ## Building only redirects? Start here
22
-
23
- Run `urlcode context --project DIR --task redirects` (MCP `get_context {"task":"redirects"}`)
24
- first: one bounded call returning every supported shape below with exact YAML, the
25
- gaps with their exact validation error, and this project's own redirects cheaper
26
- than reading this file or the recipe catalog. `urlcode recipes search redirect` and
27
- `urlcode recipes show redirect` give a runnable starting project.
20
+ 1. Run `urlcode context --project DIR` (MCP `get_context`). For redirect-only work run
21
+ `urlcode context --project DIR --task redirects`: supported shapes with exact YAML, gaps
22
+ with their exact validation error, a paste-ready `starter`, and this project's own redirects.
23
+ 2. For a new redirect-only project, `urlcode init DIR --template redirects` writes the tested starter (yaml, 404 page, `npm start` honoring `PORT`, fixtures). Otherwise write `urlcode.yaml`, then `urlcode validate --local --project DIR` and `urlcode test`.
24
+ 3. Do not read or grep [llms-full.txt](llms-full.txt) (about 60k tokens) for a routine task; it is the
25
+ deliberate fallback when the steps above do not answer the question.
28
26
 
29
27
  | Shape | Supported? | Use |
30
28
  |---|---|---|
31
- | Fixed redirect, any of 301/302/303/307/308 (302 default) | Yes | `redirect: {url, status}` — [redirects](docs/yaml/redirects.md) |
32
- | Parameterized path (`/users/{id}` to `/profiles/{id}`) | Yes | `{name}` placeholder in `redirect.url`, naming a declared path parameter |
33
- | Fixed-depth suffix (`/legacy/a/b` to `/modern/a/b`) | Yes, one route per depth | same as above with one placeholder per segment |
34
- | Query-string preservation | Yes, opt-in only | `redirect.query.pass` (explicit allowlist) / `query.map` |
35
- | Method-preserving redirect (keep POST body) | Yes | `methods` plus `status: 307` or `308` |
36
- | 404 for unmatched paths | Yes | `site.notFound` (a project-relative `.html` file) |
37
- | Wildcard/suffix redirect (`/legacy/*` to `/modern/*`, any depth) | No | terminal `/*` and `{rest...}` are refused on `redirect`; report the gap ([open decision](docs/OPEN-DECISIONS.md)) |
38
- | Host-based, scheme-based or relative-URL redirect | No | destination must be a literal absolute `https://host/path`; report the gap |
39
-
40
- ## Declarative-first default
41
-
42
- > Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
43
-
44
- Check the installed version's primitives, YAML configuration, policies, supported
45
- extensions and recipes/templates before writing a custom function or middleware.
46
- Keep necessary custom code focused and report the capability gap; never invent
47
- fields or bypass target limits or operator grants. See [the design principle](docs/PROJECT-DIRECTION.md#design-principle-declarative-first).
48
-
49
- ## Need X? Use this built-in first
50
-
51
- Route a task to the feature before writing code. Every row is off unless declared.
52
-
53
- | I need | Use | Where |
54
- |---|---|---|
55
- | Security headers (CSP, HSTS, nosniff, frame and referrer policy) | `policies.security` (`headers: oshp`, or `profile: hardened`) | [security](docs/policies/security.md), [policies](docs/POLICIES.md) |
56
- | Cache headers for a page, download or static folder | `cacheControl`, exactly one of `no-cache` (default), `no-store`, `public, max-age=3600`, `public, max-age=31536000, immutable` | [assets](docs/yaml/assets.md) |
57
- | Cache strategy or origin cache for any route | `policies.cache` | [cache](docs/policies/cache.md) |
58
- | Request body size, required body, content type, JSON syntax check | `request.body` (`maxBytes`, `required`, `contentTypes`, `format`); 413/415/400 | [HTTP](docs/HTTP.md) |
59
- | Restrict or allow HTTP methods | `methods` (default GET/HEAD); 405 with `Allow` | [HTTP](docs/HTTP.md) |
60
- | Rate limiting | `policies.throttle` | [throttle](docs/policies/throttle.md) |
61
- | Block bots or AI crawlers | `policies.agents`; `robots` via `site` | [agents](docs/policies/agents.md), [site](docs/SITE.md) |
62
- | Compression | `policies.compression` | [compression](docs/policies/compression.md) |
63
- | Fixed JSON, text or headers with no code | `respond` and `response.headers` | [HTTP](docs/HTTP.md) |
64
- | robots.txt, sitemap, favicon, security.txt | top-level `site` | [site](docs/SITE.md) |
65
- | Sign-in, roles, protected routes | `extensions.auth`, then `auth: true` on the route | [extensions](docs/EXTENSIONS.md) |
66
-
67
- Which handler serves the response:
68
-
69
- | The response is | Handler | Recipe |
70
- |---|---|---|
71
- | Fixed text or JSON | `respond` | `health-page` |
72
- | A short HTML snippet | `respond` `text` plus `response.headers` `Content-Type: text/html; charset=utf-8` | `docs/HTTP.md` |
73
- | One HTML file | `page` | `static-page` |
74
- | A directory of files | `static` | `static-plus-api` |
75
- | An attachment | `download` | `protected-download` |
76
-
77
- Persistence: core has no storage handler, but the operator-installed store extension serves declared collections as a CRUD API; `urlcode recipes search "crud store persist"` finds the `store-crud` recipe (operator installs the package; `init --with store` is not yet available from npm, #323). Gaps to report rather than invent: a database, filtering or per-record ownership beyond that recipe, CORS, and a native link store. `urlcode context`
78
- prints the same pointer for the project in front of you.
79
-
80
- After a real authoring attempt, draft feedback only for a capability gap,
81
- repeated workaround, documentation/discovery gap or suspected defect. Include
82
- the runtime/target, sanitized YAML, observed validation/test evidence, expected
83
- behavior and a proposed fixture. Search existing URLCode issues first. Never
84
- publish or comment on an issue without the user's explicit approval.
85
-
86
- ## Authoring
87
- - [AI authoring contract](docs/AI-AUTHORING.md): workflow, capability matrix, checks.
88
- - [Authoring skill](.claude/skills/urlcode-authoring/SKILL.md): loadable authoring skill shipped with this revision; see [distribution](docs/AI-AUTHORING.md#agent-skills).
89
- - [Operations skill](.claude/skills/urlcode-operations/SKILL.md): loadable deployment/verification/resilience skill shipped with this revision.
90
- - [YAML guide](docs/YAML-GUIDE.md): recipes for all handlers and common options.
91
- - [JSON Schema](schemas/urlcode.schema.json): accepted fields and types.
92
- - [Field reference](docs/YAML-REFERENCE.md): generated exhaustive field inventory.
93
- - [Semantics](docs/SPECIFICATION.md): validation, defaults and sandbox API.
94
- - [Runnable cookbook](examples/cookbook/README.md): 25 routes with HTTP fixtures.
95
- - [Route matching](docs/ROUTING.md): precedence, non-greedy parameters, updates.
96
- - [Middleware](docs/MIDDLEWARE.md): next(), state, ordering and native body limits.
97
- - [Policies](docs/POLICIES.md): optional host-enforced `policies`/`profiles` keys, all off by default: `throttle`, `agents`, `security`, `compression`, `cache`; merge rules and per-target support.
98
- - [Plugins](docs/PLUGINS.md): host hook API operators pass in code; never named in YAML.
99
- - [TypeScript](docs/TYPESCRIPT.md): the package ships declarations for every export (`urlcode`, `@jimhoyd/urlcode/plugins`, `@jimhoyd/urlcode/policies`, `@jimhoyd/urlcode/observability`, `@jimhoyd/urlcode/compliance`, `@jimhoyd/urlcode/prerender`, `@jimhoyd/urlcode/sandbox`, `@jimhoyd/urlcode/aws`, `@jimhoyd/urlcode/vercel`, `@jimhoyd/urlcode/cloudflare`); the runtime source is TypeScript, `dist/` is its stripped JavaScript.
100
- - [HTTP](docs/HTTP.md): methods, request bodies and response headers.
101
- - [Assets](docs/ASSETS.md): pages, MIME, downloads, cache and ranges.
102
- - [Site conventions](docs/SITE.md): optional top-level `site` key, all off by default: `robots`, `sitemap`, `favicon`, `securityTxt`, `llms` generate native routes; declared routes win; absolute URLs need `--origin`.
103
- - [Prerendering](docs/PRERENDER.md): `@jimhoyd/urlcode/prerender` build helper and recipe; render function/middleware routes once into native page routes, no request-time guest code.
104
- - [Organization](docs/ORGANIZATION.md): entry point and included files.
105
-
106
- ## Start here
107
- - [The framework](docs/FRAMEWORK.md): four packages, the ladder from redirects to a full app, the composition contract, the rules an agent must follow.
108
- - [AI authoring contract](docs/AI-AUTHORING.md): workflow, capability matrix, copyable task prompt, checks.
109
- - [JSON Schema](schemas/urlcode.schema.json) and [field reference](docs/YAML-REFERENCE.md): every accepted field.
110
- - [YAML guide](docs/YAML-GUIDE.md) and [runnable cookbook](examples/cookbook/README.md): recipes with HTTP fixtures.
111
- - [Semantics](docs/SPECIFICATION.md): validation, defaults, sandbox API.
112
-
113
- ## Routes and handlers
114
- - [Routing](docs/ROUTING.md): exact and `{param}` paths, precedence, `/*` only on static and extension mounts.
115
- - [HTTP](docs/HTTP.md): methods, validated inputs, bodies, response headers, cookies.
116
- - [Function security](docs/FUNCTION-SECURITY.md): trusted and unsandboxed by default, `sandbox: true` QuickJS/WASM opt-in, Request/Response subset, operator grants.
117
- - [Middleware](docs/MIDDLEWARE.md) and [examples](docs/MIDDLEWARE-EXAMPLES.md): `next()`, state, ordering; also `urlcode recipes add middleware`.
118
- - [Assets](docs/ASSETS.md): pages, static, downloads, MIME, ranges. [Prerender](docs/PRERENDER.md): render once, no request-time code.
119
- - [Conditions](docs/CONDITIONS.md): exact predicates, disjoint redirect/respond cases, no-store.
120
- - [Egress](docs/EGRESS.md): bounded HTTPS proxy and best-effort signals behind operator grants; self-hosted only.
121
- - [Policies](docs/POLICIES.md): `throttle`, `agents`, `security`, `compression`, `cache`; all off unless declared. [Site](docs/SITE.md): robots, sitemap, favicon, security.txt, llms.txt.
122
- - [Organization](docs/ORGANIZATION.md), [best practices](docs/BEST-PRACTICES.md), [scaffolding](docs/SCAFFOLDING.md).
123
- - [Interchange](docs/INTERCHANGE.md), [bulk import](docs/BULK.md), [recipes](docs/RECIPES.md) (`urlcode recipes search`, `examples search`), [TypeScript guests](docs/TYPESCRIPT-AUTHORING.md).
124
-
125
- ## Extensions (accounts, administration, presentation)
126
- - [Extensions](docs/EXTENSIONS.md): `extensions.<name>` blocks, mounts, policies, operator registration, the shared trusted hook primitive and its machine-readable hook contracts. UI/auth/admin project hooks run trusted in-process; contract v1 rejects `sandbox: true`.
127
- - [Signed declarative artifacts](docs/EXTENSIONS.md#signed-declarative-artifacts): project-pinned, attested JSON/Markdown data for offline tooling. MCP `get_extension_artifacts` verifies/inventories a committed lock and `get_extension_artifact` reads one bounded member; neither installs or activates an npm extension.
128
- - [Composing a site](docs/COMPOSING-A-SITE.md): what `urlcode init site --with ui,auth,admin` wires, which `--with` combinations are supported, the presentation override paths under `ui/`, and per-package lifecycle hook input, verdict, timing and failure semantics. Separates declarative configuration, project functions and the TypeScript a new extension needs.
129
- - [urlcode-auth](packages/auth): npm: @jimhoyd/urlcode-auth; accounts, sessions, MFA, roles, account page; its own llms.txt. Lives in this repository as a workspace package.
130
- - [urlcode-admin](packages/admin): npm: @jimhoyd/urlcode-admin; users, sessions, roles, audit, cases; its own llms.txt. Lives in this repository as a workspace package.
131
- - [urlcode-ui](packages/ui): npm: @jimhoyd/urlcode-ui; escaped templates, shadcn/ui partials, themes, translations; its own llms.txt. Lives in this repository as a workspace package, not in a separate repository.
132
- - urlcode-middleware (retired): per-route middleware briefly lived here as an extension-seam alternative; the package was unpublished at 0.1.0-alpha.2 and its repository deleted. This is not a gap — per-route middleware is native to core via the `middleware:` array (docs/MIDDLEWARE.md); use that.
133
- - urlcode-dynamic-link (retired): stored short links briefly lived here as a mount-based extension; the package was unpublished and its repository deleted. Core has no native `link` handler and no supported extension provides stored links — report them as a gap.
134
- - [Plugins](docs/PLUGINS.md): host hook API in operator code, never named in YAML.
135
-
136
- ## Tooling and API
137
- - [Tooling SDK and MCP](docs/TOOLING.md): `urlcode mcp`, read-only inspection, validation, conversion previews.
138
- - [TypeScript](docs/TYPESCRIPT.md): declarations for `@jimhoyd/urlcode` and its `/plugins`, `/policies`, `/observability`, `/compliance`, `/prerender`, `/extensions`, `/sandbox`, `/aws`, `/vercel`, `/cloudflare` entries.
139
- - [Capabilities](docs/CAPABILITIES.md): per-target support; `urlcode capabilities --target NAME`.
140
-
141
- ## Operations
142
- - [Operations](docs/OPERATIONS.md), [install](docs/INSTALL.md), [deployment checks](docs/DEPLOYMENT-CHECKS.md), [CI action](docs/CI.md).
143
- - [Readiness](docs/READINESS.md), [capacity](docs/CAPACITY.md), [resilience](docs/RESILIENCE.md), [observability](docs/OBSERVABILITY.md).
144
- - [Vercel](docs/VERCEL.md), [AWS](docs/AWS.md), [Cloudflare](docs/CLOUDFLARE.md): adapters with local conformance tests; no provider deployment verified yet.
145
- - [Static hosting](docs/STATIC.md): `urlcode build --target static` compiles redirects/pages/static/downloads to S3 + CloudFront objects and redirect metadata; no server, so function/middleware/extension/proxy/signals/conditions/parameters/bindings/policies are all refused; GitHub Pages is explicitly out of scope.
146
- - [Release readiness](docs/RELEASE-READINESS.md), [roadmap](ROADMAP.md): what is proven, what is planned.
29
+ | Fixed redirect, 301/302/303/307/308 (302 default) | Yes | `redirect: {url, status}` |
30
+ | Parameterized path (`/users/{id}` to `/profiles/{id}`) | Yes | `{name}` in `redirect.url`, naming a declared path parameter |
31
+ | Root-relative destination (`/users/{id}` to `/profiles/{id}`) | Yes | `redirect.url: /profiles/{id}`: one leading slash, path only |
32
+ | Wildcard suffix (`/legacy/**` to `/modern/{**}`, any depth) | Yes | terminal `/**` key with a literal prefix; `{**}` is the remaining segments (redirect only; not static or Cloudflare) |
33
+ | Query-string preservation | Yes, opt-in | `redirect.query.pass` / `query.map` |
34
+ | Method-preserving redirect | Yes | `methods` plus `status: 307` or `308` |
35
+ | 404 for unmatched paths | Yes | `site.notFound` (project-relative `.html`) |
36
+ | Host or scheme chosen from the request | No | destination is a literal absolute `https://` URL or a root-relative path; report the gap |
37
+
38
+ ## Declarative-first
39
+
40
+ Use the highest-level declarative feature before writing a function or middleware. Custom code
41
+ only when the framework cannot express the requirement; then report the gap. Never invent fields
42
+ or bypass target limits or operator grants ([design principle](docs/PROJECT-DIRECTION.md#design-principle-declarative-first)).
43
+
44
+ | I need | Use |
45
+ |---|---|
46
+ | Security headers | `policies.security` ([security](docs/policies/security.md)) |
47
+ | Cache headers / cache strategy | `cacheControl` / `policies.cache` ([assets](docs/yaml/assets.md), [cache](docs/policies/cache.md)) |
48
+ | Body size, content type, JSON check | `request.body` ([HTTP](docs/HTTP.md)) |
49
+ | Restrict HTTP methods | `methods` (default GET/HEAD; 405 with `Allow`) |
50
+ | Rate limiting, bot/AI-crawler blocking, compression | `policies.throttle`, `policies.agents`, `policies.compression` ([policies](docs/POLICIES.md)) |
51
+ | Fixed JSON/text/headers | `respond` and `response.headers` |
52
+ | robots.txt, sitemap, favicon, security.txt | top-level `site` ([site](docs/SITE.md)) |
53
+ | Sign-in, roles, protected routes | `extensions.auth`, then `auth: true` ([extensions](docs/EXTENSIONS.md)) |
54
+ | Persistence | operator-installed store extension, recipe `store-crud`; a database, filtering and per-record ownership beyond it are gaps |
55
+
56
+ When only code can do it, one function serves one or more routes:
57
+
58
+ ```yaml
59
+ /hello/{name}:
60
+ function: functions/hello.mjs # binds args.name; context.route.pattern is "/hello/{name}"
61
+ ```
62
+ ```js
63
+ export default (request, { args, route }) => Response.json({ hello: args.name });
64
+ ```
65
+
66
+ Handler for the response: fixed text or JSON is `respond`; one HTML file is `page`; a directory is
67
+ `static`; an attachment is `download`; a short HTML snippet is `respond` `text` with a
68
+ `Content-Type: text/html; charset=utf-8` header. Functions are trusted by default; add
69
+ `sandbox: true` to opt a route in ([function security](docs/FUNCTION-SECURITY.md)).
70
+
71
+ After a real attempt, draft feedback only for a capability gap, repeated workaround, documentation
72
+ gap or suspected defect, with sanitized YAML and validation evidence. Search existing issues first;
73
+ never publish or comment without the user's approval.
74
+
75
+ ## Reference (load one only when needed)
76
+
77
+ - Authoring: [AI authoring contract](docs/AI-AUTHORING.md), [YAML guide](docs/YAML-GUIDE.md), [field reference](docs/YAML-REFERENCE.md), [JSON Schema](schemas/urlcode.schema.json), [semantics](docs/SPECIFICATION.md), [cookbook](examples/cookbook/README.md), [recipes](docs/RECIPES.md).
78
+ - Skills: [authoring](.claude/skills/urlcode-authoring/SKILL.md), [operations](.claude/skills/urlcode-operations/SKILL.md).
79
+ - Routes and handlers: [routing](docs/ROUTING.md), [HTTP](docs/HTTP.md), [middleware](docs/MIDDLEWARE.md), [assets](docs/ASSETS.md), [conditions](docs/CONDITIONS.md), [egress](docs/EGRESS.md), [prerender](docs/PRERENDER.md), [site](docs/SITE.md), [organization](docs/ORGANIZATION.md).
80
+ - Extensions: [extensions](docs/EXTENSIONS.md) (including [signed declarative artifacts](docs/EXTENSIONS.md#signed-declarative-artifacts); MCP `get_extension_artifacts`), [composing a site](docs/COMPOSING-A-SITE.md), [urlcode-auth](packages/auth), [urlcode-admin](packages/admin), [urlcode-ui](packages/ui), each with its own llms.txt and in this repository as a workspace package. Per-route middleware is native core (`middleware:` array), not a retired extension; core has no native `link` handler and no supported extension provides stored links, so report them as a gap.
81
+ - Tooling: [tooling and MCP](docs/TOOLING.md), [TypeScript](docs/TYPESCRIPT.md), [capabilities](docs/CAPABILITIES.md), [plugins](docs/PLUGINS.md), [interchange](docs/INTERCHANGE.md), [bulk import](docs/BULK.md).
82
+ - Operations: [operations](docs/OPERATIONS.md), [install](docs/INSTALL.md), [deployment checks](docs/DEPLOYMENT-CHECKS.md), [readiness](docs/READINESS.md), [capacity](docs/CAPACITY.md), [resilience](docs/RESILIENCE.md), [observability](docs/OBSERVABILITY.md), [Vercel](docs/VERCEL.md), [AWS](docs/AWS.md), [Cloudflare](docs/CLOUDFLARE.md), [static hosting](docs/STATIC.md), [release readiness](docs/RELEASE-READINESS.md), [roadmap](ROADMAP.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jimhoyd/urlcode",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "Portable runtime for programmable URL behavior",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -62,6 +62,10 @@
62
62
  "types": "./dist/types/extensions.d.ts",
63
63
  "default": "./dist/extensions.js"
64
64
  },
65
+ "./extension-bundles": {
66
+ "types": "./dist/types/extension-bundles.d.ts",
67
+ "default": "./dist/extension-bundles.js"
68
+ },
65
69
  "./sandbox": {
66
70
  "types": "./dist/types/sandbox.d.ts",
67
71
  "default": "./dist/sandbox.js"
@@ -69,7 +73,7 @@
69
73
  "./package.json": "./package.json"
70
74
  },
71
75
  "files": [
72
- ".claude",
76
+ ".claude/skills",
73
77
  "dist",
74
78
  "schemas",
75
79
  "data",
@@ -110,6 +114,7 @@
110
114
  "docs:llms": "node scripts/build-llms-full.ts",
111
115
  "docs:cookbook-index": "node scripts/build-cookbook-index.ts",
112
116
  "artifacts:prepare": "node --disable-warning=ExperimentalWarning scripts/prepare-extension-artifacts.ts",
117
+ "bundles:prepare": "node --disable-warning=ExperimentalWarning scripts/prepare-extension-bundles.ts",
113
118
  "check:downstream-skills": "node scripts/check-downstream-skill-drift.ts",
114
119
  "sync:agents": "node scripts/sync-agent-lists.ts",
115
120
  "check:docs": "node scripts/check-trust-model-prose.ts && node scripts/check-version-statements.ts && node scripts/check-local-links.ts && node scripts/check-guidance-claims.ts && node scripts/generate-yaml-reference.ts --check && node scripts/build-llms-full.ts --check && node scripts/build-cookbook-index.ts --check && node scripts/generate-claude-plugin.ts --check",
@@ -14,8 +14,8 @@ Run `urlcode validate --local --project .` and `urlcode serve --project .`.
14
14
  default). Add top-level `site: {notFound: 404.html}` for a custom 404 page
15
15
  instead of the plain default.
16
16
 
17
- For the shapes this recipe does not cover — a wildcard/suffix redirect that
18
- matches any depth (`/legacy/*`), or a host/scheme-based redirect — run
17
+ For the shapes this recipe does not cover — a root-relative or `/**` suffix
18
+ redirect, or a host/scheme-based redirect — run
19
19
  `urlcode context --project . --task redirects` (MCP `get_context
20
20
  {"task":"redirects"}`) for the exact supported alternative and the exact
21
21
  validation error, or see [docs/OPEN-DECISIONS.md](../../docs/OPEN-DECISIONS.md).
@@ -15,7 +15,7 @@ static serving and authentication. Read this file before changing anything.
15
15
  (limits; `--target NAME` before promising a provider), `get_schema`,
16
16
  `recipes search TEXT`/`search_recipes` then `recipes add NAME --out DIR`,
17
17
  `explain` and, with an operator host file, `get_extensions`. Bare
18
- `capabilities` and `recipes list` are complete catalogs: fallback, not step one.
18
+ `capabilities` and `recipes list` are complete catalogs: fallback, not step one. Do not read or grep `llms-full.txt` or the packaged docs for a routine task.
19
19
  4. Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement. Check supported extensions and recipes first; explain any capability gap.
20
20
 
21
21
  ## MCP
@@ -49,7 +49,7 @@ skill only in a React frontend with `components.json`; start with `shadcn info
49
49
  A `function`/`middleware` is trusted, in-process Node with only declared
50
50
  `args`/`env`/`secrets`. Add `sandbox: true` for code needing isolation, not
51
51
  merely untrusted input. The sandbox is text/JSON-only; use `proxy`/a binding and
52
- record the reason in `sandboxReason`.
52
+ record the reason in `sandboxReason`. Try `redirect` (relative or `/**`) or `respond` first; a function gets `context.route.pattern`.
53
53
 
54
54
  ## Checks that count as evidence
55
55