@jimhoyd/urlcode 0.4.0-alpha.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +20 -15
  3. package/ROADMAP.md +16 -11
  4. package/dist/BUILD-MANIFEST.json +16 -15
  5. package/dist/authoring.js +15 -1
  6. package/dist/capability-query.js +0 -1
  7. package/dist/catalog.js +0 -1
  8. package/dist/cli.js +23 -7
  9. package/dist/config.js +1 -1
  10. package/dist/explain.js +1 -1
  11. package/dist/http-response.js +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/init-with.js +36 -11
  14. package/dist/manifest.js +1 -1
  15. package/dist/mcp-authoring.js +2 -2
  16. package/dist/mcp.js +1 -1
  17. package/dist/policies/cache.js +2 -2
  18. package/dist/project-dependencies.js +305 -0
  19. package/dist/runtime.js +1 -1
  20. package/dist/trusted-functions.js +4 -5
  21. package/dist/types/authoring.d.ts +9 -1
  22. package/dist/types/capability-query.d.ts +0 -1
  23. package/dist/types/catalog.d.ts +0 -4
  24. package/dist/types/config.d.ts +1 -9
  25. package/dist/types/explain.d.ts +0 -1
  26. package/dist/types/http-response.d.ts +0 -1
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/init-with.d.ts +7 -13
  29. package/dist/types/manifest.d.ts +0 -1
  30. package/dist/types/project-dependencies.d.ts +78 -0
  31. package/dist/types/trusted-functions.d.ts +1 -4
  32. package/docs/AI-AUTHORING.md +5 -1
  33. package/docs/AWS.md +9 -0
  34. package/docs/CI-FOLLOWUP-2026-09-19.md +1 -1
  35. package/docs/CODEBASE-AUDIT-2026-09-20.md +6 -0
  36. package/docs/COMPOSING-A-SITE.md +278 -0
  37. package/docs/DEVELOPMENT-PIPELINE.md +208 -119
  38. package/docs/EXTENSIONS.md +36 -6
  39. package/docs/FRAMEWORK.md +45 -30
  40. package/docs/INSTALL.md +13 -8
  41. package/docs/MIDDLEWARE.md +10 -4
  42. package/docs/OPEN-DECISIONS.md +46 -6
  43. package/docs/READINESS.md +4 -3
  44. package/docs/README.md +3 -4
  45. package/docs/RELEASE-0.4.1.md +73 -0
  46. package/docs/RELEASE-SECURITY.md +27 -12
  47. package/docs/SPECIFICATION.md +5 -1
  48. package/docs/SPIKE-CORE-LAYERING.md +1 -1
  49. package/docs/STARTERS.md +17 -5
  50. package/docs/TOOLING.md +6 -4
  51. package/docs/VERCEL.md +10 -2
  52. package/docs/VERSION-ALIGNMENT.md +42 -8
  53. package/docs/archive/2026-09-19/ROADMAP.md +1 -0
  54. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +1 -0
  55. package/docs/{SPIKE-LAMBDA-COMPILE.md → archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md} +168 -12
  56. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +2 -0
  57. package/docs/archive/README.md +1 -0
  58. package/docs/yaml/functions.md +10 -2
  59. package/docs/yaml/middleware.md +5 -3
  60. package/examples/cookbook/middleware/envelope.mjs +4 -2
  61. package/llms-full.txt +387 -44
  62. package/llms.txt +1 -0
  63. package/package.json +8 -5
  64. package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
  65. package/recipes/middleware/middleware/envelope.mjs +4 -2
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude plugins published from the URLCode repository.",
9
- "version": "0.4.0-alpha.3"
9
+ "version": "0.4.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
package/README.md CHANGED
@@ -32,8 +32,10 @@ compiles it once, and refuses anything a target cannot enforce with the route
32
32
  named. Functions and middleware run trusted, in-process, with full Node
33
33
  access by default; a route opts into an isolated QuickJS/WebAssembly sandbox
34
34
  with a fresh heap per call and no Node, filesystem or network by declaring
35
- `sandbox: true`. Either way, secrets reach them only through operator grants
36
- pinned to the project revision.
35
+ `sandbox: true`. Either way, the `env`/`secrets` the runtime *hands* a route
36
+ come only from operator grants pinned to the project revision; grants govern
37
+ that injected context, not the ambient Node environment a trusted, in-process
38
+ module can reach on its own like any other code in the host.
37
39
 
38
40
  URLCode is not a URL shortener: stored short links are an operator-installed
39
41
  extension, not core's job. It is not a
@@ -53,16 +55,17 @@ an AI agent must follow are in [the framework](docs/FRAMEWORK.md).
53
55
 
54
56
  | Package | Adds | Status |
55
57
  |---|---|---|
56
- | [urlcode](https://github.com/jimhoyd-com/urlcode) (this repository) | Runtime, CLI, policies, provider adapters, extension contract | `0.4.0-alpha.2` (alpha) on top of the `0.3.0` release, Apache-2.0 |
57
- | [urlcode-ui](packages/ui) (in this repository) | Shared presentation: escaped templates, shadcn/ui partials, themes, translations | `0.1.0-alpha.5` on npm, alpha: review pending |
58
- | [urlcode-auth](packages/auth) (in this repository) | Accounts: password, passkeys, OIDC, email codes, TOTP, sessions, roles, account page | `0.1.0-alpha.3` on npm, alpha: review pending |
59
- | [urlcode-admin](packages/admin) (in this repository) | Administration: users, sessions, roles, audit, approvals, cases, impersonation | `0.1.0-alpha.3` on npm, alpha: review pending |
60
-
61
- Every version in this table is the one published to npm under the `alpha`
62
- dist-tag, and they are released together in the order core → ui → auth →
63
- admin. The authoritative cross-repository register, including each
64
- package's declared peer ranges and the rules that keep them consistent, is
65
- [docs/VERSION-ALIGNMENT.md](docs/VERSION-ALIGNMENT.md).
58
+ | [urlcode](https://github.com/jimhoyd-com/urlcode) (this repository) | Runtime, CLI, policies, provider adapters, extension contract | `0.4.1` stable release target, Apache-2.0 |
59
+ | [urlcode-ui](packages/ui) (in this repository) | Shared presentation: escaped templates, shadcn/ui partials, themes, translations | `0.4.1` stable release target; review pending |
60
+ | [urlcode-auth](packages/auth) (in this repository) | Accounts: password, passkeys, OIDC, email codes, TOTP, sessions, roles, account page | `0.4.1` stable release target; review pending |
61
+ | [urlcode-admin](packages/admin) (in this repository) | Administration: users, sessions, roles, audit, approvals, cases, impersonation | `0.4.1` stable release target; review pending |
62
+
63
+ This release aligns all four packages at `0.4.1`, with publication to npm's
64
+ `latest` channel in the order core → ui → auth → admin. The table describes
65
+ the release target; confirm publication with `npm run release:status` before
66
+ installing it. Matching numbers do not establish a permanent fixed-version
67
+ policy. Peer ranges, channel rules and generated application pins are documented
68
+ in [package and channel alignment](docs/VERSION-ALIGNMENT.md).
66
69
 
67
70
  `urlcode-dynamic-link` and `urlcode-short` were published once as
68
71
  `0.1.0-alpha.1` and have since been retired: both were unpublished from npm and
@@ -103,8 +106,9 @@ it. Cross-repository acceptance is tracked in
103
106
 
104
107
  ## Status
105
108
 
106
- `0.4.0-alpha.2` is the current release of the extension contract and the
107
- agent tooling, on top of the `0.3.0` self-hosted release. `0.4.0-alpha.1`
109
+ The `0.4.1` release line brings core, UI, auth and admin to matching stable
110
+ versions. A stable version selects the npm `latest` channel; it does not close
111
+ the review and deployment evidence gaps below. `0.4.0-alpha.1`
108
112
  added the extension contract, capabilities and provider conformance, strict
109
113
  redirect interchange, bulk import, recipes and search, TypeScript guest
110
114
  authoring, conditions, bounded proxy and signals, and the MCP read and
@@ -174,7 +178,8 @@ SQLite build requirement, unverified from this repository.
174
178
  ## Try it
175
179
 
176
180
  ```sh
177
- npm install --global @jimhoyd/urlcode # or: brew tap jimhoyd-com/urlcode && brew install urlcode
181
+ # After the 0.4.1 release has been published:
182
+ npm install --global @jimhoyd/urlcode@0.4.1
178
183
  urlcode init my-urls && cd my-urls
179
184
  urlcode dev
180
185
  ```
package/ROADMAP.md CHANGED
@@ -12,7 +12,9 @@ conditions, proxy/signals, trusted Node functions and middleware, and opt-in
12
12
  `sandbox: true` isolation. Target support differs: use `urlcode capabilities`
13
13
  before promising a deployment. Stored short links have no supported package.
14
14
 
15
- Auth, admin, UI and middleware are separate optional packages. Auth/admin already
15
+ Auth, admin and UI are optional packages, developed in this repository under
16
+ `packages/` and released independently. Middleware is not a package: per-route
17
+ `middleware:` is native to core. Auth/admin already
16
18
  render through the shared UI kit when configured. Core includes scaffolding,
17
19
  searchable recipes/examples, compact context, schema queries, a semantic manifest,
18
20
  and MCP inspection with separately enabled authoring. These are implemented,
@@ -20,8 +22,13 @@ not future phases. See [the framework](docs/FRAMEWORK.md).
20
22
 
21
23
  ## Next work
22
24
 
23
- Monorepo work is starting now. Move middleware in as a separate package before
24
- folding it into core; coordinate the guidance cleanup with that migration.
25
+ Auth, admin and UI are workspace packages here, and all three have been released
26
+ from this repository; that migration is
27
+ [done](docs/OPEN-DECISIONS.md#done-the-monorepo-migration-is-complete) and its
28
+ plan is [archived](docs/archive/2026-09-19/SPIKE-MONOREPO.md). The separate
29
+ middleware package was withdrawn rather than migrated —
30
+ `@jimhoyd/urlcode-middleware` is unpublished and its repository deleted — so
31
+ there is nothing to move in and nothing to fold into core afterward.
25
32
 
26
33
  1. **Make the existing product coherent.** Keep docs, examples, generated LLM
27
34
  resources, installed skills and the standalone template consistent with their
@@ -42,14 +49,12 @@ folding it into core; coordinate the guidance cleanup with that migration.
42
49
  [Issue 58](https://github.com/jimhoyd-com/urlcode/issues/58) and
43
50
  [release readiness](docs/RELEASE-READINESS.md) retain those gates. Live
44
51
  Google/Apple/SES checks remain explicitly deferred.
45
- 4. **After the monorepo move, consolidate middleware into core.** Move the
46
- existing middleware package first, preserving behavior.
47
- [issue 172](https://github.com/jimhoyd-com/urlcode/issues/172) tracks the later consolidation
48
- and consumer migration; package retirement is not part of the initial move. Static targets continue rejecting
49
- request-time middleware.
50
- 5. **Choose expansion from evidence.** Collections, a business application suite,
51
- and per-route Lambda compilation are proposals,
52
- not available features. Decide scope before implementing them.
52
+ 4. **Choose expansion from evidence.** Collections and a business application
53
+ suite are proposals, not available features. Decide scope before implementing
54
+ them. Per-route Lambda compilation is **decided against**: projects using
55
+ `function` or `middleware` deploy as one trusted Node process, and serverless
56
+ targets keep refusing those routes at activation
57
+ ([the decision](docs/OPEN-DECISIONS.md#accepted-one-node-deployment-per-project)).
53
58
 
54
59
  ## Records and ownership
55
60
 
@@ -6,15 +6,15 @@
6
6
  "dist/agents-guide.js": "0b19909aee3f8a1039c34c43ff58116ec1bc3f1875e3dbc34b3b01655273201c",
7
7
  "dist/assets.js": "0833b093d2fe457ce68281110dc07d47914992753607741c13b339d92c76cf5c",
8
8
  "dist/authoring-files.js": "ad32814f9ae9c549c6247396f982aa700d13a293cc0a589609eccc68f8965550",
9
- "dist/authoring.js": "ec5240e8686c124f4601229b45f89ce112d9eeb9d35e3059fe55e93056c45209",
9
+ "dist/authoring.js": "5fe1e871ff216ef1f7d2f490f74397b81b1de4edfe31cdb6115907296aa1c652",
10
10
  "dist/aws.js": "7d8e3a97b4f6dbefbd68da058f05192469d1efcaed5458b284863ebe462e060e",
11
11
  "dist/build-cloudflare.js": "af4f49ad45287d7c4105bb84b1592b5b8e76c930f7159505338298f7c5fd5651",
12
12
  "dist/build-static.js": "fe01ef4fcc66d83d8be7a787538b55974e84fd3d7c89e26f0a2f1a152762d565",
13
13
  "dist/bulk.js": "aac88d422bd9a7a341a421fb4e29ff5580b28d39250b7f1f5315ba20ae92da8a",
14
14
  "dist/capabilities.js": "28dba05b3851b8b12b3c1b809af87b732f84d601c737afe9225abe429886e691",
15
- "dist/capability-query.js": "ba148b27c6a60f8c895f13f26468b16c18ae0a5cddea26b602965c8efbba90ae",
16
- "dist/catalog.js": "ab1c01c9296cc277d2bd4fd682aeb053656a7d9f91c61974f4b78feea0211de8",
17
- "dist/cli.js": "ff3e583bd43044bdc06122e33c6137f400f677967ebff1ab49900b517682531c",
15
+ "dist/capability-query.js": "9f68bc94901451d7b9ecdd3aee8c3376dca7f0b890e31d8205306cf8120dea86",
16
+ "dist/catalog.js": "c378ef9bfd63b940790f6975f7db54557c2459722b65a7c170b97df24184bc52",
17
+ "dist/cli.js": "45b8adaae66a46e7b05067f1ced488b9dd7ccff8d15ba71e858459d76f5ddbb8",
18
18
  "dist/client-address.js": "9d0d01466aab23124330605c5a0e0f981f87c897858acfad909f546a63df165b",
19
19
  "dist/cloudflare.js": "17b43b0a6b6a0ef8a893415c989e4ae3976d97cc966adbf897b785f9adc472e3",
20
20
  "dist/compliance-rules/baseline.js": "6296dea6bcb9f9bb80ed6ed5ab2f456e7f74e6de2be5d2d98ca8c983fb4df3ab",
@@ -24,14 +24,14 @@
24
24
  "dist/compliance.js": "29a18da3cefea94802ac7d817d1ab4e3674abd5c3ac4ef893cb8b01c6a23fb61",
25
25
  "dist/conditions.js": "ff25e97da550a3bdd80af55522045669438aa31a77d2bc83ad602bcefdee0475",
26
26
  "dist/config-worker.js": "bc2170d60c35f8d5de227cbfc4c5067dc61d7cbc5b98aed88e68a11797072da5",
27
- "dist/config.js": "0cd82b4c407848f9c66e804a6dd4bf9a8a8503264e51584456578073e0f01921",
27
+ "dist/config.js": "8a0fde242b4658c522031a15d89daed25f8b77761f5c4204f295d5e311ac8ff7",
28
28
  "dist/context.js": "549a53e4738b9c20a1ec55614027f758026fb5860bf4ac962ce22d7029e48df0",
29
29
  "dist/ecosystem-cli.js": "1e71bacd53d6fa8c7857cfcce3c3012ce054771a434b78d1f39e892e32642edd",
30
30
  "dist/egress.js": "2ae29fe2cb4590fd2f715abe6817db47fed2f50dd31f37946ecf42eafbb0dd9e",
31
31
  "dist/errors.js": "a908e66496afebbdd632a8c159a6e14d23f8f38798a0b4342ca122f2511c6cde",
32
32
  "dist/examples.js": "89ae2b764a688fe52acfb9b94d3d262d7742401ebd0ef10c9ebfc76edfa5631c",
33
33
  "dist/explain-cli.js": "f0f8f3b5046430d03c6abb629b38651c9c734b03456040735ce9d7e582b2bcd5",
34
- "dist/explain.js": "dff1ed9a1bd818906ea867f4fb6d4601397943f9bdc4fceccf1809a365f95168",
34
+ "dist/explain.js": "7fb632036cc68280701dd512c74d27d6e0f97b4848e9024b284d7c5393b35eb5",
35
35
  "dist/extensions.js": "0506ec6289fa0582399d8c6d27727ffeb629bd442264248c921c63cc5b46ff01",
36
36
  "dist/function-sources.js": "16fad4abc81c7ee07b6cbcef2d23a9fc50e97a17cb4dde1db53469bbb555c96f",
37
37
  "dist/function-worker.js": "35771790ada4e1b36d447d4967a5e6cf0b543f32944e4c904f5fbf8998e824a5",
@@ -39,27 +39,28 @@
39
39
  "dist/guest-api.js": "49aec9ed72733ad119e63bcfcee18158dcc8ceb951e1823b8947f6407ff24afc",
40
40
  "dist/header-validation.js": "465181dbb08ff05f52defdd29fda025c0c64589bf319fa87fa6d3ab4b68216d5",
41
41
  "dist/http-policy.js": "4cb187642269078ddf3460a3f1813dc57878a616a6f8132ef8f8d3b4b91ebb56",
42
- "dist/http-response.js": "5fc3bb9eb932a3fd2fbc72c361416a5f1439b8bf45b2be37baca1c6446abf40c",
43
- "dist/index.js": "7e9f6ec8a125e2dfd4230b2ecd17ac513c0153d778a57bbcee7a1a9cd9681f81",
44
- "dist/init-with.js": "8371f568867ebdc6ab8e9aa762234ae2aa015865f46aa901254528e20d69b791",
42
+ "dist/http-response.js": "62f5da2e08bfe7ecff07464a49aaf910aa55770308a59118be9372390c2df9a1",
43
+ "dist/index.js": "8d7532ae0a31f9439ebb62e7b01a2748aee00f350262ff55d1226121d206c1ed",
44
+ "dist/init-with.js": "bf43b3aa4b421e9a31c5227e6afdf4956aba70d836ffddde96176a772d79e09b",
45
45
  "dist/interchange-cli.js": "35bd70ba8077af5c3e39404ff0e5d28707632090a59a90645141252449fd3f04",
46
46
  "dist/interchange.js": "26789420af33344d9a08c00fe6b2708aa71994224df527b3075295be2151611e",
47
47
  "dist/logging.js": "8ec57fea26983aa071b05fe15d680d6550b03488101163ed9666da674118cf31",
48
- "dist/manifest.js": "aa450ff88d44b184ed1ad3331d1f81005a844a9f971e9e12dfa039952839bc75",
48
+ "dist/manifest.js": "83eed0d59621039ef364cf6e7b54c5bd47e0d8cf0a5abe7b07344bfc1db56ed8",
49
49
  "dist/match.js": "53ebcc2cda529a8d07fb83f8cb69a1446036ffc1641af3c4bd45a55851bded28",
50
- "dist/mcp-authoring.js": "4987b0ba08999309e07aea1b8bb31f4600cec22d79998e6982e56f765070587a",
51
- "dist/mcp.js": "fa44f5f9e0bc4ca9790d1d1eb461147673c1539cf1704a4daae9bad30e4120eb",
50
+ "dist/mcp-authoring.js": "1183a56c5decb99ecb67a5da7bd91d1b494bc17a2965051aa63c4a7e291462b5",
51
+ "dist/mcp.js": "484c03cdf81f94116ec973feb70754298f8b1d5b3b42561969f8cce26b3c97c2",
52
52
  "dist/observability.js": "f4b1ab496f051fe3ef2ed36b2b2e469c63a8940700e2df852e6ff2d9cccdd5ae",
53
53
  "dist/operator-host.js": "e3dac9d43a83beb775202b4ab9eeeba7ac63cebfc5dcc669be38d825be587bd0",
54
54
  "dist/plugins.js": "a61aa933cd9e1b41020b86fae78aa3a7f45f5313124aa11b50cb57898722385e",
55
55
  "dist/policies/agents.js": "c1dbc1e90339f472eeeb2372f7f4637f59a5d210797ea680ac8120a02aa20c10",
56
- "dist/policies/cache.js": "03f79b91694bb78a98de4473384e5110e1ae1c3c83d46f929495d03af04d9da1",
56
+ "dist/policies/cache.js": "d08184e50b0ecb7331b29e4145aaefacb23b64ddaf3555d579743762f90c1629",
57
57
  "dist/policies/compression.js": "800f3ae3b1a4c1fe58c3d9908364ea9d78f655ff50c7f7589f584378e7f194b0",
58
58
  "dist/policies/security.js": "2b3bfdf82140156fea4b162adc28ee43ccd85c402fb200680dbb8d8706417420",
59
59
  "dist/policies/throttle.js": "bbb0cb1c24cf70d0a1ac32fc0618efb6c44e5b5d42564842f9c585ff7e501603",
60
60
  "dist/policies.js": "3750a7032ec7326aa544f4808683ad9eb91d4f2c5afe79904420cd8a355710a9",
61
61
  "dist/policy.js": "33c527008fafe4af18243ba668d069517c20659150f9f1d1a5a1d076e8724f89",
62
62
  "dist/prerender.js": "6210999e848ab86c87f265943960ef63716896da7a40e248221a6a30425c56b8",
63
+ "dist/project-dependencies.js": "b43a8f3125f62d38f0e71237b7813a785ef0613a83fcdec3fa8314f33758a534",
63
64
  "dist/project-tests.js": "445eaa139d2f1a2534369cfdeef081ef518de03218a467e7c5c2059caf840270",
64
65
  "dist/provider-verification.js": "ffcf9f05e8cb5f71ea46a97e279e8a613fdfdf9435ab02bd57af11e2680a9107",
65
66
  "dist/proxy.js": "34107e59ac0dbbea090efa3cf5badf3226c5302ba25e8f8ff9b2b5596ed80e50",
@@ -67,7 +68,7 @@
67
68
  "dist/recipes.js": "11853f23e63c3c65c5bed03a278cde0349d28c9fa23266c9907a2df318bb5fc3",
68
69
  "dist/route-diff.js": "8d71d50d332ffaaed588656bd32aa36763260d1ad87bb62d7ea1c8fb23383969",
69
70
  "dist/router.js": "cf831025d5ed6778d1d6a0e541177799794e76766c82084ded81d3e87317ca1e",
70
- "dist/runtime.js": "0470dcab0774d2da3336e9ce76a4853757bf96081e52bd741f6b44c1bdd4161f",
71
+ "dist/runtime.js": "2346d4630d178ee52caa7a94d4289f9b16f4b8ee467070b39fdd05742ed4513f",
71
72
  "dist/sandbox.js": "c88c5aa5dd95b4f55f1ba62da7969ad66dbd97c30f72e5b5f2817a2098ac1c65",
72
73
  "dist/scaffold.js": "3ef243aceca7ddf2780aa2c8c04ae17edd07682d3a418fd5d65ba00f5c762f14",
73
74
  "dist/schema-query.js": "bfd1844acd8d54ac361115191fcfbaf828a67e223b9ec6e9c22f158761af149b",
@@ -75,7 +76,7 @@
75
76
  "dist/signals.js": "b55e54efc8fb6703e08f2a1e808ec8e011bda67738db8ceadf1b248f695795be",
76
77
  "dist/site.js": "c40a06a6f9e2d6d3bea94727783c115fed4309bb0190787f58434eed64cbde84",
77
78
  "dist/tooling.js": "7409db98422325cd4181d073d645ea57debca0e54cb151123474b85f4d4ed7dc",
78
- "dist/trusted-functions.js": "befd256848be786f97e372c6af0e05ac08a0c93baed38bf2470ea49bc00dd3e4",
79
+ "dist/trusted-functions.js": "f3800c75f45ce90faf85ba6664398ad9031c5a9ac51d5ce42bdcff08a497d48e",
79
80
  "dist/types.js": "2fe7664dbb5695ec72114197abde72de4031df9c3b211740eacefa68e9b21844",
80
81
  "dist/typescript-authoring.js": "2e98498a15bdbb76f36fbee84afb7c42ca95bbf80a2e6dd4040147f3b1642a29",
81
82
  "dist/vercel.js": "a0da20c095d0c7fa8f579ed4dd5951e7a34191f8a72a57c5e782c9eab39f6f08",
package/dist/authoring.js CHANGED
@@ -8,9 +8,18 @@ import { renderAgentsGuide, renderMcpConfig, mcpConfigFile } from './agents-guid
8
8
  import { compileRoutes } from './router.js';
9
9
  import { prepareFunctionSnapshot, requestedPermissions } from './policy.js';
10
10
  import { assert } from './errors.js';
11
+ import { renderPackageManifest } from './project-dependencies.js';
12
+
11
13
 
12
14
 
13
- export async function initProject(destination ) {
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+ export async function initProject(destination , { manifest } = {}) {
14
23
  const target = resolve(destination);
15
24
  await mkdir(dirname(target), { recursive: true });
16
25
  // Reserve destination before copying; never merge into existing user files.
@@ -30,6 +39,11 @@ export async function initProject(destination ) {
30
39
  // .mcp.json registers the read-only server for repository-aware agents; the starter carries the same bytes.
31
40
  const mcp = await open(join(target,mcpConfigFile), 'wx', 0o644);
32
41
  try { await mcp.writeFile(renderMcpConfig('.')); } finally { await mcp.close(); }
42
+ if (manifest) {
43
+ // Exclusive create: the starter ships no package.json, so this never merges into or overwrites one.
44
+ const pkg = await open(join(target,'package.json'), 'wx', 0o644);
45
+ try { await pkg.writeFile(renderPackageManifest(target, manifest)); } finally { await pkg.close(); }
46
+ }
33
47
  } catch (error) { await rm(target, { recursive: true, force: true }); throw error; }
34
48
  return target;
35
49
  }
@@ -44,7 +44,6 @@ function usage(base ,files ,name )
44
44
  }
45
45
  return result;
46
46
  }
47
- export function capabilityNameList() {return capabilityNames;}
48
47
  /** One catalog entry with its schema fragments and bundled usage. No project, credentials or network are read. */
49
48
  export function getCapability(name ) {
50
49
  if(typeof name!=='string'||!(capabilityNames ).includes(name))throw new ConfigError('Unknown capability; valid names: '+capabilityNames.join(', '));
package/dist/catalog.js CHANGED
@@ -36,7 +36,6 @@ async function validate() {
36
36
  }
37
37
  return validator;
38
38
  }
39
- export const metadataFiles={recipe:'recipe.yaml',example:'example.yaml'} ;
40
39
  /** Reads and schema-validates one metadata file; the id must equal the directory name, and file lists stay authoring-safe paths. */
41
40
  export async function readMetadata(root ,id ,file ) {
42
41
  const text=(await readAuthoringFile(root,file,65536)).toString('utf8');
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { startServer } from './server.js';
14
14
  import {scaffoldProject} from './scaffold.js';
15
15
  import { initProject, addRedirect } from './authoring.js';
16
16
  import { initProjectWith, parseWithNames } from './init-with.js';
17
+ import { collectDependencySet, installSteps, parsePin } from './project-dependencies.js';
17
18
  import { runProjectTests } from './project-tests.js';
18
19
  import { verifyDeployment, failLevels } from './verify-deployment.js';
19
20
 
@@ -27,8 +28,11 @@ import { loadComplianceRules, profileNames as complianceProfiles } from './compl
27
28
  import { parseRouteSnapshot, diffRoutes, renderRouteDiff } from './route-diff.js';
28
29
  import { readFile } from 'node:fs/promises';
29
30
 
30
- const usage = `URLCode 0.4.0-alpha.3 — local/self-hosted runtime
31
- urlcode init <directory> [--with auth,admin] # --with: layered site from installed @jimhoyd/urlcode-<name> packages
31
+ const usage = `URLCode 0.4.1 — local/self-hosted runtime
32
+ urlcode init <directory> [--with ui,auth,admin] [--manifest|--no-manifest] [--pin @scope/pkg=specifier]
33
+ # --with: layered site from installed @jimhoyd/urlcode-<name> packages, with a package.json pinning them exactly; name ui first
34
+ # --manifest: also pin the runtime for a route-only project; --no-manifest: --with without a package.json
35
+ # --pin: record a local path or tarball instead of the registry version; repeatable. No install is ever run for you.
32
36
  urlcode scaffold [--project directory] [--dry-run]
33
37
  urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
34
38
  urlcode dev [--project directory] [--port 3000] [--host 127.0.0.1]
@@ -80,6 +84,7 @@ const print = (value ) => process.stdout.write(typeof value ===
80
84
  const options = {
81
85
  json:{ type:'boolean' }, yaml:{ type:'boolean' }, report:{type:'string'}, 'accept-provider-differences':{type:'boolean'},
82
86
  project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
87
+ manifest:{type:'boolean'}, 'no-manifest':{type:'boolean'}, pin:{type:'string', multiple:true},
83
88
  port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
84
89
  'expect-routes':{type:'string'}, requests:{type:'string'}, concurrency:{type:'string'}, seconds:{type:'string'}, 'max-p95-ms':{type:'string'}, warmup:{type:'string'}, target:{type:'string'},
85
90
  workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
@@ -149,6 +154,8 @@ try {
149
154
  if (command !== 'mcp' && command !== 'context') operatorHost = await loadOperatorHost(values['host-file'], values.project);
150
155
  }
151
156
  if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
157
+ if ((values.manifest || values['no-manifest'] || values.pin !== undefined) && command !== 'init') throw new ConfigError('--manifest/--no-manifest/--pin are only supported by init');
158
+ if (values.manifest && values['no-manifest']) throw new ConfigError('Use either --manifest or --no-manifest');
152
159
  if (values['allow-authoring'] && command !== 'mcp') throw new ConfigError('--allow-authoring is only supported by mcp');
153
160
  const hostOptions = { extensions: operatorHost.extensions, plugins: operatorHost.plugins };
154
161
  if ((!['import','recipes','recipe','examples','example','bulk-import'].includes(command) && extra.length) || (!['init','add','import','recipes','recipe','examples','example','bulk-import','explain','capabilities','schema'].includes(command) && arg)) throw new ConfigError('Unexpected positional arguments');
@@ -266,14 +273,23 @@ try {
266
273
  const loaded = await loadDocument(values.project);
267
274
  print(requestedPermissions(loaded,await prepareFunctionSnapshot(loaded))); break;
268
275
  }
269
- case 'init':
276
+ case 'init': {
270
277
  if (!arg) throw new ConfigError('Provide a new project directory');
271
- if (values.with === undefined) { await initProject(arg); print({ event:'created' }); break; }
272
- {
273
- const created = await initProjectWith(arg, parseWithNames(values.with));
274
- print({ event:'created', ...created, review:`Review ${created.project}/urlcode.yaml and pin its revision explicitly (for example PROJECT_SHA256=${created.projectSha256}); re-review after any project change` });
278
+ // Pins are opt-in for a route-only project (its runtime may be managed elsewhere) and the default for
279
+ // --with, which has just resolved the very packages the generated site depends on.
280
+ const wanted = values.with === undefined ? values.manifest === true : !values['no-manifest'];
281
+ const pins = new Map((values.pin ?? []).map(parsePin));
282
+ if (pins.size && !wanted) throw new ConfigError('--pin needs a manifest; drop --no-manifest or add --manifest');
283
+ if (values.with === undefined) {
284
+ const set = wanted ? await collectDependencySet([], [], { overrides: pins }) : undefined;
285
+ const created = await initProject(arg, { manifest: set });
286
+ print(set ? { event:'created', dependencies:set.pins, nextSteps:installSteps(created, set) } : { event:'created' });
287
+ break;
275
288
  }
289
+ const created = await initProjectWith(arg, parseWithNames(values.with), { manifest: wanted, pins });
290
+ print({ event:'created', ...created, review:`Review ${created.project}/urlcode.yaml and pin its revision explicitly (for example PROJECT_SHA256=${created.projectSha256}); re-review after any project change` });
276
291
  break;
292
+ }
277
293
  case 'validate': {
278
294
  const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions, origin:values.origin });
279
295
  print({ event:'valid', routes:runtime.count, version:runtime.version }); await runtime.close(); break;
package/dist/config.js CHANGED
@@ -74,7 +74,7 @@ function modulePath(pattern , kind , file
74
74
  * a route-level `cache` becomes `policies.cache` (refused alongside a direct `policies.cache`).
75
75
  * Everything downstream (routes, audit, the compiled table) sees only the long form.
76
76
  */
77
- export function normalizeRoute(pattern , route ) {
77
+ function normalizeRoute(pattern , route ) {
78
78
  const authored = route ;
79
79
  const needsFunction = typeof authored.function === 'string';
80
80
  const needsMiddleware = authored.middleware?.some(entry => typeof entry === 'string') ?? false;
package/dist/explain.js CHANGED
@@ -94,7 +94,7 @@ function targetsOf(loaded ,route ,options
94
94
  }
95
95
  return result;
96
96
  }
97
- export function routeState(route ,now ) {return route.enabled===false?'disabled':route.expiresAt&&now>=route.expiresAt?'expired':'active';}
97
+ function routeState(route ,now ) {return route.enabled===false?'disabled':route.expiresAt&&now>=route.expiresAt?'expired':'active';}
98
98
  /** Describe one compiled route. `chain` is the policy chain compiled for it, when the project declares policies. */
99
99
  export function explainCompiledRoute(loaded ,route ,chain ,options ={}) {
100
100
  const root=loaded.root,declared=loaded.routes[route.pattern];
@@ -16,7 +16,7 @@ import { HttpError } from './errors.js';
16
16
 
17
17
 
18
18
  // Hop-by-hop and runtime-owned headers a handler must never set on the wire.
19
- export const forbiddenHeaders = new Set(['connection','keep-alive','transfer-encoding','content-length','upgrade','trailer','proxy-authenticate','proxy-authorization','te']);
19
+ const forbiddenHeaders = new Set(['connection','keep-alive','transfer-encoding','content-length','upgrade','trailer','proxy-authenticate','proxy-authorization','te']);
20
20
 
21
21
  // One place decides what a URLCode response *is*, independent of how a host
22
22
  // delivers it. A Node server writes it to a socket; a Lambda returns it as
package/dist/index.js CHANGED
@@ -42,4 +42,5 @@ export {scaffoldProject} from './scaffold.js';
42
42
 
43
43
  export {initProject, addRedirect} from './authoring.js';
44
44
  export {initProjectWith} from './init-with.js';
45
+ export {collectDependencySet,renderPackageManifest,installSteps} from './project-dependencies.js';
45
46
 
package/dist/init-with.js CHANGED
@@ -8,13 +8,21 @@ import { mcpConfigFile, renderMcpConfig } from './agents-guide.js';
8
8
  import { loadDocument, parseYaml, validateDocument } from './config.js';
9
9
  import { inspectExtensionRevision } from './extensions.js';
10
10
 
11
+ import { collectDependencySet, installSteps, renderPackageManifest } from './project-dependencies.js';
12
+
11
13
  import { ConfigError, assert } from './errors.js';
12
14
 
13
15
  /** Directory names inside the generated site. The route project lives under `app/`; everything else is operator-owned. */
14
- export const PROJECT_DIRECTORY = 'app', HOST_FILE = 'host.mjs', ROUTES_FILE = 'routes/extensions.yaml';
16
+ const PROJECT_DIRECTORY = 'app', HOST_FILE = 'host.mjs', ROUTES_FILE = 'routes/extensions.yaml';
15
17
  const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
16
-
17
-
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
18
26
 
19
27
  export function parseWithNames(value ) {
20
28
  const names = value.split(',').map(name => name.trim());
@@ -22,7 +30,7 @@ export function parseWithNames(value ) {
22
30
  assert(new Set(names).size === names.length, 'Duplicate --with names');
23
31
  return names;
24
32
  }
25
- export const packageName = (name ) => `@jimhoyd/urlcode-${name}`;
33
+ const packageName = (name ) => `@jimhoyd/urlcode-${name}`;
26
34
  const isCode = (error , code ) => error instanceof Error && 'code' in error && error.code === code;
27
35
  const strings = (value ) => Array.isArray(value) && value.every(item => typeof item === 'string');
28
36
  const record = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -32,7 +40,7 @@ const record = (value ) => value !== n
32
40
  * conditions), imports it, and calls its `scaffold` export. Nothing is bundled; core never imports these packages
33
41
  * at build time. Refuses a missing package or a package without `scaffold` before anything is written.
34
42
  */
35
- export async function loadScaffold(name , request , cwd ) {
43
+ async function loadScaffold(name , request , cwd ) {
36
44
  const pkg = packageName(name);
37
45
  let entry ;
38
46
  try { entry = createRequire(join(cwd, 'package.json')).resolve(pkg); }
@@ -68,7 +76,7 @@ async function write(target , content , mode = 0o644)
68
76
  const file = await open(target, 'wx', mode);
69
77
  try { await file.writeFile(content); await file.sync(); } finally { await file.close(); }
70
78
  }
71
- export function renderHost(names , results ) {
79
+ function renderHost(names , results ) {
72
80
  const lines = [`// Generated by urlcode init --with ${names.join(',')}. Trusted operator code: keep it outside ${PROJECT_DIRECTORY}/ and review before serving.`];
73
81
  for (const result of results) lines.push(...result.hostImports);
74
82
  lines.push('');
@@ -85,12 +93,23 @@ function demote(markdown ) {
85
93
  let fence = false;
86
94
  return markdown.split('\n').map(line => { if (/^\s*(?:```|~~~)/.test(line)) fence = !fence; return !fence && /^#{1,5} /.test(line) ? `#${line}` : line; }).join('\n');
87
95
  }
88
- export function renderReadme(directory , names , results , starter , env , projectSha256 ) {
89
- const steps = results.flatMap(result => result.nextSteps);
96
+ function renderDependencySection(directory , set ) {
97
+ const rows = set.pins.map(pin => `- \`${pin.name}\` ${pin.version} (${pin.role})${pin.specifier === pin.version ? '' : ` installed from \`${pin.specifier}\``}`);
98
+ const lines = ['## Dependencies', '',
99
+ '`package.json` pins the runtime, every extension named in `--with` and their declared peers to the exact versions that were installed when this site was generated. Those versions were checked against each package\'s own `peerDependencies` as one set.', '',
100
+ ...rows, '',
101
+ ...installSteps(directory, set).flatMap(step => [step, '']),
102
+ set.local ? 'At least one pin is a local path or tarball rather than a registry version: reproducing this install needs that path to exist, so keep it under your control or replace the specifier before publishing the site.' : 'The pins are registry versions; `npm install` resolves them without the network only if your cache or mirror already holds them.', '',
103
+ 'There is no upgrade command. Changing a pinned version today means editing `package.json` yourself and re-running `npm install`; review the extension changelogs first.', ''];
104
+ return lines.join('\n');
105
+ }
106
+ function renderReadme(directory , names , results , starter , env , projectSha256 , set ) {
107
+ const steps = [...(set ? installSteps(directory, set) : []), ...results.flatMap(result => result.nextSteps)];
90
108
  const parts = [`# ${basename(directory)}`, '',
91
109
  `Created with \`urlcode init ${basename(directory)} --with ${names.join(',')}\`. \`${PROJECT_DIRECTORY}/\` is the route project (\`urlcode.yaml\`, functions, tests); \`${HOST_FILE}\` is the trusted operator host that wires the installed extension packages; operator modules and private data stay outside the project. Run every command with \`--project ${PROJECT_DIRECTORY} --host-file "$PWD/${HOST_FILE}"\`.`, '',
92
110
  '## Starter', '', `The starter files live in \`${PROJECT_DIRECTORY}/\`; add \`--project ${PROJECT_DIRECTORY}\` and the host file to the commands below.`, '', demote(starter).trim(), ''];
93
111
  for (const result of results) parts.push(`## Extension: ${result.name}`, '', result.readme.trim(), '');
112
+ if (set) parts.push(renderDependencySection(directory, set));
94
113
  parts.push('## Next steps', '', ...steps.map((step, index) => `${index + 1}. ${step}`), '');
95
114
  if (Object.keys(env).length) parts.push('## Environment', '', ...Object.entries(env).map(([key, text]) => `- \`${key}\`: ${text}`), '');
96
115
  parts.push('## Project revision', '', `\`${PROJECT_DIRECTORY}/urlcode.yaml\` currently has revision \`${projectSha256}\` (\`inspectExtensionRevision\`). Review the project, then pin exactly that value where the host expects it; any change to extension YAML, policies or mounts changes it and needs a new explicit review.`, '');
@@ -102,7 +121,7 @@ export function renderReadme(directory , names , result
102
121
  * `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
103
122
  * their scaffolds computed before anything is written, so a refusal leaves no directory behind.
104
123
  */
105
- export async function initProjectWith(destination , names , { cwd = process.cwd() } = {}) {
124
+ export async function initProjectWith(destination , names , { cwd = process.cwd(), manifest = true, pins } = {}) {
106
125
  assert(names.length > 0, 'Provide at least one --with name');
107
126
  const directory = resolve(destination), project = join(directory, PROJECT_DIRECTORY), hostFile = join(directory, HOST_FILE);
108
127
  const request = { directory, project, hostFile, names };
@@ -120,6 +139,9 @@ export async function initProjectWith(destination , names
120
139
  const seen = new Set ();
121
140
  for (const file of result.files) { const path = filePath(directory, file.path); assert(!seen.has(path), `${result.name} scaffolds ${file.path} twice`); seen.add(path); }
122
141
  }
142
+ // Also resolved before the destination exists: an incompatible or incompletely installed set refuses with
143
+ // nothing written. It runs after the scaffold conflicts so a composition error is still reported as one.
144
+ const dependencies = manifest ? await collectDependencySet(names, names.map(packageName), { cwd, ...(pins === undefined ? {} : { overrides: pins }) }) : undefined;
123
145
  await mkdir(dirname(directory), { recursive: true });
124
146
  await mkdir(directory, { mode: 0o700 }); // refuses an existing destination
125
147
  try {
@@ -153,13 +175,16 @@ export async function initProjectWith(destination , names
153
175
  await write(target, file.content, file.mode ?? 0o644); written.add(target);
154
176
  }
155
177
  await write(hostFile, renderHost(names, results), 0o600);
156
- await write(join(directory, 'README.md'), renderReadme(directory, names, results, starter, env, projectSha256));
178
+ if (dependencies) await write(join(directory, 'package.json'), renderPackageManifest(directory, dependencies));
179
+ await write(join(directory, 'README.md'), renderReadme(directory, names, results, starter, env, projectSha256, dependencies));
157
180
  await write(join(directory, '.gitignore'), 'node_modules/\ndata/\n.env\n.env.*\n');
158
181
  // The read-only MCP server for agents opened at the site root; --host-file and --allow-authoring stay operator choices.
159
182
  await write(join(directory, mcpConfigFile), renderMcpConfig(PROJECT_DIRECTORY));
160
183
  // AGENTS.md: initProject writes the application-level file into app/ once it produces one (NEXT-STEPS 1.1);
161
184
  // nothing here overrides it. A site-level agent note would be assembled beside README.md at this point.
162
- return { directory, project, hostFile, extensions: [...names], projectSha256, nextSteps: results.flatMap(result => result.nextSteps) };
185
+ return { directory, project, hostFile, extensions: [...names], projectSha256,
186
+ nextSteps: [...(dependencies ? installSteps(directory, dependencies) : []), ...results.flatMap(result => result.nextSteps)],
187
+ dependencies: dependencies?.pins ?? [] };
163
188
  } catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
164
189
  } finally { wipe(); }
165
190
  }
package/dist/manifest.js CHANGED
@@ -111,5 +111,5 @@ export async function buildManifest(project ,options ={})
111
111
  }
112
112
  /** The manifest as `build` writes it: two-space JSON with a trailing newline. */
113
113
  export function renderManifest(manifest ) {return JSON.stringify(manifest,null,2)+'\n';}
114
- export const manifestFileName='manifest.json';
114
+ const manifestFileName='manifest.json';
115
115
  export function manifestPath(out ) {return join(out,manifestFileName);}
@@ -54,7 +54,7 @@ async function verdict(root ,origin ) {
54
54
  catch{return {valid:false ,note:'Project does not validate; call run_validate for the CLI report.'};}
55
55
  }
56
56
  function object(value ) {return value!==null&&typeof value==='object'&&!Array.isArray(value);}
57
- function expandHandler(path ,handler ) {
57
+ function expandHandler(handler ) {
58
58
  if(object(handler))return handler;
59
59
  assert(typeof handler==='string','Handler must be a route object or a short form');
60
60
  if(/^https?:\/\//.test(handler))return {redirect:{url:handler}};
@@ -83,7 +83,7 @@ async function createRoute(root ,args ,origin
83
83
  assert(file==='urlcode.yaml'||(loaded.document.includes??[]).includes(file),'file must be urlcode.yaml or an include listed in it');
84
84
  const target=await confinedPath(root,file);
85
85
  assert(!Object.hasOwn(loaded.routes,path),'Route already exists');
86
- const route={...expandHandler(path,args.handler)};
86
+ const route={...expandHandler(args.handler)};
87
87
  const middleware=expandMiddleware(args.middleware);if(middleware)route.middleware=middleware;
88
88
  const lockPath=join(root,'urlcode.yaml.lock'),lock=await open(lockPath,'wx',0o600);
89
89
  let temp ;
package/dist/mcp.js CHANGED
@@ -78,7 +78,7 @@ export async function serveMcp(options ) {
78
78
  if(message.method==='initialize') {
79
79
  if(initialized){await error(id,-32600,'Already initialized');return;}
80
80
  if(typeof params.protocolVersion!=='string'||!object(params.capabilities)||!object(params.clientInfo)||typeof params.clientInfo.name!=='string'||typeof params.clientInfo.version!=='string'){await error(id,-32602,'Invalid initialize params');return;}
81
- initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.0-alpha.3'}}});return;
81
+ initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.1'}}});return;
82
82
  }
83
83
  if(message.method==='ping'){await send({jsonrpc:'2.0',id,result:{}});return;}
84
84
  if(!ready){await error(id,-32002,'Initialize first');return;}
@@ -198,7 +198,7 @@ function bodyOf(result ) { return result.body ? (Buffer.is
198
198
 
199
199
  // Conditional requests for results the handler did not validate itself:
200
200
  // assets answer 304 before this phase, so only 200 results are examined.
201
- function revalidate(state , req , result ) {
201
+ function revalidate(req , result ) {
202
202
  if (result.status !== 200 || (req.method !== 'GET' && req.method !== 'HEAD')) return result;
203
203
  let headers = result.headers, etag = header(headers, 'etag');
204
204
  if (!etag) {
@@ -249,7 +249,7 @@ export function onResponse(state , req , result
249
249
  // refusal produced ahead of the handler keeps its own headers.
250
250
  if (flight || state.statuses.has(result.status)) headers = mergeVary(headers, state.vary);
251
251
  let out = { ...result, headers };
252
- if (state.strategy === 'revalidate') out = revalidate(state, req, out);
252
+ if (state.strategy === 'revalidate') out = revalidate(req, out);
253
253
  if (!flight) return out;
254
254
  // Store decision for the request that reached the handler; waiters are
255
255
  // released either way, with the entry or with nothing.