@jimhoyd/urlcode 0.4.1 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +60 -7
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +19 -15
- package/SECURITY.md +5 -3
- package/dist/BUILD-MANIFEST.json +31 -28
- package/dist/agent-context.js +82 -0
- package/dist/agents-guide.js +42 -42
- package/dist/authoring.js +12 -2
- package/dist/body-schema.js +159 -0
- package/dist/build-cloudflare.js +2 -0
- package/dist/capabilities.js +1 -1
- package/dist/cli.js +29 -12
- package/dist/config.js +74 -6
- package/dist/context.js +4 -6
- package/dist/errors.js +3 -1
- package/dist/examples.js +1 -1
- package/dist/extensions.js +138 -3
- package/dist/http-policy.js +19 -4
- package/dist/http-response.js +2 -2
- package/dist/init-with.js +71 -9
- package/dist/mcp.js +15 -2
- package/dist/pattern-guard.js +32 -0
- package/dist/policies/security.js +0 -0
- package/dist/policy.js +16 -0
- package/dist/project-tests.js +35 -11
- package/dist/readiness.js +188 -32
- package/dist/recipes.js +1 -1
- package/dist/router.js +17 -0
- package/dist/runtime.js +26 -3
- package/dist/scaffold.js +0 -0
- package/dist/server.js +26 -3
- package/dist/site.js +0 -0
- package/dist/tooling.js +3 -1
- package/dist/types/agent-context.d.ts +44 -0
- package/dist/types/authoring.d.ts +3 -1
- package/dist/types/body-schema.d.ts +58 -0
- package/dist/types/config.d.ts +10 -2
- package/dist/types/context.d.ts +1 -1
- package/dist/types/errors.d.ts +9 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/extensions.d.ts +105 -2
- package/dist/types/http-policy.d.ts +3 -0
- package/dist/types/init-with.d.ts +10 -1
- package/dist/types/pattern-guard.d.ts +10 -0
- package/dist/types/project-tests.d.ts +9 -0
- package/dist/types/readiness.d.ts +71 -0
- package/dist/types/recipes.d.ts +1 -1
- package/dist/types/runtime.d.ts +4 -0
- package/dist/types/server.d.ts +9 -1
- package/dist/types/tooling.d.ts +2 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types.js +10 -2
- package/dist/typescript-authoring.js +5 -3
- package/dist/verify-deployment.js +16 -10
- package/examples/body-validation/README.md +16 -0
- package/examples/body-validation/example.yaml +17 -0
- package/examples/body-validation/tests/requests.json +15 -0
- package/examples/body-validation/urlcode.yaml +34 -0
- package/examples/coverage-waiver/README.md +8 -0
- package/examples/coverage-waiver/example.yaml +16 -0
- package/examples/coverage-waiver/functions/notes.mjs +2 -0
- package/examples/coverage-waiver/tests/requests.json +3 -0
- package/examples/coverage-waiver/urlcode.yaml +10 -0
- package/examples/data-dir/README.md +39 -0
- package/examples/data-dir/data/welcome.txt +1 -0
- package/examples/data-dir/example.yaml +22 -0
- package/examples/data-dir/functions/note.mjs +18 -0
- package/examples/data-dir/tests/requests.json +6 -0
- package/examples/data-dir/urlcode.yaml +6 -0
- package/examples/lifecycle/README.md +15 -0
- package/examples/lifecycle/example.yaml +19 -0
- package/examples/lifecycle/functions/notes.mjs +29 -0
- package/examples/lifecycle/tests/requests.json +12 -0
- package/examples/lifecycle/urlcode.yaml +29 -0
- package/examples/not-found/README.md +10 -0
- package/examples/not-found/example.yaml +17 -0
- package/examples/not-found/public/404.html +3 -0
- package/examples/not-found/public/index.html +3 -0
- package/examples/not-found/tests/requests.json +7 -0
- package/examples/not-found/urlcode.yaml +7 -0
- package/examples/shared-blocks/README.md +11 -0
- package/examples/shared-blocks/example.yaml +17 -0
- package/examples/shared-blocks/tests/requests.json +7 -0
- package/examples/shared-blocks/urlcode.yaml +37 -0
- package/llms-full.txt +436 -149
- package/llms.txt +44 -6
- package/package.json +15 -22
- package/recipes/static-page/README.md +9 -0
- package/recipes/static-page/public/index.html +11 -0
- package/recipes/static-page/recipe.yaml +21 -0
- package/recipes/static-page/tests/requests.json +22 -0
- package/recipes/static-page/urlcode.yaml +7 -0
- package/recipes/static-plus-api/README.md +6 -0
- package/recipes/static-plus-api/urlcode.yaml +4 -0
- package/recipes/store-crud/README.md +53 -0
- package/recipes/store-crud/recipe.yaml +31 -0
- package/recipes/store-crud/tests/requests.json +18 -0
- package/recipes/store-crud/urlcode.yaml +18 -0
- package/schemas/urlcode.schema.json +125 -60
- package/skills/urlcode/SKILL.md +53 -26
- package/starters/default/AGENTS.md +43 -43
- package/starters/page/README.md +14 -0
- package/starters/page/public/index.html +12 -0
- package/starters/page/tests/requests.json +17 -0
- package/starters/page/urlcode.yaml +6 -0
- package/.claude-plugin/marketplace.json +0 -18
- package/CONTRIBUTING.md +0 -112
- package/ROADMAP.md +0 -66
- package/docs/AI-AUTHORING.md +0 -338
- package/docs/ASSETS.md +0 -107
- package/docs/AUTH-BACKUP.md +0 -32
- package/docs/AWS.md +0 -86
- package/docs/BEST-PRACTICES.md +0 -276
- package/docs/BULK.md +0 -79
- package/docs/CAPABILITIES.md +0 -192
- package/docs/CAPACITY.md +0 -305
- package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
- package/docs/CI.md +0 -147
- package/docs/CLOUDFLARE.md +0 -109
- package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
- package/docs/COMPLIANCE.md +0 -239
- package/docs/COMPOSING-A-SITE.md +0 -278
- package/docs/CONDITIONS.md +0 -74
- package/docs/DEPLOYMENT-CHECKS.md +0 -108
- package/docs/DEVELOPMENT-PIPELINE.md +0 -270
- package/docs/EGRESS.md +0 -125
- package/docs/EXTENSIONS.md +0 -438
- package/docs/FRAMEWORK.md +0 -217
- package/docs/FUNCTION-SECURITY.md +0 -254
- package/docs/HTTP.md +0 -129
- package/docs/INSTALL.md +0 -128
- package/docs/INTERCHANGE.md +0 -134
- package/docs/LOAD-TESTING.md +0 -91
- package/docs/LOCAL-DEVELOPMENT.md +0 -102
- package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
- package/docs/MIDDLEWARE.md +0 -102
- package/docs/MONITORING.md +0 -115
- package/docs/OBSERVABILITY.md +0 -222
- package/docs/OPEN-DECISIONS.md +0 -224
- package/docs/OPERATIONAL-PROOF.md +0 -41
- package/docs/OPERATIONS.md +0 -201
- package/docs/ORGANIZATION.md +0 -135
- package/docs/PERFORMANCE.md +0 -72
- package/docs/PLUGINS.md +0 -271
- package/docs/POLICIES.md +0 -211
- package/docs/PRERENDER.md +0 -245
- package/docs/PROJECT-DIRECTION.md +0 -118
- package/docs/PROVIDER-VERIFICATION.md +0 -84
- package/docs/READINESS.md +0 -150
- package/docs/README.md +0 -87
- package/docs/RECIPES.md +0 -99
- package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
- package/docs/RELEASE-0.4.1.md +0 -73
- package/docs/RELEASE-READINESS.md +0 -117
- package/docs/RELEASE-SECURITY.md +0 -96
- package/docs/RESILIENCE.md +0 -161
- package/docs/ROUTING.md +0 -92
- package/docs/SANDBOX-REVIEW.md +0 -72
- package/docs/SCAFFOLDING.md +0 -70
- package/docs/SECURITY-AUDIT.md +0 -164
- package/docs/SITE.md +0 -150
- package/docs/SPECIFICATION.md +0 -359
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
- package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
- package/docs/SPIKE-CORE-LAYERING.md +0 -368
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -211
- package/docs/STANDARDS.md +0 -311
- package/docs/STARTERS.md +0 -83
- package/docs/STATIC.md +0 -105
- package/docs/TOOLING.md +0 -298
- package/docs/TUNNELS.md +0 -72
- package/docs/TYPESCRIPT-AUTHORING.md +0 -87
- package/docs/TYPESCRIPT.md +0 -123
- package/docs/VERCEL.md +0 -114
- package/docs/VERSION-ALIGNMENT.md +0 -80
- package/docs/YAML-GUIDE.md +0 -57
- package/docs/YAML-REFERENCE.md +0 -449
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
- package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
- package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
- package/docs/archive/2026-09-19/ROADMAP.md +0 -387
- package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
- package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
- package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
- package/docs/archive/README.md +0 -28
- package/docs/policies/agents.md +0 -182
- package/docs/policies/cache.md +0 -152
- package/docs/policies/compression.md +0 -169
- package/docs/policies/contract.md +0 -52
- package/docs/policies/hardened.md +0 -56
- package/docs/policies/interoperability.md +0 -169
- package/docs/policies/operations.md +0 -45
- package/docs/policies/security.md +0 -161
- package/docs/policies/throttle.md +0 -103
- package/docs/yaml/assets.md +0 -36
- package/docs/yaml/conditions.md +0 -20
- package/docs/yaml/functions.md +0 -168
- package/docs/yaml/middleware.md +0 -31
- package/docs/yaml/organization.md +0 -74
- package/docs/yaml/policies.md +0 -37
- package/docs/yaml/redirects.md +0 -64
- package/docs/yaml/responses.md +0 -57
- package/docs/yaml/site.md +0 -24
- package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -120
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/docs/EXTENSIONS.md
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
# Operator-installed extensions
|
|
2
|
-
|
|
3
|
-
Extensions are trusted operator modules, separate from a project's own
|
|
4
|
-
`function`/`middleware` code. Auth
|
|
5
|
-
and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
|
|
6
|
-
supplies only the generic integration contract. No project file can import a host
|
|
7
|
-
extension or choose its npm package.
|
|
8
|
-
|
|
9
|
-
Stored short links moved out of core this way too: a `urlcode-dynamic-link`
|
|
10
|
-
package (mount-based, like `auth`/`admin`) owned the durable link store, its CLI
|
|
11
|
-
and management API. That package has since been retired and unpublished, so no
|
|
12
|
-
supported stored-link extension ships today. Core no longer has a native `link`
|
|
13
|
-
handler or a `dynamicLinks` project flag.
|
|
14
|
-
|
|
15
|
-
A project declares versioned configuration and exclusive route mounts:
|
|
16
|
-
|
|
17
|
-
```yaml
|
|
18
|
-
version: "1"
|
|
19
|
-
extensions:
|
|
20
|
-
auth:
|
|
21
|
-
version: "1"
|
|
22
|
-
config: {}
|
|
23
|
-
routes:
|
|
24
|
-
/auth/*:
|
|
25
|
-
extension: auth
|
|
26
|
-
methods: [GET, HEAD, POST]
|
|
27
|
-
/private:
|
|
28
|
-
respond: {text: Private}
|
|
29
|
-
policies:
|
|
30
|
-
extensions:
|
|
31
|
-
auth: {signedIn: true}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Protecting a route: the `auth` short form
|
|
35
|
-
|
|
36
|
-
When the project declares `extensions.auth`, a route may say `auth` instead of
|
|
37
|
-
spelling out `policies.extensions.auth`. This is the preferred way to protect a
|
|
38
|
-
route:
|
|
39
|
-
|
|
40
|
-
```yaml
|
|
41
|
-
routes:
|
|
42
|
-
/account:
|
|
43
|
-
respond: {text: Account}
|
|
44
|
-
auth: {role: member} # or `auth: true` for any signed-in principal
|
|
45
|
-
/docs:
|
|
46
|
-
respond: {text: Docs}
|
|
47
|
-
auth: {required: false} # documents intent; emits no requirement
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The compiler expands the short form before anything else reads the project:
|
|
51
|
-
`auth: true` becomes `policies.extensions.auth: {}` and an object becomes the
|
|
52
|
-
same object minus `required`. The long form stays the canonical representation,
|
|
53
|
-
so `routes`, `audit` and `explain` show the expansion, the extension revision
|
|
54
|
-
hash covers it, and the installed auth extension validates the expanded
|
|
55
|
-
requirement with its own policy schema. The keys other than `required` are
|
|
56
|
-
exactly that schema's keys (`role`, `permission`, `verified`,
|
|
57
|
-
`freshWithinSeconds`, `onDeny`); the runtime adds nothing of its own. Loading
|
|
58
|
-
fails, naming the route, when `auth` appears without an `extensions.auth`
|
|
59
|
-
declaration, next to `policies.extensions.auth`, or next to
|
|
60
|
-
`policies.extensions: false`.
|
|
61
|
-
|
|
62
|
-
The same shape is used for the cache policy: a route-level `cache: {strategy,
|
|
63
|
-
maxAge, ...}` expands to `policies.cache` in the same pass (see
|
|
64
|
-
[policies](POLICIES.md)).
|
|
65
|
-
|
|
66
|
-
The configuration and requirement objects above are validated by the installed
|
|
67
|
-
extension's schemas. They are examples of extension-owned fields, not built-in
|
|
68
|
-
authentication behavior. See the executable generic fixture in
|
|
69
|
-
[examples/extensions](../examples/extensions). Included files can declare
|
|
70
|
-
extensions; duplicate names fail rather than silently override one another.
|
|
71
|
-
|
|
72
|
-
The operator passes `extensions: RuntimeExtension[]` to `createRuntime`,
|
|
73
|
-
`startServer`, or the AWS/Vercel adapter. Types and
|
|
74
|
-
`inspectExtensionRevision(project)` are exported from
|
|
75
|
-
`@jimhoyd/urlcode/extensions`. Inspection does not grant access: review the
|
|
76
|
-
project and place the exact returned SHA-256 in each registration's
|
|
77
|
-
`projectSha256`. YAML extension configuration, policies and routes participate
|
|
78
|
-
in the revision. Changing them requires an explicit operator reapproval.
|
|
79
|
-
|
|
80
|
-
Registrations provide a name, contract version, target list, JSON configuration
|
|
81
|
-
schema, optional policy schema, an optional declared `cacheSensitive` (below)
|
|
82
|
-
and activation factory. Activation receives the
|
|
83
|
-
canonical operator origin, target, revision and mount bases. Its instance handles
|
|
84
|
-
bounded requests and, when named in a route's policies, gates the request via
|
|
85
|
-
`authorize`, wraps the rest of the pipeline via `middleware`, or both (see
|
|
86
|
-
[Wrapping a route](#wrapping-a-route-extension-middleware) above). Missing
|
|
87
|
-
registrations, stale grants, invalid configuration and unsupported targets fail
|
|
88
|
-
activation. Multiple mounts cannot overlap other declared routes.
|
|
89
|
-
|
|
90
|
-
For extension-protected routes, agents/throttle run before authorization and
|
|
91
|
-
cache access happens only after authorization. This part is unconditional:
|
|
92
|
-
naming any extension in `policies.extensions` always runs its `authorize()`
|
|
93
|
-
(when it implements one) before the route's own handler, whatever this
|
|
94
|
-
section says next.
|
|
95
|
-
|
|
96
|
-
An `extension:` mount is always confidential: its route rejects cache
|
|
97
|
-
strategies other than no-store, and every response is forced to no-store
|
|
98
|
-
after host response hooks, with compression disabled. A `policies.extensions`
|
|
99
|
-
route (no mount, `authorize`/`middleware` only) gets the same treatment
|
|
100
|
-
**unless every extension it names explicitly declares
|
|
101
|
-
`cacheSensitive: false`** on its `RuntimeExtension` registration. That field
|
|
102
|
-
defaults to sensitive (unset or `true`): the safe default is unchanged, and
|
|
103
|
-
relaxing it is an explicit, reviewed operator opt-in an extension author
|
|
104
|
-
makes once, in host code, never inferred from a route or from a response the
|
|
105
|
-
extension happens to return. It exists for a generic, cache-transparent
|
|
106
|
-
extension whose `middleware()` is pure request/response wrapping with no
|
|
107
|
-
access-control semantics of its own (a logging or header-rewriting
|
|
108
|
-
extension, for example) — declared this way, its wrapped route keeps
|
|
109
|
-
whatever `Cache-Control` its own handler sets, exactly like the native
|
|
110
|
-
`middleware:` array already does, and compression is not disabled either. A
|
|
111
|
-
route naming more than one extension stays confidential if any one of them
|
|
112
|
-
is sensitive (or leaves the field unset); one `cacheSensitive: false`
|
|
113
|
-
extension cannot relax a route that also names a sensitive one. This can
|
|
114
|
-
only relax the no-store floor a generic extension would otherwise inherit —
|
|
115
|
-
it has no effect on `authorize()`, which runs the same way regardless, and
|
|
116
|
-
`auth`/`admin`-style extensions gating real access must leave it at the
|
|
117
|
-
default.
|
|
118
|
-
|
|
119
|
-
## Wrapping a route: extension middleware
|
|
120
|
-
|
|
121
|
-
`authorize` is a gate: it runs once, before the route's handler, and can only
|
|
122
|
-
either let the request through unchanged or answer instead of it. It cannot
|
|
123
|
-
see or change what the handler itself returns.
|
|
124
|
-
|
|
125
|
-
`middleware` is a wrap. An extension instance may implement it alongside or
|
|
126
|
-
instead of `authorize`, attached the same way, via
|
|
127
|
-
`policies.extensions.<name>` on a route (no `extension:` mount required); its
|
|
128
|
-
`config` is exactly the same per-route value `authorize`'s `requirement`
|
|
129
|
-
receives, validated once against the extension's `policySchema`:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
middleware?(config: Readonly<Record<string, unknown>>, request: ExtensionRequest,
|
|
133
|
-
next: () => Promise<HandlerResult>): HandlerResult | Promise<HandlerResult>;
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
`next()` invokes the rest of the pipeline for that route: any other extension
|
|
137
|
-
`middleware()` also declared on the route (see below), then the route's own
|
|
138
|
-
native `middleware:` chain and handler, dispatched through the sandboxed or
|
|
139
|
-
trusted engine exactly as it is today. Calling it lets the hook run code
|
|
140
|
-
before and after the rest of the pipeline, inspecting or mutating the
|
|
141
|
-
`HandlerResult` it resolves to — the same "add a header to whatever the
|
|
142
|
-
handler returns" shape as the native `middleware/headers.mjs` cookbook
|
|
143
|
-
recipe, but declared by an operator-installed extension instead of project
|
|
144
|
-
code. Skipping it short-circuits everything after that point, the same
|
|
145
|
-
capability `authorize` already has, just usable from either side of the
|
|
146
|
-
handler now. `next()` may be called at most once; calling it again throws.
|
|
147
|
-
|
|
148
|
-
A route naming more than one extension in `policies.extensions` chains every
|
|
149
|
-
one that implements `middleware`, in the order the keys are declared, each
|
|
150
|
-
one's `next()` reaching the next one and the innermost `next()` reaching the
|
|
151
|
-
native pipeline — the first declared name is outermost. This is purely
|
|
152
|
-
additive at the `policies.extensions` layer and never touches the native
|
|
153
|
-
`middleware:` array, its schema, or its dispatch, all of which are unchanged.
|
|
154
|
-
|
|
155
|
-
`authorize` and `middleware` compose on the same route, from the same or
|
|
156
|
-
different extensions, without special-casing: `authorize` always runs first
|
|
157
|
-
(unchanged), and any declared `middleware()` wraps everything after that
|
|
158
|
-
point, including the rest of the authorize-gated pipeline. A route naming an
|
|
159
|
-
extension via `policies.extensions` only requires that extension to
|
|
160
|
-
implement `authorize`, `middleware`, or both — never both unconditionally.
|
|
161
|
-
|
|
162
|
-
One exception exists for content-hashed assets. A registration may declare
|
|
163
|
-
`immutableAssets: {prefix: '/static'}`, a normalized literal path under each
|
|
164
|
-
of its mounts (no `.` or `..` segments, no trailing slash). The runtime then
|
|
165
|
-
answers `Cache-Control: public, max-age=31536000, immutable` instead of
|
|
166
|
-
no-store only when every condition holds: the request path lies under
|
|
167
|
-
`<mount><prefix>/`, the method is GET or HEAD, the status is 200 or 304, the
|
|
168
|
-
response carries exactly one strong ETag, sets no Set-Cookie, and does not
|
|
169
|
-
vary on Cookie, Authorization or `*`. A stricter Cache-Control the extension
|
|
170
|
-
set (no-store, no-cache, private or a shorter max-age) is preserved; other CDN
|
|
171
|
-
cache headers are still stripped and compression stays disabled. Anything
|
|
172
|
-
that fails a condition, including a cookie added by a later response hook,
|
|
173
|
-
stays no-store. The extension owns the content-hashed filename: a file under
|
|
174
|
-
the prefix must change its name when its bytes change, because clients never
|
|
175
|
-
revalidate it. The prefix belongs to the operator registration, not to the
|
|
176
|
-
pinned project revision. The runtime withholds Cookie and Authorization plus any declared
|
|
177
|
-
credential headers from all application guest requests and mapped parameters.
|
|
178
|
-
This does not isolate browser JavaScript running on the same origin: application
|
|
179
|
-
HTML/JS on an authentication origin must be trusted by that site's operator.
|
|
180
|
-
|
|
181
|
-
Cloudflare refuses extensions until its artifact format supports their execution.
|
|
182
|
-
Node adapter conformance is not a live-provider deployment claim.
|
|
183
|
-
|
|
184
|
-
## Project-level lifecycle hooks
|
|
185
|
-
|
|
186
|
-
`authorize` and `middleware` let core call *into* an extension. They do not
|
|
187
|
-
let a project hand its own code *to* an extension to run at a defined point.
|
|
188
|
-
That gap matters once an extension has meaningful lifecycle events —
|
|
189
|
-
registration, deletion, an administrative action, a link resolution, and so
|
|
190
|
-
on. Presentation already has a standard layering mechanism for this: a
|
|
191
|
-
project customizes an extension's *look* through `urlcode-ui`'s
|
|
192
|
-
`copy`/`extra.css`/`templates` without forking it. Behavior needs the same
|
|
193
|
-
standard, or every extension author (and every project depending on one)
|
|
194
|
-
either reinvents it or forks the extension. `urlcode-auth` and
|
|
195
|
-
`urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
|
|
196
|
-
[SPIKE-AUTH.md](../packages/auth/docs/SPIKE-AUTH.md)
|
|
197
|
-
already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
|
|
198
|
-
|
|
199
|
-
**The pattern.** An extension with lifecycle events an author judges worth
|
|
200
|
-
exposing should let the project name its own function in the extension's own
|
|
201
|
-
`config`, using the same source shape `function`/`middleware` routes already
|
|
202
|
-
use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
|
|
203
|
-
and add its own `sandbox` boolean next to it (below). The extension's own
|
|
204
|
-
`activate()` reads that config, and its own runtime dispatch — not a new core
|
|
205
|
-
primitive, an ordinary call the extension package makes with the request
|
|
206
|
-
context it already has — invokes the named function at the lifecycle point
|
|
207
|
-
it defines, with a typed input and a typed verdict the extension's own
|
|
208
|
-
schema documents. For example, an auth-style extension might declare:
|
|
209
|
-
|
|
210
|
-
```yaml
|
|
211
|
-
extensions:
|
|
212
|
-
auth:
|
|
213
|
-
version: "1"
|
|
214
|
-
config:
|
|
215
|
-
hooks:
|
|
216
|
-
beforeRegister:
|
|
217
|
-
source: ./hooks/registration-rule.mjs
|
|
218
|
-
export: default
|
|
219
|
-
onSignUp:
|
|
220
|
-
source: ./hooks/on-signup.mjs
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
with `beforeRegister` called before an account is created, given a typed
|
|
224
|
-
`{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
|
|
225
|
-
or `{allow: false, reason}`), and `onSignUp` called after, for side effects
|
|
226
|
-
such as provisioning a workspace. The exact hook names, input/verdict shapes
|
|
227
|
-
and invocation points are the extension's own design — `hooks` is not a core
|
|
228
|
-
schema key — but the source/export/sandbox shape, and the fact that this is
|
|
229
|
-
project code the extension calls rather than an operator callback in
|
|
230
|
-
`host.mjs`, should be consistent across extensions so an author who has
|
|
231
|
-
learned one has learned the pattern.
|
|
232
|
-
|
|
233
|
-
**Trust and execution mode.** Project-level lifecycle hooks are first-party
|
|
234
|
-
project code, the same category as any `function`/`middleware` route, and
|
|
235
|
-
follow the same trusted-by-default rule with no special case
|
|
236
|
-
(docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
|
|
237
|
-
trusted, in-process execution by default; a project sets `sandbox: true` on
|
|
238
|
-
a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
|
|
239
|
-
the mechanism `function`/`middleware` routes already use and for the same
|
|
240
|
-
reason — the hook's own code, not the trustworthiness of whatever triggered
|
|
241
|
-
it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
|
|
242
|
-
route needs `sandbox: true`"). This was raised as an open question — whether
|
|
243
|
-
a lifecycle hook should always run sandboxed because it makes a
|
|
244
|
-
security-relevant decision — and settled the other way: uniformity with the
|
|
245
|
-
rest of the trust model was chosen over hardwiring isolation for lifecycle
|
|
246
|
-
hooks specifically, the same "no special case" call already made between
|
|
247
|
-
`function` and `middleware` trust. A `beforeRegister` hook enforcing "only
|
|
248
|
-
`@acme.com` may register" is the project's own governance rule over its own
|
|
249
|
-
signup flow; it is not more dangerous than any other route the project
|
|
250
|
-
wrote, and does not get a different default.
|
|
251
|
-
|
|
252
|
-
Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
|
|
253
|
-
`src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
|
|
254
|
-
each half of a hook's `sandbox: true` opt-in has its own answer:
|
|
255
|
-
|
|
256
|
-
- **Trusted (the default, no `sandbox: true`).** No core primitive is needed
|
|
257
|
-
or provided: this is ordinary first-party project code, and the
|
|
258
|
-
extension's own `activate()` already has `ExtensionActivation.root` to
|
|
259
|
-
resolve the hook's `source` against and can `import()` it directly, the
|
|
260
|
-
same way any trusted `function`/`middleware` route does. Do that import
|
|
261
|
-
with a per-activation cache-busting query, the way core's own trusted
|
|
262
|
-
route activation does (`src/trusted-functions.ts`): Node's ESM loader
|
|
263
|
-
caches a resolved module forever by URL, so a plain `import()` of the
|
|
264
|
-
unchanged file URL makes a second activation in the same process keep
|
|
265
|
-
serving the hook code that was on disk at the first one
|
|
266
|
-
(jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
|
|
267
|
-
this way — modules the hook itself imports stay on Node's module cache,
|
|
268
|
-
the same limitation the trusted route path has, so a change to a hook's
|
|
269
|
-
own dependency still needs a process restart.
|
|
270
|
-
- **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
|
|
271
|
-
`SandboxPool`, the same QuickJS/worker-thread engine that backs a
|
|
272
|
-
sandboxed `function`/`middleware` route — the identical module-allowlist
|
|
273
|
-
walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
|
|
274
|
-
response-shape validation, with no separate or weaker engine for
|
|
275
|
-
extensions. It takes an explicit list of `{source, export}` entries
|
|
276
|
-
(resolve a hook's `source` string with the re-exported `functionFile()`,
|
|
277
|
-
the same resolution/validation a native route's `source` gets) instead of
|
|
278
|
-
anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
|
|
279
|
-
in place of a `FunctionRoute`. There is no "trusted" mode exported
|
|
280
|
-
alongside it — `SandboxPool` is only ever the isolated path; see
|
|
281
|
-
[FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
|
|
282
|
-
[TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
|
|
283
|
-
|
|
284
|
-
An extension honoring a hook's `sandbox: true` is expected to actually
|
|
285
|
-
isolate that invocation through `SandboxPool` now that the primitive exists
|
|
286
|
-
(or document plainly that it does not yet, rather than accepting the field
|
|
287
|
-
and silently running it trusted) — say which, in the extension's own docs,
|
|
288
|
-
so an author reading them is not misled about what opt-in exists.
|
|
289
|
-
|
|
290
|
-
## Discovering schemas
|
|
291
|
-
|
|
292
|
-
Each registration carries the JSON Schemas that validate its `config` block and
|
|
293
|
-
its per-route policy requirements. `urlcode extensions` prints them together with
|
|
294
|
-
the project's own declarations so an author can see what a mount accepts:
|
|
295
|
-
|
|
296
|
-
```sh
|
|
297
|
-
urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--json]
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
For every registration in the host file it reports the name, contract version,
|
|
301
|
-
targets, credential headers, configuration schema, policy schema (if any),
|
|
302
|
-
whether the project declares it, whether its `projectSha256` matches the current
|
|
303
|
-
revision, the routes that mount it and the routes whose policies require it.
|
|
304
|
-
Declared names the host does not register are listed as unregistered. The command
|
|
305
|
-
executes the trusted host module exactly as `validate` does, including its
|
|
306
|
-
absolute-path and outside-project rules, and calls `close` afterwards; it never
|
|
307
|
-
activates an extension and grants nothing. Without `--host-file` it lists only
|
|
308
|
-
the names the project declares and notes that schemas need the host file.
|
|
309
|
-
|
|
310
|
-
The same report is available as `inspectExtensions({project, hostFile?})` from
|
|
311
|
-
the package root and, for assistants, as the MCP tool `get_extensions`, which the
|
|
312
|
-
server advertises only when the operator started `urlcode mcp` with
|
|
313
|
-
`--host-file`. No tool argument can name a host file. See [TOOLING.md](TOOLING.md).
|
|
314
|
-
|
|
315
|
-
## CLI host binding
|
|
316
|
-
|
|
317
|
-
Use an explicitly named operator ES module outside the application directory:
|
|
318
|
-
|
|
319
|
-
```sh
|
|
320
|
-
urlcode serve --project ./site --origin https://site.example \
|
|
321
|
-
--host-file /absolute/operator/host.mjs
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
The module default-exports `{extensions, plugins?, close?}`. It may import installed
|
|
325
|
-
operator packages, open their stores and read operator secrets. `close` releases
|
|
326
|
-
shared services when the CLI command finishes or the server shuts down. A runtime
|
|
327
|
-
reload closes extension instances but does not close caller-owned services. Host
|
|
328
|
-
modules are not watched or automatically rediscovered. Restart to update them.
|
|
329
|
-
|
|
330
|
-
The same explicit option is supported by dev, validate, test, routes, audit,
|
|
331
|
-
benchmark, extensions and mcp. These commands execute trusted host activation and may access its
|
|
332
|
-
store; read-only project inspection commands never implicitly load a host file.
|
|
333
|
-
Host-file paths must be absolute `.mjs`/`.js` files whose real path lies outside
|
|
334
|
-
the project, including after symlink resolution. This is an operator-code trust
|
|
335
|
-
boundary, not a JavaScript sandbox or an independent security review.
|
|
336
|
-
|
|
337
|
-
## Scaffolding with `init --with`
|
|
338
|
-
|
|
339
|
-
`urlcode init <directory> --with ui,auth,admin` produces the layered site the
|
|
340
|
-
[framework page](FRAMEWORK.md#the-composition-contract) describes in one
|
|
341
|
-
command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
|
|
342
|
-
and each extension's own operator files. Core never bundles or imports the
|
|
343
|
-
extension packages at build time; at run time it resolves
|
|
344
|
-
`@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
|
|
345
|
-
the invoking directory. Install a compatible package set there, as shown in
|
|
346
|
-
[the framework guide](FRAMEWORK.md#the-composition-contract), before using
|
|
347
|
-
`--with ui,auth`; UI must activate before auth. Core imports the package and calls its
|
|
348
|
-
`scaffold` export with this request:
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
interface ScaffoldRequest {
|
|
352
|
-
directory: string; // absolute site directory; result file paths are relative to it
|
|
353
|
-
project: string; // absolute route project, <directory>/app (holds urlcode.yaml)
|
|
354
|
-
hostFile: string; // absolute combined host module, <directory>/host.mjs
|
|
355
|
-
names: readonly string[]; // every name in --with order, including this one
|
|
356
|
-
}
|
|
357
|
-
interface ScaffoldFile { path: string; content: string | Uint8Array; mode?: number }
|
|
358
|
-
interface ScaffoldResult {
|
|
359
|
-
name: string; // must equal the requested name
|
|
360
|
-
extensions: Record<string, unknown>; // merged into the project's top-level extensions
|
|
361
|
-
routes: Record<string, unknown>; // merged into app/routes/extensions.yaml
|
|
362
|
-
hostImports: string[]; hostSetup: string[]; hostEntries: string[]; hostClose?: string[];
|
|
363
|
-
files: ScaffoldFile[]; // written relative to directory with their modes
|
|
364
|
-
readme: string; nextSteps: string[]; // README section and numbered steps
|
|
365
|
-
env?: Record<string, string>; // environment variables the host reads
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
`scaffold` writes nothing; it returns fragments and may generate key material
|
|
370
|
-
in memory (core zeroes `Uint8Array` contents after writing or on failure). The
|
|
371
|
-
types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
|
|
372
|
-
against them.
|
|
373
|
-
|
|
374
|
-
Assembly rules, in `--with` order:
|
|
375
|
-
|
|
376
|
-
- Every package is resolved and every `scaffold` is called before anything is
|
|
377
|
-
written. A name that is not installed refuses with the `npm install` command;
|
|
378
|
-
a package without a `scaffold` export refuses and names the package; an error
|
|
379
|
-
thrown by a `scaffold` (for example admin without auth in the same `--with`)
|
|
380
|
-
is reported as that package's refusal. No directory is left behind.
|
|
381
|
-
- `extensions` fragments are declared in `app/urlcode.yaml`; `routes`
|
|
382
|
-
fragments are written to `app/routes/extensions.yaml`, appended to the
|
|
383
|
-
starter's `includes`, so the starter's own routes load first. A route or
|
|
384
|
-
extension key produced twice, or one the starter already declares, is refused
|
|
385
|
-
naming both sources.
|
|
386
|
-
- `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
|
|
387
|
-
`extensions` array of every `hostEntries` item, then `close()` running the
|
|
388
|
-
`hostClose` statements in reverse `--with` order so later entries release
|
|
389
|
-
before what they built on. Setup lines share one module scope: admin's entry
|
|
390
|
-
references the `service`, `csrfKey` and `projectSha256` identifiers that
|
|
391
|
-
auth's setup defines, which is why `names` carries the full list.
|
|
392
|
-
- `files` are created exclusively (`wx`) with their `mode` (default `0644`),
|
|
393
|
-
must stay inside the site directory and outside `app/`, and never pass
|
|
394
|
-
through a symlink. Nothing generated is ever overwritten; an existing
|
|
395
|
-
destination refuses like plain `init`.
|
|
396
|
-
- `README.md` holds the starter's README as a section, then each result's
|
|
397
|
-
`readme` under `## Extension: <name>`, the merged numbered `nextSteps`, the
|
|
398
|
-
merged `env` table and the project revision. The command prints that
|
|
399
|
-
revision (`inspectExtensionRevision` of `app/`) with the instruction to
|
|
400
|
-
review the project and pin it explicitly; the host is generated to require
|
|
401
|
-
the pin, never to compute it.
|
|
402
|
-
|
|
403
|
-
### Recorded versions
|
|
404
|
-
|
|
405
|
-
`init --with` also writes `<directory>/package.json`: a private manifest that
|
|
406
|
-
pins, at exactly the version that was resolved, the running runtime, every
|
|
407
|
-
extension named in `--with`, and every package those extensions declare in
|
|
408
|
-
`peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
|
|
409
|
-
although nobody named it). Before anything is written, the whole set is checked
|
|
410
|
-
against every declared peer range; an incompatible combination or a missing
|
|
411
|
-
required peer refuses and names it, leaving no directory behind.
|
|
412
|
-
|
|
413
|
-
Nothing is installed. The generated site has no `node_modules` and no
|
|
414
|
-
`package-lock.json` until you run `npm install` in it yourself, which the
|
|
415
|
-
command and the generated README both state as the next step. Reproducibility
|
|
416
|
-
comes from that install, not from generation.
|
|
417
|
-
|
|
418
|
-
- `--no-manifest` generates the site without a `package.json`, for a site whose
|
|
419
|
-
dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
|
|
420
|
-
still writes no manifest; add `--manifest` to pin the runtime for a
|
|
421
|
-
route-only project too.
|
|
422
|
-
- `--pin <package>=<specifier>` records a specifier instead of the resolved
|
|
423
|
-
version, for local tarball or offline development
|
|
424
|
-
(`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
|
|
425
|
-
package installed from a local path or tarball is detected from npm's own
|
|
426
|
-
install record and pinned by that path without any flag; the README says so,
|
|
427
|
-
because such a pin only reproduces where that path exists.
|
|
428
|
-
|
|
429
|
-
There is no upgrade command. Moving a generated project to newer versions today
|
|
430
|
-
means editing its `package.json` and re-running `npm install` yourself; nothing
|
|
431
|
-
in this runtime selects a newer tested set, shows the change, or updates a
|
|
432
|
-
lockfile for you.
|
|
433
|
-
|
|
434
|
-
Serving the result is the usual explicit host binding:
|
|
435
|
-
|
|
436
|
-
```sh
|
|
437
|
-
urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
|
|
438
|
-
```
|
package/docs/FRAMEWORK.md
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
# The URLCode framework
|
|
2
|
-
|
|
3
|
-
One page for people and AI agents. It says what the four packages are, how a
|
|
4
|
-
project grows from a handful of redirects into an application with accounts
|
|
5
|
-
and an administration console, and which facts an agent must not guess. Every
|
|
6
|
-
claim here is implemented in the linked repository; nothing is roadmap.
|
|
7
|
-
|
|
8
|
-
## Four packages, one project shape
|
|
9
|
-
|
|
10
|
-
| Package | Source | What it adds | How a project declares it |
|
|
11
|
-
|---|---|---|---|
|
|
12
|
-
| `@jimhoyd/urlcode` | this repository | The runtime: YAML routes, functions and middleware (trusted by default, `sandbox: true` opt-in), pages and assets, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
|
|
13
|
-
| `@jimhoyd/urlcode-ui` | [`packages/ui`](../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 |
|
|
14
|
-
| `@jimhoyd/urlcode-auth` | [`packages/auth`](../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 |
|
|
15
|
-
| `@jimhoyd/urlcode-admin` | [`packages/admin`](../packages/admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
|
|
16
|
-
|
|
17
|
-
All four are Apache-2.0. The `0.4.1` release line aligns their stable versions;
|
|
18
|
-
check `npm run release:status` for publication progress. A stable npm channel is
|
|
19
|
-
not an independent assessment: review, deployment evidence and an accessibility
|
|
20
|
-
assessment are still pending
|
|
21
|
-
([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
|
|
22
|
-
files say exactly what is built: [auth](../packages/auth/IMPLEMENTATION-STATUS.md),
|
|
23
|
-
[admin](../packages/admin/IMPLEMENTATION-STATUS.md),
|
|
24
|
-
[ui](../packages/ui/IMPLEMENTATION-STATUS.md).
|
|
25
|
-
The current version of each package is its own manifest, and the peer ranges it
|
|
26
|
-
declares are in that manifest too; do not read a version number out of this
|
|
27
|
-
page. How versions, channels and release tags line up is recorded in
|
|
28
|
-
[package and channel alignment](VERSION-ALIGNMENT.md), and `npm run
|
|
29
|
-
release:status` reports the live registry and tag state.
|
|
30
|
-
|
|
31
|
-
## The ladder
|
|
32
|
-
|
|
33
|
-
A project climbs these rungs by adding YAML, never by rewriting what it has.
|
|
34
|
-
Each rung's YAML is valid on every rung above it.
|
|
35
|
-
|
|
36
|
-
1. **Redirects.** A `urlcode.yaml` with `redirect` routes. No code, no database,
|
|
37
|
-
runs anywhere, including Vercel, AWS Lambda and Cloudflare Workers.
|
|
38
|
-
Thousands of rows import from CSV or provider files with `bulk-import`.
|
|
39
|
-
2. **Responses, pages and files.** `respond`, `page`, `static` and `download`
|
|
40
|
-
handlers, `site` conventions (robots, sitemap, favicon, security.txt,
|
|
41
|
-
llms.txt) and `policies` (throttle, agents, security headers, compression,
|
|
42
|
-
cache). Still no code.
|
|
43
|
-
3. **Functions and middleware.** `function` routes and ordered `middleware`
|
|
44
|
-
in JavaScript, trusted and in-process by default; a route declaring
|
|
45
|
-
`sandbox: true` runs isolated instead (QuickJS inside WebAssembly, fresh
|
|
46
|
-
heap per call, no Node, filesystem or network). The `env`/`secrets` the
|
|
47
|
-
runtime injects into a function come only from an operator grant pinned to
|
|
48
|
-
the project revision; the grant governs that injected context, not the
|
|
49
|
-
ambient Node environment trusted in-process code can reach on its own.
|
|
50
|
-
4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
|
|
51
|
-
page and protected routes. The operator installs it in a host file outside
|
|
52
|
-
the project; YAML only declares the mount and configuration.
|
|
53
|
-
5. **Administration.** The `admin` extension on the same service: manage the
|
|
54
|
-
people who signed up, their sessions and roles, review the audit trail.
|
|
55
|
-
|
|
56
|
-
6. **Your own look.** A shared `presentation` (catalogue and theme variables)
|
|
57
|
-
restyles auth and admin together; the `ui` extension adds the template kit,
|
|
58
|
-
project copy, template and stylesheet overrides for kit-rendered pages.
|
|
59
|
-
|
|
60
|
-
Stored short links previously sat here as a native `link` route; that handler
|
|
61
|
-
was removed from core. A `urlcode-dynamic-link` package owned them the same way
|
|
62
|
-
`auth`/`admin` own their mounts, but it has been retired and unpublished; no
|
|
63
|
-
package occupies this rung today.
|
|
64
|
-
|
|
65
|
-
Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
|
|
66
|
-
packages installed from npm and a Node host with a
|
|
67
|
-
patched SQLite build; see each package's README ([auth](../packages/auth/README.md),
|
|
68
|
-
[admin](../packages/admin/README.md), [ui](../packages/ui/README.md)) for the
|
|
69
|
-
exact requirement.
|
|
70
|
-
|
|
71
|
-
## The composition contract
|
|
72
|
-
|
|
73
|
-
After the aligned `0.4.1` packages are published, an extended project starts
|
|
74
|
-
with the packages and one command:
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
npm install --save-exact @jimhoyd/urlcode@0.4.1 @jimhoyd/urlcode-ui@0.4.1 @jimhoyd/urlcode-auth@0.4.1 @jimhoyd/urlcode-admin@0.4.1
|
|
78
|
-
urlcode init my-site --with ui,auth,admin
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
|
|
82
|
-
local tarballs from a reviewed checkout for operators who install only source
|
|
83
|
-
they have read — one revision now covers core and every extension. Three files make an extended
|
|
84
|
-
project. Nothing else is discovered by convention.
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
site/
|
|
88
|
-
urlcode.yaml the project: routes, extensions, policies (Git-owned, untrusted content)
|
|
89
|
-
functions/, public/ guest code and assets referenced from the YAML
|
|
90
|
-
operator/
|
|
91
|
-
host.mjs trusted operator code: default-exports { extensions, plugins?, close? }
|
|
92
|
-
operator-service.mjs opens the auth store, keys and senders; imported by host.mjs
|
|
93
|
-
data/ private: auth.sqlite, encryption key, CSRF key
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
The project declares logical extensions and exclusive mounts:
|
|
97
|
-
|
|
98
|
-
```yaml
|
|
99
|
-
version: "1"
|
|
100
|
-
extensions:
|
|
101
|
-
ui: { version: "1", config: { theme: { name: Acme, colors: { primary: "24 95% 53%" } } } }
|
|
102
|
-
auth: { version: "1", config: { registration: "off" } }
|
|
103
|
-
admin: { version: "1", config: {} }
|
|
104
|
-
routes:
|
|
105
|
-
/assets/ui/*: { extension: ui, methods: [GET, HEAD] }
|
|
106
|
-
/account/*: { extension: auth, methods: [GET, HEAD, POST] }
|
|
107
|
-
/admin/*: { extension: admin, methods: [GET, HEAD, POST] }
|
|
108
|
-
/private:
|
|
109
|
-
respond: { text: Signed in }
|
|
110
|
-
policies:
|
|
111
|
-
extensions:
|
|
112
|
-
auth: {}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The operator host explicitly registers the packages. Registration is an
|
|
116
|
-
activation boundary; it does not isolate trusted application code from the host:
|
|
117
|
-
|
|
118
|
-
```js
|
|
119
|
-
import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
|
|
120
|
-
import { authExtension, createPresentation, englishCatalogue } from '@jimhoyd/urlcode-auth';
|
|
121
|
-
import { adminExtension } from '@jimhoyd/urlcode-admin';
|
|
122
|
-
import { service, csrfKey, projectSha256 } from './operator-service.mjs';
|
|
123
|
-
|
|
124
|
-
const ui = createUiExtension({ projectSha256, projectRoot: '/absolute/site', sources: [englishCatalogue] });
|
|
125
|
-
const presentation = createPresentation({ theme: { '--ui-accent': '#0645ad' } });
|
|
126
|
-
export default {
|
|
127
|
-
extensions: [
|
|
128
|
-
ui.registration,
|
|
129
|
-
authExtension({ service, csrfKey, projectSha256, presentation }),
|
|
130
|
-
adminExtension({ service, csrfKey, projectSha256, authMount: '/account', presentation }),
|
|
131
|
-
],
|
|
132
|
-
async close() { await service.close(); },
|
|
133
|
-
};
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Auth and admin already render their screens through `ui.kit` when the host
|
|
137
|
-
supplies the UI extension; their package-owned templates and catalogues must be
|
|
138
|
-
registered with that kit. Without it, both retain shared primitive rendering
|
|
139
|
-
through `presentation`. The example above uses that primitive fallback. The UI
|
|
140
|
-
block is optional, and a migration to the kit is not unfinished framework work.
|
|
141
|
-
See each package README for its complete kit registration example.
|
|
142
|
-
|
|
143
|
-
```sh
|
|
144
|
-
urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
`urlcode init <dir> --with ui,auth,admin` writes this layout in one step: it
|
|
148
|
-
resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
|
|
149
|
-
calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
|
|
150
|
-
one `host.mjs` and one `README.md`, refusing before writing anything when a
|
|
151
|
-
package is missing or two fragments collide (the contract is documented under
|
|
152
|
-
[scaffolding](EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
|
|
153
|
-
and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
|
|
154
|
-
administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
|
|
155
|
-
the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
|
|
156
|
-
or mounts changes the revision and needs an explicit operator reapproval.
|
|
157
|
-
|
|
158
|
-
The presentation tooling composes the same way, by naming packages rather than
|
|
159
|
-
depending on them. `urlcode-ui` is the kit alone until
|
|
160
|
-
`--extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin` names the packages
|
|
161
|
-
that ship the other namespaces: each is resolved from `--project` with Node
|
|
162
|
-
package resolution and imported for the namespace it exports, so `list`,
|
|
163
|
-
`doctor`, `eject`, `preview` and `copy --missing` cover the `auth/*` and
|
|
164
|
-
`admin/*` templates and copy the host registers, and a project override of an
|
|
165
|
-
extension template is checked against the shipped view model. A package that is
|
|
166
|
-
not installed is skipped. `urlcode init --with` writes the commands with the
|
|
167
|
-
flag already set; `@jimhoyd/urlcode-ui` still depends on neither peer.
|
|
168
|
-
|
|
169
|
-
## Rules an agent must follow
|
|
170
|
-
|
|
171
|
-
These are the facts that keep generated projects valid. The full matrix is in
|
|
172
|
-
[AI authoring](AI-AUTHORING.md); this is the short list.
|
|
173
|
-
|
|
174
|
-
- **Extension YAML names logical extensions, not host packages or credentials.**
|
|
175
|
-
Function and middleware `source` fields do name project modules. Extensions are
|
|
176
|
-
logical names; the host file chooses the implementation. There is no
|
|
177
|
-
`--extension` flag, no `import` in YAML, no interpolation.
|
|
178
|
-
- **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
|
|
179
|
-
`function`, `proxy`, `conditional` or `extension`, plus optional
|
|
180
|
-
`middleware`. Paths are exact or single-segment `{param}`; `/*` only on
|
|
181
|
-
`static` and `extension` mounts. No regex.
|
|
182
|
-
- **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
|
|
183
|
-
It runs in-process with full Node access unless the route declares
|
|
184
|
-
`sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
|
|
185
|
-
subset, validated `args` and granted `env`, with no `fetch`, Node,
|
|
186
|
-
filesystem or general network access; bounded timers are available. Either way, `args`/`env`/`secrets` are exactly what
|
|
187
|
-
the route declares and an operator grants — trust changes where code runs,
|
|
188
|
-
not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
|
|
189
|
-
docs/FUNCTION-SECURITY.md.
|
|
190
|
-
- **Authentication is host processing.** Do not build login forms, session
|
|
191
|
-
cookies or password checks in functions. With the auth extension declared,
|
|
192
|
-
prefer `auth: true` or `auth: {role: admin}`; these expand to
|
|
193
|
-
`policies.extensions.auth`. The runtime filters credential headers passed to
|
|
194
|
-
application handlers. This is not a security boundary against trusted Node code.
|
|
195
|
-
- **Everything is validated before it runs.** `urlcode validate --local`,
|
|
196
|
-
`urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
|
|
197
|
-
with the route named; nothing degrades silently.
|
|
198
|
-
- **Provider targets refuse what they cannot enforce.** Cloudflare runs
|
|
199
|
-
redirects and declared responses only. Serverless adapters refuse functions,
|
|
200
|
-
proxy, signals and extensions. The `static` target (S3 + CloudFront,
|
|
201
|
-
no server) refuses everything that needs request-time logic, keeping only
|
|
202
|
-
`redirect`/`respond`/`page`/`static`/`download` — see [static
|
|
203
|
-
hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
|
|
204
|
-
promising a deployment.
|
|
205
|
-
- **Report evidence, not hope.** The commands above are the evidence. Local
|
|
206
|
-
tests are not deployment, soak or independent security review.
|
|
207
|
-
|
|
208
|
-
## Where to read next
|
|
209
|
-
|
|
210
|
-
| Need | Read |
|
|
211
|
-
|---|---|
|
|
212
|
-
| Write or change routes | [YAML guide](YAML-GUIDE.md), [field reference](YAML-REFERENCE.md), [cookbook](../examples/cookbook/README.md) |
|
|
213
|
-
| Add accounts | [auth README](../packages/auth/README.md), [auth security](../packages/auth/SECURITY.md) |
|
|
214
|
-
| Add administration | [admin README](../packages/admin/README.md) |
|
|
215
|
-
| Restyle every page | [ui README](../packages/ui/README.md), [ui contract](../packages/ui/CONTRACT.md) |
|
|
216
|
-
| Write an extension | [extensions](EXTENSIONS.md), [extension model review](archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
|
|
217
|
-
| Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |
|