@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.2
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 +24 -8
- package/.claude/skills/urlcode-operations/SKILL.md +10 -16
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +75 -62
- package/ROADMAP.md +61 -16
- package/SECURITY.md +23 -8
- package/dist/BUILD-MANIFEST.json +38 -43
- package/dist/agents-guide.js +18 -18
- package/dist/build-static.js +134 -0
- package/dist/capabilities.js +85 -31
- package/dist/capability-query.js +0 -1
- package/dist/cli.js +23 -31
- 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 +3 -3
- package/dist/explain.js +5 -6
- 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 +5 -7
- package/dist/mcp.js +1 -1
- package/dist/observability.js +1 -21
- package/dist/policies/cache.js +8 -3
- package/dist/policy.js +28 -9
- package/dist/project-tests.js +3 -3
- package/dist/readiness.js +32 -9
- 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/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 +1 -3
- package/dist/types/observability.d.ts +1 -14
- package/dist/types/project-tests.d.ts +1 -2
- package/dist/types/readiness.d.ts +10 -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 +14 -7
- package/dist/types/verify-deployment.d.ts +2 -2
- package/dist/types.js +17 -4
- package/dist/typescript-authoring.js +55 -17
- package/dist/verify-deployment.js +3 -3
- package/docs/AI-AUTHORING.md +104 -9
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +3 -2
- package/docs/CAPABILITIES.md +29 -16
- package/docs/CAPACITY.md +128 -31
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/COMPLIANCE.md +6 -9
- package/docs/DEPLOYMENT-CHECKS.md +1 -1
- package/docs/EXTENSIONS.md +183 -11
- package/docs/FRAMEWORK.md +35 -19
- package/docs/FUNCTION-SECURITY.md +129 -32
- package/docs/INSTALL.md +0 -5
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/MIDDLEWARE.md +27 -16
- package/docs/MONITORING.md +2 -19
- package/docs/NEXT-PHASE-PLAN.md +12 -4
- package/docs/NEXT-STEPS.md +259 -40
- package/docs/OBSERVABILITY.md +7 -16
- package/docs/OPEN-DECISIONS.md +212 -0
- package/docs/OPERATIONAL-PROOF.md +26 -30
- package/docs/OPERATIONS.md +23 -32
- package/docs/POLICIES.md +15 -4
- package/docs/PRERENDER.md +2 -2
- package/docs/PROJECT-DIRECTION.md +34 -9
- package/docs/README.md +13 -11
- package/docs/RELEASE-READINESS.md +49 -29
- package/docs/RELEASE-SECURITY.md +89 -3
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +8 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +40 -0
- package/docs/SPECIFICATION.md +60 -26
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +8 -0
- package/docs/SPIKE-CORE-LAYERING.md +337 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +2 -2
- package/docs/SPIKE-EXTENSIONS.md +5 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +4 -2
- package/docs/SPIKE-MONOREPO.md +322 -0
- package/docs/STARTERS.md +1 -1
- package/docs/STATIC.md +94 -0
- package/docs/TOOLING.md +13 -9
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +27 -12
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/USABILITY-REVIEW.md +7 -1
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +205 -0
- package/docs/YAML-GUIDE.md +0 -3
- package/docs/YAML-REFERENCE.md +16 -6
- package/docs/policies/cache.md +13 -0
- package/examples/assets/example.yaml +1 -1
- 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/egress/example.yaml +1 -1
- package/examples/extensions/example.yaml +1 -1
- package/examples/prerender/README.md +2 -2
- package/examples/prerender/example.yaml +1 -1
- package/examples/provider-conformance/example.yaml +1 -1
- package/examples/vercel/example.yaml +1 -1
- package/llms-full.txt +594 -219
- package/llms.txt +23 -12
- package/package.json +10 -3
- package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +24 -8
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +10 -16
- package/recipes/authenticated-json-api/recipe.yaml +1 -1
- package/recipes/contact-form/recipe.yaml +1 -1
- package/recipes/cors-api/recipe.yaml +1 -1
- package/recipes/health-page/recipe.yaml +1 -1
- package/recipes/json-api/recipe.yaml +1 -1
- 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/recipe.yaml +1 -1
- package/recipes/redirect/recipe.yaml +1 -1
- package/recipes/static-plus-api/recipe.yaml +1 -1
- package/recipes/typescript/README.md +2 -1
- package/recipes/typescript/recipe.yaml +1 -1
- 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 +29 -8
- 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/MANAGEMENT-SECURITY.md +0 -82
- 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/CLOUDFLARE.md
CHANGED
|
@@ -21,8 +21,7 @@ route pattern and the reason named:
|
|
|
21
21
|
|
|
22
22
|
| Handler | Why it is refused |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| `function`, `middleware` | need worker threads and the QuickJS WASM engine |
|
|
25
|
-
| `link` | needs a durable writable store the platform does not provide here |
|
|
24
|
+
| `function`, `middleware` | need the self-hosted Node lifecycle, and worker threads and the QuickJS WASM engine when a route declares `sandbox: true` |
|
|
26
25
|
| `page`, `static`, `download` | need a platform static-asset binding, not an inline copy |
|
|
27
26
|
| `env`, `secrets` | would have to be baked into the artifact |
|
|
28
27
|
|
package/docs/COMPLIANCE.md
CHANGED
|
@@ -49,11 +49,11 @@ A **project** rule runs once with:
|
|
|
49
49
|
|---|---|
|
|
50
50
|
| `document` | The parsed and validated `urlcode.yaml` (includes are merged into `routes`) |
|
|
51
51
|
| `routes` | Route configuration by pattern, as written in YAML |
|
|
52
|
-
| `plan` | `testPlan()`: `inventory[]` (`path`, `handler`, `methods`, `middleware`, `policies`, `state`), `policies` (the per-route describe map)
|
|
52
|
+
| `plan` | `testPlan()`: `inventory[]` (`path`, `handler`, `methods`, `middleware`, `policies`, `state`), `policies` (the per-route describe map) |
|
|
53
53
|
| `policies` | `effectivePolicies(document, route)` by pattern: the merged configuration of every policy on each route |
|
|
54
54
|
| `origin` | The declared public origin, or `null` |
|
|
55
55
|
| `target` | `node` unless the caller states another |
|
|
56
|
-
| `host` | `{ requestLog
|
|
56
|
+
| `host` | `{ requestLog }` as declared for the deployment; `null` where undeclared |
|
|
57
57
|
|
|
58
58
|
A **route** rule runs once per inventory entry and additionally receives
|
|
59
59
|
`route` (the inventory entry), `config` (that route's YAML), `policy` (the
|
|
@@ -86,7 +86,6 @@ cache and compression secrets handling in `src/policies/cache.ts` and
|
|
|
86
86
|
| `rfc6585/throttle-functions` | [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585) §4 | medium | Every active function or middleware route has an effective `policies.throttle` | Declare a throttle on the route or the project |
|
|
87
87
|
| `rfc9309/robots` | [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309) | low | Some active route declares `policies.agents`, or an active `/robots.txt` `respond` route exists | Add a `/robots.txt` route or an agents deny list |
|
|
88
88
|
| `rfc9110/expired-routes` | [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110) §15.5.11 | info | Lists routes past `expires` that still answer 410 | Remove them once the 410 window has served its purpose |
|
|
89
|
-
| `ops/management-private` | [Management security](MANAGEMENT-SECURITY.md) | info | Reminder when `dynamicLinks: true`: the management API and `/_urlcode` probes belong on a private bind | Run `links api` privately with an auth file; keep probes internal |
|
|
90
89
|
|
|
91
90
|
### `strict` (baseline plus)
|
|
92
91
|
|
|
@@ -102,15 +101,13 @@ cache and compression secrets handling in `src/policies/cache.ts` and
|
|
|
102
101
|
|
|
103
102
|
These rules check deployment settings, so `audit` takes `--request-log` to
|
|
104
103
|
declare the level the deployment uses (the audit process itself always logs
|
|
105
|
-
nothing)
|
|
106
|
-
false`; embedders pass `host` themselves. The references are the runtime's
|
|
104
|
+
nothing); embedders pass `host` themselves. The references are the runtime's
|
|
107
105
|
own [logging guarantees](MONITORING.md): records carry no URL, query, header,
|
|
108
106
|
body or binding, and `detailed` adds only the method and route pattern.
|
|
109
107
|
|
|
110
108
|
| Rule | Standard | Severity | Checks | Remediation |
|
|
111
109
|
|---|---|---|---|---|
|
|
112
110
|
| `privacy/request-log-minimal` | [Monitoring](MONITORING.md), Log records | medium (`info` when undeclared) | `host.requestLog` is `minimal` | Use the default log unless per-route rates are required |
|
|
113
|
-
| `privacy/link-events-off` | [Monitoring](MONITORING.md), The link event channel | medium; `high` with `includeCode` (`info` when undeclared and `dynamicLinks` is on) | The link event channel is off, or on without `includeCode` | Enable it only for a declared purpose; never disclose codes |
|
|
114
111
|
| `privacy/detailed-log-parameters` | [Monitoring](MONITORING.md), Log records | low | With `detailed` logging, no active route takes parameters (records name the pattern and method, never values) | Keep `minimal` on parameterised deployments |
|
|
115
112
|
|
|
116
113
|
## Writing custom rules
|
|
@@ -159,9 +156,9 @@ Without any compliance flag the audit report is unchanged apart from
|
|
|
159
156
|
"findings": [{ "rule": "rfc6585/throttle-functions", "severity": "medium", "route": "/hello/{name}",
|
|
160
157
|
"message": "…", "remediation": "…", "standard": { "name": "RFC 6585 …", "reference": "…", "section": "…" } }],
|
|
161
158
|
"counts": { "high": 0, "medium": 4, "low": 11, "info": 1 }, "pass": true,
|
|
162
|
-
"evidence": { "routes": 21, "active": 19, "
|
|
159
|
+
"evidence": { "routes": 21, "active": 19, "policies": ["agents", "cache", "security", "throttle"],
|
|
163
160
|
"files": ["urlcode.yaml", "routes/code.yaml"], "origin": null, "target": "node",
|
|
164
|
-
"host": { "requestLog": "minimal"
|
|
161
|
+
"host": { "requestLog": "minimal" },
|
|
165
162
|
"scope": "declared configuration and runtime facts; not a deployment or certification" } }
|
|
166
163
|
```
|
|
167
164
|
|
|
@@ -190,7 +187,7 @@ const report = await runCompliance(runtime, {
|
|
|
190
187
|
rules: [], override: {}, disable: [], // as a rules module would export them
|
|
191
188
|
ignore: ['rfc9110/expired-routes'],
|
|
192
189
|
origin: 'https://links.example',
|
|
193
|
-
host: { requestLog: 'minimal'
|
|
190
|
+
host: { requestLog: 'minimal' }, // what the deployment is configured with
|
|
194
191
|
});
|
|
195
192
|
await runtime.close();
|
|
196
193
|
```
|
|
@@ -19,7 +19,7 @@ URLs for; defaults to the target), `--expect-routes N`, `--expect-metrics`
|
|
|
19
19
|
(per request, 100-120000, default 10000), `--fail-on high|medium|low|info|none`
|
|
20
20
|
(default `high`), the `audit` compliance flags (`--compliance`,
|
|
21
21
|
`--compliance-rules`, `--compliance-ignore`, `--compliance-warn`), and the
|
|
22
|
-
usual `--policy`
|
|
22
|
+
usual `--policy` binding the local snapshot needs to start.
|
|
23
23
|
|
|
24
24
|
## What it verifies
|
|
25
25
|
|
package/docs/EXTENSIONS.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
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 are moving out of core this way too: a future
|
|
10
|
+
`urlcode-dynamic-link` package (mount-based, like `auth`/`admin`, not yet
|
|
11
|
+
published) will own the durable link store, its CLI and management API. Core
|
|
12
|
+
no longer has a native `link` handler or a `dynamicLinks` project flag.
|
|
13
|
+
|
|
8
14
|
A project declares versioned configuration and exclusive route mounts:
|
|
9
15
|
|
|
10
16
|
```yaml
|
|
@@ -52,9 +58,9 @@ fails, naming the route, when `auth` appears without an `extensions.auth`
|
|
|
52
58
|
declaration, next to `policies.extensions.auth`, or next to
|
|
53
59
|
`policies.extensions: false`.
|
|
54
60
|
|
|
55
|
-
The same shape is
|
|
56
|
-
maxAge}`
|
|
57
|
-
|
|
61
|
+
The same shape is used for the cache policy: a route-level `cache: {strategy,
|
|
62
|
+
maxAge, ...}` expands to `policies.cache` in the same pass (see
|
|
63
|
+
[policies](POLICIES.md)).
|
|
58
64
|
|
|
59
65
|
The configuration and requirement objects above are validated by the installed
|
|
60
66
|
extension's schemas. They are examples of extension-owned fields, not built-in
|
|
@@ -71,17 +77,86 @@ project and place the exact returned SHA-256 in each registration's
|
|
|
71
77
|
in the revision. Changing them requires an explicit operator reapproval.
|
|
72
78
|
|
|
73
79
|
Registrations provide a name, contract version, target list, JSON configuration
|
|
74
|
-
schema, optional policy schema
|
|
80
|
+
schema, optional policy schema, an optional declared `cacheSensitive` (below)
|
|
81
|
+
and activation factory. Activation receives the
|
|
75
82
|
canonical operator origin, target, revision and mount bases. Its instance handles
|
|
76
|
-
bounded requests and, when
|
|
83
|
+
bounded requests and, when named in a route's policies, gates the request via
|
|
84
|
+
`authorize`, wraps the rest of the pipeline via `middleware`, or both (see
|
|
85
|
+
[Wrapping a route](#wrapping-a-route-extension-middleware) above). Missing
|
|
77
86
|
registrations, stale grants, invalid configuration and unsupported targets fail
|
|
78
87
|
activation. Multiple mounts cannot overlap other declared routes.
|
|
79
88
|
|
|
80
89
|
For extension-protected routes, agents/throttle run before authorization and
|
|
81
|
-
cache access happens only after authorization.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
cache access happens only after authorization. This part is unconditional:
|
|
91
|
+
naming any extension in `policies.extensions` always runs its `authorize()`
|
|
92
|
+
(when it implements one) before the route's own handler, whatever this
|
|
93
|
+
section says next.
|
|
94
|
+
|
|
95
|
+
An `extension:` mount is always confidential: its route rejects cache
|
|
96
|
+
strategies other than no-store, and every response is forced to no-store
|
|
97
|
+
after host response hooks, with compression disabled. A `policies.extensions`
|
|
98
|
+
route (no mount, `authorize`/`middleware` only) gets the same treatment
|
|
99
|
+
**unless every extension it names explicitly declares
|
|
100
|
+
`cacheSensitive: false`** on its `RuntimeExtension` registration. That field
|
|
101
|
+
defaults to sensitive (unset or `true`): the safe default is unchanged, and
|
|
102
|
+
relaxing it is an explicit, reviewed operator opt-in an extension author
|
|
103
|
+
makes once, in host code, never inferred from a route or from a response the
|
|
104
|
+
extension happens to return. It exists for a generic, cache-transparent
|
|
105
|
+
extension whose `middleware()` is pure request/response wrapping with no
|
|
106
|
+
access-control semantics of its own (a logging or header-rewriting
|
|
107
|
+
extension, for example) — declared this way, its wrapped route keeps
|
|
108
|
+
whatever `Cache-Control` its own handler sets, exactly like the native
|
|
109
|
+
`middleware:` array already does, and compression is not disabled either. A
|
|
110
|
+
route naming more than one extension stays confidential if any one of them
|
|
111
|
+
is sensitive (or leaves the field unset); one `cacheSensitive: false`
|
|
112
|
+
extension cannot relax a route that also names a sensitive one. This can
|
|
113
|
+
only relax the no-store floor a generic extension would otherwise inherit —
|
|
114
|
+
it has no effect on `authorize()`, which runs the same way regardless, and
|
|
115
|
+
`auth`/`admin`-style extensions gating real access must leave it at the
|
|
116
|
+
default.
|
|
117
|
+
|
|
118
|
+
## Wrapping a route: extension middleware
|
|
119
|
+
|
|
120
|
+
`authorize` is a gate: it runs once, before the route's handler, and can only
|
|
121
|
+
either let the request through unchanged or answer instead of it. It cannot
|
|
122
|
+
see or change what the handler itself returns.
|
|
123
|
+
|
|
124
|
+
`middleware` is a wrap. An extension instance may implement it alongside or
|
|
125
|
+
instead of `authorize`, attached the same way, via
|
|
126
|
+
`policies.extensions.<name>` on a route (no `extension:` mount required); its
|
|
127
|
+
`config` is exactly the same per-route value `authorize`'s `requirement`
|
|
128
|
+
receives, validated once against the extension's `policySchema`:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
middleware?(config: Readonly<Record<string, unknown>>, request: ExtensionRequest,
|
|
132
|
+
next: () => Promise<HandlerResult>): HandlerResult | Promise<HandlerResult>;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`next()` invokes the rest of the pipeline for that route: any other extension
|
|
136
|
+
`middleware()` also declared on the route (see below), then the route's own
|
|
137
|
+
native `middleware:` chain and handler, dispatched through the sandboxed or
|
|
138
|
+
trusted engine exactly as it is today. Calling it lets the hook run code
|
|
139
|
+
before and after the rest of the pipeline, inspecting or mutating the
|
|
140
|
+
`HandlerResult` it resolves to — the same "add a header to whatever the
|
|
141
|
+
handler returns" shape as the native `middleware/headers.mjs` cookbook
|
|
142
|
+
recipe, but declared by an operator-installed extension instead of project
|
|
143
|
+
code. Skipping it short-circuits everything after that point, the same
|
|
144
|
+
capability `authorize` already has, just usable from either side of the
|
|
145
|
+
handler now. `next()` may be called at most once; calling it again throws.
|
|
146
|
+
|
|
147
|
+
A route naming more than one extension in `policies.extensions` chains every
|
|
148
|
+
one that implements `middleware`, in the order the keys are declared, each
|
|
149
|
+
one's `next()` reaching the next one and the innermost `next()` reaching the
|
|
150
|
+
native pipeline — the first declared name is outermost. This is purely
|
|
151
|
+
additive at the `policies.extensions` layer and never touches the native
|
|
152
|
+
`middleware:` array, its schema, or its dispatch, all of which are unchanged.
|
|
153
|
+
|
|
154
|
+
`authorize` and `middleware` compose on the same route, from the same or
|
|
155
|
+
different extensions, without special-casing: `authorize` always runs first
|
|
156
|
+
(unchanged), and any declared `middleware()` wraps everything after that
|
|
157
|
+
point, including the rest of the authorize-gated pipeline. A route naming an
|
|
158
|
+
extension via `policies.extensions` only requires that extension to
|
|
159
|
+
implement `authorize`, `middleware`, or both — never both unconditionally.
|
|
85
160
|
|
|
86
161
|
One exception exists for content-hashed assets. A registration may declare
|
|
87
162
|
`immutableAssets: {prefix: '/static'}`, a normalized literal path under each
|
|
@@ -105,6 +180,103 @@ HTML/JS on an authentication origin must be trusted by that site's operator.
|
|
|
105
180
|
Cloudflare refuses extensions until its artifact format supports their execution.
|
|
106
181
|
Node adapter conformance is not a live-provider deployment claim.
|
|
107
182
|
|
|
183
|
+
## Project-level lifecycle hooks
|
|
184
|
+
|
|
185
|
+
`authorize` and `middleware` let core call *into* an extension. They do not
|
|
186
|
+
let a project hand its own code *to* an extension to run at a defined point.
|
|
187
|
+
That gap matters once an extension has meaningful lifecycle events —
|
|
188
|
+
registration, deletion, an administrative action, a link resolution, and so
|
|
189
|
+
on. Presentation already has a standard layering mechanism for this: a
|
|
190
|
+
project customizes an extension's *look* through `urlcode-ui`'s
|
|
191
|
+
`copy`/`extra.css`/`templates` without forking it. Behavior needs the same
|
|
192
|
+
standard, or every extension author (and every project depending on one)
|
|
193
|
+
either reinvents it or forks the extension. `urlcode-auth` and
|
|
194
|
+
`urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
|
|
195
|
+
[SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
|
|
196
|
+
already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
|
|
197
|
+
|
|
198
|
+
**The pattern.** An extension with lifecycle events an author judges worth
|
|
199
|
+
exposing should let the project name its own function in the extension's own
|
|
200
|
+
`config`, using the same source shape `function`/`middleware` routes already
|
|
201
|
+
use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
|
|
202
|
+
and add its own `sandbox` boolean next to it (below). The extension's own
|
|
203
|
+
`activate()` reads that config, and its own runtime dispatch — not a new core
|
|
204
|
+
primitive, an ordinary call the extension package makes with the request
|
|
205
|
+
context it already has — invokes the named function at the lifecycle point
|
|
206
|
+
it defines, with a typed input and a typed verdict the extension's own
|
|
207
|
+
schema documents. For example, an auth-style extension might declare:
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
extensions:
|
|
211
|
+
auth:
|
|
212
|
+
version: "1"
|
|
213
|
+
config:
|
|
214
|
+
hooks:
|
|
215
|
+
beforeRegister:
|
|
216
|
+
source: ./hooks/registration-rule.mjs
|
|
217
|
+
export: default
|
|
218
|
+
onSignUp:
|
|
219
|
+
source: ./hooks/on-signup.mjs
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
with `beforeRegister` called before an account is created, given a typed
|
|
223
|
+
`{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
|
|
224
|
+
or `{allow: false, reason}`), and `onSignUp` called after, for side effects
|
|
225
|
+
such as provisioning a workspace. The exact hook names, input/verdict shapes
|
|
226
|
+
and invocation points are the extension's own design — `hooks` is not a core
|
|
227
|
+
schema key — but the source/export/sandbox shape, and the fact that this is
|
|
228
|
+
project code the extension calls rather than an operator callback in
|
|
229
|
+
`host.mjs`, should be consistent across extensions so an author who has
|
|
230
|
+
learned one has learned the pattern.
|
|
231
|
+
|
|
232
|
+
**Trust and execution mode.** Project-level lifecycle hooks are first-party
|
|
233
|
+
project code, the same category as any `function`/`middleware` route, and
|
|
234
|
+
follow the same trusted-by-default rule with no special case
|
|
235
|
+
(docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
|
|
236
|
+
trusted, in-process execution by default; a project sets `sandbox: true` on
|
|
237
|
+
a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
|
|
238
|
+
the mechanism `function`/`middleware` routes already use and for the same
|
|
239
|
+
reason — the hook's own code, not the trustworthiness of whatever triggered
|
|
240
|
+
it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
|
|
241
|
+
route needs `sandbox: true`"). This was raised as an open question — whether
|
|
242
|
+
a lifecycle hook should always run sandboxed because it makes a
|
|
243
|
+
security-relevant decision — and settled the other way: uniformity with the
|
|
244
|
+
rest of the trust model was chosen over hardwiring isolation for lifecycle
|
|
245
|
+
hooks specifically, the same "no special case" call already made between
|
|
246
|
+
`function` and `middleware` trust. A `beforeRegister` hook enforcing "only
|
|
247
|
+
`@acme.com` may register" is the project's own governance rule over its own
|
|
248
|
+
signup flow; it is not more dangerous than any other route the project
|
|
249
|
+
wrote, and does not get a different default.
|
|
250
|
+
|
|
251
|
+
Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
|
|
252
|
+
`src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
|
|
253
|
+
each half of a hook's `sandbox: true` opt-in has its own answer:
|
|
254
|
+
|
|
255
|
+
- **Trusted (the default, no `sandbox: true`).** No core primitive is needed
|
|
256
|
+
or provided: this is ordinary first-party project code, and the
|
|
257
|
+
extension's own `activate()` already has `ExtensionActivation.root` to
|
|
258
|
+
resolve the hook's `source` against and can `import()` it directly, the
|
|
259
|
+
same way any trusted `function`/`middleware` route does.
|
|
260
|
+
- **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
|
|
261
|
+
`SandboxPool`, the same QuickJS/worker-thread engine that backs a
|
|
262
|
+
sandboxed `function`/`middleware` route — the identical module-allowlist
|
|
263
|
+
walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
|
|
264
|
+
response-shape validation, with no separate or weaker engine for
|
|
265
|
+
extensions. It takes an explicit list of `{source, export}` entries
|
|
266
|
+
(resolve a hook's `source` string with the re-exported `functionFile()`,
|
|
267
|
+
the same resolution/validation a native route's `source` gets) instead of
|
|
268
|
+
anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
|
|
269
|
+
in place of a `FunctionRoute`. There is no "trusted" mode exported
|
|
270
|
+
alongside it — `SandboxPool` is only ever the isolated path; see
|
|
271
|
+
[FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
|
|
272
|
+
[TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
|
|
273
|
+
|
|
274
|
+
An extension honoring a hook's `sandbox: true` is expected to actually
|
|
275
|
+
isolate that invocation through `SandboxPool` now that the primitive exists
|
|
276
|
+
(or document plainly that it does not yet, rather than accepting the field
|
|
277
|
+
and silently running it trusted) — say which, in the extension's own docs,
|
|
278
|
+
so an author reading them is not misled about what opt-in exists.
|
|
279
|
+
|
|
108
280
|
## Discovering schemas
|
|
109
281
|
|
|
110
282
|
Each registration carries the JSON Schemas that validate its `config` block and
|
|
@@ -161,7 +333,7 @@ and each extension's own operator files. Core never bundles or imports the
|
|
|
161
333
|
extension packages at build time; at run time it resolves
|
|
162
334
|
`@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
|
|
163
335
|
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.
|
|
336
|
+
directory, from npm where the packages are published as `0.1.0-alpha.x`
|
|
165
337
|
prereleases, is the normal path and what makes `--with auth` work), imports
|
|
166
338
|
the package and calls its
|
|
167
339
|
`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 future `urlcode-dynamic-link` package (not yet
|
|
60
|
+
published, so this one line is the exception to "nothing is roadmap" above)
|
|
61
|
+
is expected to own them the same way `auth`/`admin` own their mounts.
|
|
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
|
|
|
@@ -151,12 +159,17 @@ These are the facts that keep generated projects valid. The full matrix is in
|
|
|
151
159
|
logical names; the host file chooses the implementation. There is no
|
|
152
160
|
`--extension` flag, no `import` in YAML, no interpolation.
|
|
153
161
|
- **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
|
|
154
|
-
`function`, `
|
|
162
|
+
`function`, `proxy`, `conditional` or `extension`, plus optional
|
|
155
163
|
`middleware`. Paths are exact or single-segment `{param}`; `/*` only on
|
|
156
164
|
`static` and `extension` mounts. No regex.
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
- **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
|
|
166
|
+
It runs in-process with full Node access unless the route declares
|
|
167
|
+
`sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
|
|
168
|
+
subset, validated `args` and granted `env`, with no `fetch`, Node,
|
|
169
|
+
filesystem or timers. Either way, `args`/`env`/`secrets` are exactly what
|
|
170
|
+
the route declares and an operator grants — trust changes where code runs,
|
|
171
|
+
not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
|
|
172
|
+
docs/FUNCTION-SECURITY.md.
|
|
160
173
|
- **Authentication is host processing.** Do not build login forms, session
|
|
161
174
|
cookies or password checks in functions. Declare `policies.extensions.auth`
|
|
162
175
|
on the route; the runtime withholds `Cookie` and `Authorization` from guests.
|
|
@@ -165,8 +178,11 @@ These are the facts that keep generated projects valid. The full matrix is in
|
|
|
165
178
|
with the route named; nothing degrades silently.
|
|
166
179
|
- **Provider targets refuse what they cannot enforce.** Cloudflare runs
|
|
167
180
|
redirects and declared responses only. Serverless adapters refuse functions,
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
proxy, signals and extensions. The `static` target (S3 + CloudFront,
|
|
182
|
+
no server) refuses everything that needs request-time logic, keeping only
|
|
183
|
+
`redirect`/`respond`/`page`/`static`/`download` — see [static
|
|
184
|
+
hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
|
|
185
|
+
promising a deployment.
|
|
170
186
|
- **Report evidence, not hope.** The commands above are the evidence. Local
|
|
171
187
|
tests are not deployment, soak or independent security review.
|
|
172
188
|
|
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# Function execution: trusted by default, sandboxed opt-in
|
|
2
|
+
|
|
3
|
+
`function` and `middleware` routes run **trusted and unsandboxed by default**:
|
|
4
|
+
in the host process, with full Node, filesystem and network access, exactly
|
|
5
|
+
like any other project code (docs/SPIKE-DEFAULT-TRUST-MODEL.md). This is a
|
|
6
|
+
deliberate, maintainer-decided reversal of alpha.2's blanket sandbox — see
|
|
7
|
+
that spike document for the full rationale. It is a call the project makes,
|
|
8
|
+
not a property the runtime can verify: URLCode cannot know whether your code
|
|
9
|
+
is safe to trust, only whether you asked for isolation.
|
|
10
|
+
|
|
11
|
+
Declare `sandbox: true` on a route when its code specifically warrants
|
|
12
|
+
isolation: it processes input from a source the project doesn't fully trust
|
|
13
|
+
(a third-party webhook payload, for example), it is a contribution nobody on
|
|
14
|
+
the team has reviewed, or it handles a secret sensitive enough that a bug in
|
|
15
|
+
that one route should not be able to reach the rest of the process or the
|
|
16
|
+
filesystem. A sandboxed route runs in QuickJS inside WebAssembly, in a
|
|
17
|
+
separate worker thread, with none of the host access described below — its
|
|
18
|
+
guarantees are unchanged from every earlier release and are described in
|
|
19
|
+
full in the rest of this document. Absence of `sandbox` (or `sandbox: false`)
|
|
20
|
+
means trusted; there is no separate `unsafe`/`trusted` field to opt back into
|
|
21
|
+
the old sandboxed-by-default behavior — set `sandbox: true` per route instead.
|
|
22
|
+
|
|
23
|
+
**Either way, binding grants are unaffected.** Trusting a route's code by
|
|
24
|
+
default does not grant it any `env`/`secrets` it was not explicitly declared
|
|
25
|
+
in YAML and approved by an operator policy pinned to the project revision
|
|
26
|
+
(see "Granting selected bindings" below). A trusted function only *can* do
|
|
27
|
+
more with Node once it runs — it does not receive anything more than a
|
|
28
|
+
sandboxed one would.
|
|
29
|
+
|
|
30
|
+
This is a claim about `context`/`context.secrets` injection, not an
|
|
31
|
+
access-control guarantee on trusted code. The binding grant governs only what
|
|
32
|
+
URLCode hands a route through `context`; it does not restrict what trusted
|
|
33
|
+
(non-`sandbox`) code can independently do, because that code has full Node
|
|
34
|
+
access by design. A trusted function can read `process.env`, open files or
|
|
35
|
+
make network calls on its own regardless of what its route was or was not
|
|
36
|
+
granted — withholding a binding grant limits what URLCode gives the code
|
|
37
|
+
through `context`, not what the code itself, running with full Node access,
|
|
38
|
+
can go and get. A sandboxed route has no such independent access: the guest
|
|
39
|
+
API is all it has, so its binding grant *is* effectively its whole reach into
|
|
40
|
+
the environment. Trusted code's reach is not bounded that way; treat the
|
|
41
|
+
grant as scoping `context`, not as scoping the process.
|
|
42
|
+
|
|
43
|
+
## What "sandboxed" (`sandbox: true`) still guarantees
|
|
9
44
|
|
|
10
45
|
- Function sources are parsed/snapshotted without importing them into Node.
|
|
11
46
|
- Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
|
|
@@ -22,9 +57,64 @@ is no `unsafe`, `trusted` or automatic host-execution fallback.
|
|
|
22
57
|
Operator grants are exact-name, route-scoped and pinned to configuration/source.
|
|
23
58
|
|
|
24
59
|
The guest API is intentionally narrower than Node or full Fetch; see the
|
|
25
|
-
[implemented contract](SPECIFICATION.md).
|
|
26
|
-
or binary/stream APIs must be rewritten
|
|
27
|
-
|
|
60
|
+
[implemented contract](SPECIFICATION.md). A function moving from trusted to
|
|
61
|
+
`sandbox: true` that uses Node/network or binary/stream APIs must be rewritten
|
|
62
|
+
for the supported guest profile, or stay trusted. Redirects need none of this
|
|
63
|
+
machinery either way.
|
|
64
|
+
|
|
65
|
+
This engine — worker spawning, the module-allowlist walk, the two-layer
|
|
66
|
+
deadline, `maxBytes` and response-shape validation — is one implementation
|
|
67
|
+
shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
|
|
68
|
+
the public primitive an extension package uses to run a project-supplied hook
|
|
69
|
+
through this exact isolation when the project's own config declares
|
|
70
|
+
`sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
|
|
71
|
+
and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
|
|
72
|
+
through that entry point; there is no separate, weaker sandbox for extensions
|
|
73
|
+
to reach for, and no "trusted" mode exported there — trusted execution needs
|
|
74
|
+
no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
|
|
75
|
+
|
|
76
|
+
## What the trusted default can and can't do
|
|
77
|
+
|
|
78
|
+
A trusted route (no `sandbox`, or `sandbox: false`) has none of the guest
|
|
79
|
+
restrictions above:
|
|
80
|
+
|
|
81
|
+
- Full Node built-ins, `process`, the filesystem, `fetch`, sockets, workers
|
|
82
|
+
and npm packages are available, exactly as in any other Node module.
|
|
83
|
+
- Module resolution is ordinary Node ESM resolution: bare specifiers, dynamic
|
|
84
|
+
`import()` and node_modules all work. There is no dependency-graph allowlist
|
|
85
|
+
and no per-module/total source-size budget (function-sources.ts's
|
|
86
|
+
`MODULE_LIMIT`/`MODULE_BYTE_LIMIT`/`TOTAL_BYTE_LIMIT` apply only to what a
|
|
87
|
+
sandboxed snapshot bundles).
|
|
88
|
+
- Node's own module cache is shared across invocations and across the whole
|
|
89
|
+
process; there is no fresh heap per call. Module-level state persists
|
|
90
|
+
between requests exactly like an ordinary long-running Node server, so a
|
|
91
|
+
trusted function that mutates shared/global state affects later requests
|
|
92
|
+
the way hand-written server code would.
|
|
93
|
+
- There is no worker-thread deadline that force-terminates a stuck call. A
|
|
94
|
+
trusted invocation races a configurable timeout, but that race can only
|
|
95
|
+
reject the *call*; it cannot preempt code that blocks the event loop
|
|
96
|
+
synchronously. See [capacity](CAPACITY.md) for what this means for one slow
|
|
97
|
+
or hung trusted route's effect on the rest of the process.
|
|
98
|
+
- A snapshot reload re-imports a trusted route's own entry file fresh (each
|
|
99
|
+
reload gets its own cache-busted module registration), so editing the
|
|
100
|
+
`source` file a route declares and reloading picks up the change, the same
|
|
101
|
+
as the sandboxed pool rebuilding from scratch. A file that entry file
|
|
102
|
+
merely *imports* is not similarly busted: Node's own module cache is
|
|
103
|
+
keyed by the resolved URL of that import statement, which this runtime
|
|
104
|
+
does not rewrite, so an edited dependency two files deep from the route
|
|
105
|
+
keeps serving its old content until the process restarts. Restructure a
|
|
106
|
+
route so the code you expect to hot-reload is the declared entry file
|
|
107
|
+
itself, or restart rather than reload after editing a trusted route's
|
|
108
|
+
dependencies. A `sandbox: true` route has no such gap: reload always
|
|
109
|
+
rebuilds its whole snapshot, dependencies included.
|
|
110
|
+
|
|
111
|
+
What does **not** change with trust: `args` are still exactly the validated
|
|
112
|
+
values the route declares (never raw request input), and `env`/`secrets` are
|
|
113
|
+
still exactly what the route's YAML requests and an operator policy grants,
|
|
114
|
+
pinned to the project revision — trust changes where code runs, not what
|
|
115
|
+
it is handed *through `context`*. It does not change what the code can go get
|
|
116
|
+
on its own once it is running; see "binding grants are unaffected" above for
|
|
117
|
+
that distinction.
|
|
28
118
|
|
|
29
119
|
## Granting selected bindings
|
|
30
120
|
|
|
@@ -63,26 +153,28 @@ urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
|
|
|
63
153
|
|
|
64
154
|
`dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
|
|
65
155
|
The JavaScript API accepts an equivalent operator-supplied `permissions` object.
|
|
66
|
-
Every config
|
|
67
|
-
|
|
68
|
-
|
|
156
|
+
Every config change invalidates the grant, and so does a module change within
|
|
157
|
+
what the approval digest actually hashes: for a sandboxed route, its
|
|
158
|
+
middleware/function sources and their full dependency graph; for a trusted
|
|
159
|
+
route, only its own entry-file source (see the next paragraph — a trusted
|
|
160
|
+
route's transitive dependencies are explicitly **not** part of that digest).
|
|
161
|
+
Inspect/review the new revision before updating the operator file. Policies
|
|
162
|
+
are read at startup, not hot-reloaded. A failed development candidate leaves
|
|
163
|
+
the previous approved snapshot running.
|
|
69
164
|
|
|
70
165
|
Granting a secret deliberately makes it available to every middleware and function
|
|
71
|
-
in that route
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
filesystem handle, database path or management token. Native link data changes do
|
|
84
|
-
not authorize new code or bindings. Management requires a separate operator CLI
|
|
85
|
-
or token-protected listener. See [dynamic links](DYNAMIC-LINKS.md).
|
|
166
|
+
in that route, trusted or sandboxed alike. A sandboxed route's middleware
|
|
167
|
+
sources and their full dependency graph are included in the approval digest,
|
|
168
|
+
as before; a trusted route's own entry-file source is included too, so
|
|
169
|
+
changing that file's content invalidates the grant, but a change to a helper
|
|
170
|
+
module it merely imports does not by itself (see function-sources.ts's
|
|
171
|
+
`collectTrustedSources`) — a known, documented gap versus the sandboxed path's
|
|
172
|
+
full dependency-graph hashing: a trusted route's grant scope is entry-file-only,
|
|
173
|
+
not transitive. Either way, code can include any granted data
|
|
174
|
+
in its HTTP response: neither the sandbox nor the trusted default promises
|
|
175
|
+
secrecy from code that was explicitly authorized to read a value. Minimize
|
|
176
|
+
grants, use scoped/short-lived credentials and revoke/restart when needed.
|
|
177
|
+
Other routes get none of that context.
|
|
86
178
|
|
|
87
179
|
## Next capability work
|
|
88
180
|
|
|
@@ -90,9 +182,14 @@ Outbound requests need a host-owned broker with explicit destination/method
|
|
|
90
182
|
allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
|
|
91
183
|
redirect revalidation, deadlines and byte/concurrency limits. Application YAML
|
|
92
184
|
must not grant those permissions. Persistent state needs similarly scoped access.
|
|
93
|
-
Until such brokers are implemented and tested, these capabilities are unavailable
|
|
94
|
-
Provider adapters must preserve
|
|
95
|
-
|
|
185
|
+
Until such brokers are implemented and tested, these capabilities are unavailable
|
|
186
|
+
to a *sandboxed* route. Provider adapters must preserve a `sandbox: true`
|
|
187
|
+
route's isolation or reject deployment; they cannot silently downgrade a
|
|
188
|
+
route that explicitly asked for the sandbox into unrestricted Node execution.
|
|
189
|
+
(A trusted route, by contrast, already has unrestricted Node execution by
|
|
190
|
+
design on the self-hosted target — see "What the trusted default can and
|
|
191
|
+
can't do" above; non-Node targets refuse `function`/`middleware` entirely,
|
|
192
|
+
trusted or sandboxed, since neither execution mode exists there.)
|
|
96
193
|
|
|
97
194
|
## Verification and remaining risk
|
|
98
195
|
|
package/docs/INSTALL.md
CHANGED
|
@@ -6,11 +6,6 @@ pick whichever fits how you already manage tools. All of them require
|
|
|
6
6
|
TypeScript source (`dist/`, with declarations; see [TypeScript](TYPESCRIPT.md)),
|
|
7
7
|
so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
|
|
8
8
|
|
|
9
|
-
Live short-link storage additionally needs a Node build carrying the patched
|
|
10
|
-
SQLite WAL fix. Run `urlcode doctor` after installing and check `liveLinks`;
|
|
11
|
-
everything except live links works on any supported build. See
|
|
12
|
-
[dynamic links](DYNAMIC-LINKS.md#node-build-requirement).
|
|
13
|
-
|
|
14
9
|
## npm
|
|
15
10
|
|
|
16
11
|
```sh
|