@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3
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 +36 -22
- package/.claude/skills/urlcode-operations/SKILL.md +16 -22
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +20 -16
- package/README.md +83 -80
- package/ROADMAP.md +61 -331
- package/SECURITY.md +23 -8
- package/dist/BUILD-MANIFEST.json +42 -47
- package/dist/agents-guide.js +18 -18
- package/dist/build-static.js +136 -0
- package/dist/capabilities.js +85 -31
- package/dist/capability-query.js +0 -1
- package/dist/cli.js +24 -32
- package/dist/compliance-rules/baseline.js +2 -10
- package/dist/compliance-rules/privacy.js +5 -16
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance.js +6 -8
- package/dist/config.js +15 -8
- package/dist/context.js +9 -10
- package/dist/examples.js +2 -2
- package/dist/explain-cli.js +7 -5
- package/dist/explain.js +10 -5
- package/dist/extensions.js +61 -2
- package/dist/function-sources.js +34 -2
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +3 -6
- package/dist/manifest.js +11 -7
- package/dist/mcp-authoring.js +2 -2
- package/dist/mcp.js +1 -1
- package/dist/observability.js +1 -21
- package/dist/policies/cache.js +8 -3
- package/dist/policies.js +3 -1
- package/dist/policy.js +28 -9
- package/dist/prerender.js +4 -0
- package/dist/project-tests.js +3 -3
- package/dist/readiness.js +35 -9
- package/dist/route-diff.js +12 -5
- package/dist/router.js +5 -7
- package/dist/runtime.js +77 -58
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/tooling.js +1 -1
- package/dist/trusted-functions.js +210 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/capabilities.d.ts +14 -5
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/config.d.ts +2 -1
- package/dist/types/context.d.ts +2 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/explain.d.ts +6 -0
- package/dist/types/extensions.d.ts +55 -0
- package/dist/types/function-sources.d.ts +4 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +3 -6
- package/dist/types/manifest.d.ts +5 -3
- package/dist/types/observability.d.ts +1 -14
- package/dist/types/project-tests.d.ts +1 -2
- package/dist/types/readiness.d.ts +12 -3
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +0 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/server.d.ts +1 -4
- package/dist/types/tooling.d.ts +3 -3
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +22 -8
- package/dist/types/verify-deployment.d.ts +2 -2
- package/dist/types.js +25 -5
- package/dist/typescript-authoring.js +67 -18
- package/dist/verify-deployment.js +3 -3
- package/docs/AI-AUTHORING.md +109 -10
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +17 -8
- package/docs/CAPABILITIES.md +30 -17
- package/docs/CAPACITY.md +128 -31
- package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
- package/docs/CI.md +8 -3
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
- package/docs/COMPLIANCE.md +6 -9
- package/docs/DEPLOYMENT-CHECKS.md +1 -1
- package/docs/DEVELOPMENT-PIPELINE.md +181 -0
- package/docs/EXTENSIONS.md +193 -11
- package/docs/FRAMEWORK.md +53 -33
- package/docs/FUNCTION-SECURITY.md +173 -32
- package/docs/INSTALL.md +0 -5
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/MIDDLEWARE.md +27 -16
- package/docs/MONITORING.md +2 -19
- package/docs/OBSERVABILITY.md +7 -16
- package/docs/OPEN-DECISIONS.md +184 -0
- package/docs/OPERATIONAL-PROOF.md +26 -30
- package/docs/OPERATIONS.md +23 -32
- package/docs/POLICIES.md +27 -8
- package/docs/PRERENDER.md +25 -13
- package/docs/PROJECT-DIRECTION.md +35 -10
- package/docs/READINESS.md +6 -2
- package/docs/README.md +20 -41
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-READINESS.md +50 -30
- package/docs/RELEASE-SECURITY.md +72 -86
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +7 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +42 -2
- package/docs/SPECIFICATION.md +63 -27
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
- package/docs/SPIKE-CORE-LAYERING.md +368 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
- package/docs/STARTERS.md +1 -1
- package/docs/STATIC.md +105 -0
- package/docs/TOOLING.md +17 -12
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +32 -12
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +46 -0
- package/docs/YAML-GUIDE.md +0 -3
- package/docs/YAML-REFERENCE.md +16 -6
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
- package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
- package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
- package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
- package/docs/archive/2026-09-19/ROADMAP.md +386 -0
- package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
- package/docs/archive/README.md +27 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/examples/assets/example.yaml +2 -2
- package/examples/aws/example.yaml +1 -1
- package/examples/cloudflare/example.yaml +1 -1
- package/examples/conditions/example.yaml +1 -1
- package/examples/cookbook/example.yaml +1 -1
- package/examples/cookbook/middleware/bucket.mjs +12 -2
- package/examples/cookbook/middleware/locale.mjs +7 -3
- package/examples/cookbook/route-index.json +1 -1
- package/examples/cookbook/routes/middleware.yaml +1 -1
- package/examples/egress/example.yaml +1 -1
- package/examples/extensions/example.yaml +1 -1
- package/examples/prerender/README.md +14 -6
- package/examples/prerender/example.yaml +1 -1
- package/examples/prerender/functions/page.mjs +4 -2
- package/examples/prerender/middleware/template.mjs +1 -1
- package/examples/prerender/prerender.mjs +1 -1
- package/examples/prerender/urlcode.yaml +8 -4
- package/examples/provider-conformance/example.yaml +1 -1
- package/examples/vercel/example.yaml +1 -1
- package/llms-full.txt +686 -239
- package/llms.txt +27 -15
- package/package.json +32 -5
- package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
- package/recipes/authenticated-json-api/README.md +4 -3
- package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
- package/recipes/authenticated-json-api/recipe.yaml +2 -2
- package/recipes/contact-form/functions/contact.mjs +2 -1
- package/recipes/contact-form/recipe.yaml +3 -3
- package/recipes/cors-api/README.md +2 -2
- package/recipes/cors-api/recipe.yaml +2 -2
- package/recipes/health-page/README.md +1 -1
- package/recipes/health-page/recipe.yaml +1 -1
- package/recipes/json-api/README.md +1 -1
- package/recipes/json-api/recipe.yaml +4 -4
- package/recipes/middleware/README.md +8 -4
- package/recipes/middleware/middleware/bucket.mjs +12 -2
- package/recipes/middleware/middleware/locale.mjs +7 -3
- package/recipes/middleware/recipe.yaml +1 -1
- package/recipes/protected-download/README.md +1 -1
- package/recipes/protected-download/recipe.yaml +2 -2
- package/recipes/redirect/recipe.yaml +1 -1
- package/recipes/static-plus-api/README.md +2 -2
- package/recipes/static-plus-api/public/index.html +1 -1
- package/recipes/static-plus-api/recipe.yaml +2 -2
- package/recipes/static-plus-api/urlcode.yaml +1 -1
- package/recipes/typescript/README.md +2 -1
- package/recipes/typescript/recipe.yaml +5 -5
- package/recipes/webhook-receiver/README.md +5 -1
- package/recipes/webhook-receiver/recipe.yaml +2 -1
- package/recipes/webhook-receiver/urlcode.yaml +7 -0
- package/schemas/recipe.schema.json +4 -3
- package/schemas/urlcode.schema.json +14 -41
- package/skills/urlcode/SKILL.md +32 -11
- package/starters/default/AGENTS.md +18 -18
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -40
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -37
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -8
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -61
- package/docs/links/cli.md +0 -110
- package/docs/links/limits.md +0 -175
- package/docs/links/management-api.md +0 -80
- package/docs/links/pools.md +0 -75
- package/docs/links/setup.md +0 -135
- package/docs/yaml/links.md +0 -30
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/example.yaml +0 -21
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
package/docs/EXTENSIONS.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# Operator-installed extensions
|
|
2
2
|
|
|
3
|
-
Extensions are trusted operator modules, separate from
|
|
3
|
+
Extensions are trusted operator modules, separate from a project's own
|
|
4
|
+
`function`/`middleware` code. Auth
|
|
4
5
|
and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
|
|
5
6
|
supplies only the generic integration contract. No project file can import a host
|
|
6
7
|
extension or choose its npm package.
|
|
7
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
|
+
|
|
8
15
|
A project declares versioned configuration and exclusive route mounts:
|
|
9
16
|
|
|
10
17
|
```yaml
|
|
@@ -52,9 +59,9 @@ fails, naming the route, when `auth` appears without an `extensions.auth`
|
|
|
52
59
|
declaration, next to `policies.extensions.auth`, or next to
|
|
53
60
|
`policies.extensions: false`.
|
|
54
61
|
|
|
55
|
-
The same shape is
|
|
56
|
-
maxAge}`
|
|
57
|
-
|
|
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)).
|
|
58
65
|
|
|
59
66
|
The configuration and requirement objects above are validated by the installed
|
|
60
67
|
extension's schemas. They are examples of extension-owned fields, not built-in
|
|
@@ -71,17 +78,86 @@ project and place the exact returned SHA-256 in each registration's
|
|
|
71
78
|
in the revision. Changing them requires an explicit operator reapproval.
|
|
72
79
|
|
|
73
80
|
Registrations provide a name, contract version, target list, JSON configuration
|
|
74
|
-
schema, optional policy schema
|
|
81
|
+
schema, optional policy schema, an optional declared `cacheSensitive` (below)
|
|
82
|
+
and activation factory. Activation receives the
|
|
75
83
|
canonical operator origin, target, revision and mount bases. Its instance handles
|
|
76
|
-
bounded requests and, when
|
|
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
|
|
77
87
|
registrations, stale grants, invalid configuration and unsupported targets fail
|
|
78
88
|
activation. Multiple mounts cannot overlap other declared routes.
|
|
79
89
|
|
|
80
90
|
For extension-protected routes, agents/throttle run before authorization and
|
|
81
|
-
cache access happens only after authorization.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
85
161
|
|
|
86
162
|
One exception exists for content-hashed assets. A registration may declare
|
|
87
163
|
`immutableAssets: {prefix: '/static'}`, a normalized literal path under each
|
|
@@ -105,6 +181,112 @@ HTML/JS on an authentication origin must be trusted by that site's operator.
|
|
|
105
181
|
Cloudflare refuses extensions until its artifact format supports their execution.
|
|
106
182
|
Node adapter conformance is not a live-provider deployment claim.
|
|
107
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](https://github.com/jimhoyd-com/urlcode-auth/blob/main/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
|
+
|
|
108
290
|
## Discovering schemas
|
|
109
291
|
|
|
110
292
|
Each registration carries the JSON Schemas that validate its `config` block and
|
|
@@ -161,7 +343,7 @@ and each extension's own operator files. Core never bundles or imports the
|
|
|
161
343
|
extension packages at build time; at run time it resolves
|
|
162
344
|
`@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
|
|
163
345
|
the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
|
|
164
|
-
directory, from npm where the packages are published as `0.1.0-alpha.
|
|
346
|
+
directory, from npm where the packages are published as `0.1.0-alpha.x`
|
|
165
347
|
prereleases, is the normal path and what makes `--with auth` work), imports
|
|
166
348
|
the package and calls its
|
|
167
349
|
`scaffold` export with this request:
|
package/docs/FRAMEWORK.md
CHANGED
|
@@ -9,14 +9,14 @@ claim here is implemented in the linked repository; nothing is roadmap.
|
|
|
9
9
|
|
|
10
10
|
| Package | Repository | What it adds | How a project declares it |
|
|
11
11
|
|---|---|---|---|
|
|
12
|
-
| `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes,
|
|
12
|
+
| `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | 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
13
|
| `@jimhoyd/urlcode-ui` | [urlcode-ui](https://github.com/jimhoyd-com/urlcode-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
14
|
| `@jimhoyd/urlcode-auth` | [urlcode-auth](https://github.com/jimhoyd-com/urlcode-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
15
|
| `@jimhoyd/urlcode-admin` | [urlcode-admin](https://github.com/jimhoyd-com/urlcode-admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
|
|
16
16
|
|
|
17
17
|
The core is Apache-2.0 and released. The three extension packages are
|
|
18
|
-
Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.
|
|
19
|
-
`@jimhoyd/urlcode-auth@0.1.0-alpha.
|
|
18
|
+
Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.4`,
|
|
19
|
+
`@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
|
|
20
20
|
on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
|
|
21
21
|
endorsement: the source is complete, but independent review, deployment
|
|
22
22
|
evidence and an accessibility assessment are still pending
|
|
@@ -24,6 +24,9 @@ evidence and an accessibility assessment are still pending
|
|
|
24
24
|
files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
|
|
25
25
|
[admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
|
|
26
26
|
[ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
|
|
27
|
+
Which core version each package supports, how it declares that, and the order
|
|
28
|
+
in which a core change reaches the downstream repositories are recorded in
|
|
29
|
+
[core version alignment](VERSION-ALIGNMENT.md).
|
|
27
30
|
|
|
28
31
|
## The ladder
|
|
29
32
|
|
|
@@ -38,22 +41,27 @@ Each rung's YAML is valid on every rung above it.
|
|
|
38
41
|
llms.txt) and `policies` (throttle, agents, security headers, compression,
|
|
39
42
|
cache). Still no code.
|
|
40
43
|
3. **Functions and middleware.** `function` routes and ordered `middleware`
|
|
41
|
-
in
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
5. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
|
|
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). Secrets reach a function
|
|
47
|
+
only through an operator grant pinned to the project revision.
|
|
48
|
+
4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
|
|
47
49
|
page and protected routes. The operator installs it in a host file outside
|
|
48
50
|
the project; YAML only declares the mount and configuration.
|
|
49
|
-
|
|
51
|
+
5. **Administration.** The `admin` extension on the same service: manage the
|
|
50
52
|
people who signed up, their sessions and roles, review the audit trail.
|
|
51
|
-
|
|
53
|
+
|
|
54
|
+
6. **Your own look.** A shared `presentation` (catalogue and theme variables)
|
|
52
55
|
restyles auth and admin together; the `ui` extension adds the template kit,
|
|
53
56
|
project copy, template and stylesheet overrides for kit-rendered pages.
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
Stored short links previously sat here as a native `link` route; that handler
|
|
59
|
+
was removed from core. A `urlcode-dynamic-link` package owned them the same way
|
|
60
|
+
`auth`/`admin` own their mounts, but it has been retired and unpublished; no
|
|
61
|
+
package occupies this rung today.
|
|
62
|
+
|
|
63
|
+
Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
|
|
64
|
+
packages, installed from npm as `0.1.0-alpha.x` prereleases, and a Node host
|
|
57
65
|
with a patched SQLite build; see each repository's README for the exact
|
|
58
66
|
requirement.
|
|
59
67
|
|
|
@@ -66,9 +74,9 @@ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/
|
|
|
66
74
|
urlcode init my-site --with auth,admin
|
|
67
75
|
```
|
|
68
76
|
|
|
69
|
-
Installing from npm is the normal path;
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
|
|
78
|
+
local tarballs from a reviewed checkout for operators who install only source
|
|
79
|
+
they have read — one revision now covers core and every extension. Three files make an extended
|
|
72
80
|
project. Nothing else is discovered by convention.
|
|
73
81
|
|
|
74
82
|
```
|
|
@@ -100,7 +108,8 @@ routes:
|
|
|
100
108
|
auth: {}
|
|
101
109
|
```
|
|
102
110
|
|
|
103
|
-
The operator host
|
|
111
|
+
The operator host explicitly registers the packages. Registration is an
|
|
112
|
+
activation boundary; it does not isolate trusted application code from the host:
|
|
104
113
|
|
|
105
114
|
```js
|
|
106
115
|
import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
|
|
@@ -120,12 +129,12 @@ export default {
|
|
|
120
129
|
};
|
|
121
130
|
```
|
|
122
131
|
|
|
123
|
-
Auth and admin
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
Auth and admin already render their screens through `ui.kit` when the host
|
|
133
|
+
supplies the UI extension; their package-owned templates and catalogues must be
|
|
134
|
+
registered with that kit. Without it, both retain shared primitive rendering
|
|
135
|
+
through `presentation`. The example above uses that primitive fallback. The UI
|
|
136
|
+
block is optional, and a migration to the kit is not unfinished framework work.
|
|
137
|
+
See each package README for its complete kit registration example.
|
|
129
138
|
|
|
130
139
|
```sh
|
|
131
140
|
urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
|
|
@@ -147,26 +156,37 @@ or mounts changes the revision and needs an explicit operator reapproval.
|
|
|
147
156
|
These are the facts that keep generated projects valid. The full matrix is in
|
|
148
157
|
[AI authoring](AI-AUTHORING.md); this is the short list.
|
|
149
158
|
|
|
150
|
-
- **YAML
|
|
159
|
+
- **Extension YAML names logical extensions, not host packages or credentials.**
|
|
160
|
+
Function and middleware `source` fields do name project modules. Extensions are
|
|
151
161
|
logical names; the host file chooses the implementation. There is no
|
|
152
162
|
`--extension` flag, no `import` in YAML, no interpolation.
|
|
153
163
|
- **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
|
|
154
|
-
`function`, `
|
|
164
|
+
`function`, `proxy`, `conditional` or `extension`, plus optional
|
|
155
165
|
`middleware`. Paths are exact or single-segment `{param}`; `/*` only on
|
|
156
166
|
`static` and `extension` mounts. No regex.
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
- **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
|
|
168
|
+
It runs in-process with full Node access unless the route declares
|
|
169
|
+
`sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
|
|
170
|
+
subset, validated `args` and granted `env`, with no `fetch`, Node,
|
|
171
|
+
filesystem or general network access; bounded timers are available. Either way, `args`/`env`/`secrets` are exactly what
|
|
172
|
+
the route declares and an operator grants — trust changes where code runs,
|
|
173
|
+
not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
|
|
174
|
+
docs/FUNCTION-SECURITY.md.
|
|
160
175
|
- **Authentication is host processing.** Do not build login forms, session
|
|
161
|
-
cookies or password checks in functions.
|
|
162
|
-
|
|
176
|
+
cookies or password checks in functions. With the auth extension declared,
|
|
177
|
+
prefer `auth: true` or `auth: {role: admin}`; these expand to
|
|
178
|
+
`policies.extensions.auth`. The runtime filters credential headers passed to
|
|
179
|
+
application handlers. This is not a security boundary against trusted Node code.
|
|
163
180
|
- **Everything is validated before it runs.** `urlcode validate --local`,
|
|
164
181
|
`urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
|
|
165
182
|
with the route named; nothing degrades silently.
|
|
166
183
|
- **Provider targets refuse what they cannot enforce.** Cloudflare runs
|
|
167
184
|
redirects and declared responses only. Serverless adapters refuse functions,
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
proxy, signals and extensions. The `static` target (S3 + CloudFront,
|
|
186
|
+
no server) refuses everything that needs request-time logic, keeping only
|
|
187
|
+
`redirect`/`respond`/`page`/`static`/`download` — see [static
|
|
188
|
+
hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
|
|
189
|
+
promising a deployment.
|
|
170
190
|
- **Report evidence, not hope.** The commands above are the evidence. Local
|
|
171
191
|
tests are not deployment, soak or independent security review.
|
|
172
192
|
|
|
@@ -178,5 +198,5 @@ These are the facts that keep generated projects valid. The full matrix is in
|
|
|
178
198
|
| Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode-auth#readme), [auth security](https://github.com/jimhoyd-com/urlcode-auth/blob/main/SECURITY.md) |
|
|
179
199
|
| Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
|
|
180
200
|
| Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode-ui#readme), [ui contract](https://github.com/jimhoyd-com/urlcode-ui/blob/main/CONTRACT.md) |
|
|
181
|
-
| Write an extension | [extensions](EXTENSIONS.md), [extension model review](SPIKE-EXTENSION-MODEL.md) |
|
|
201
|
+
| Write an extension | [extensions](EXTENSIONS.md), [extension model review](archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
|
|
182
202
|
| Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |
|