@jimhoyd/urlcode 0.4.2 → 0.4.7
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 +57 -14
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +42 -18
- package/SECURITY.md +5 -3
- package/dist/BUILD-MANIFEST.json +30 -27
- 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 +28 -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 +60 -2
- 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/project-tests.js +35 -11
- package/dist/readiness.js +184 -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 +2 -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 +47 -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 +68 -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 +1 -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 +364 -49
- package/llms.txt +44 -6
- package/package.json +15 -26
- 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 +47 -27
- 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 -148
- package/ROADMAP.md +0 -70
- package/docs/AI-AUTHORING.md +0 -339
- 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 -287
- package/docs/CONDITIONS.md +0 -74
- package/docs/CONTAINER-PROMOTION.md +0 -74
- package/docs/DEPLOYMENT-CHECKS.md +0 -108
- package/docs/DEVELOPMENT-PIPELINE.md +0 -304
- package/docs/EGRESS.md +0 -125
- package/docs/EXTENSIONS.md +0 -403
- package/docs/FRAMEWORK.md +0 -217
- package/docs/FUNCTION-SECURITY.md +0 -251
- 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 -149
- 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 -153
- package/docs/README.md +0 -93
- 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-0.4.2.md +0 -30
- package/docs/RELEASE-READINESS.md +0 -146
- package/docs/RELEASE-SECURITY.md +0 -100
- 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 -207
- 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 -88
- 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/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
- package/docs/archive/README.md +0 -29
- 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 -130
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/docs/POLICIES.md
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
# Policies
|
|
2
|
-
|
|
3
|
-
Policies are host-enforced behavior declared in YAML around a route: a
|
|
4
|
-
per-client request budget, a User-Agent deny list, a security-header profile,
|
|
5
|
-
content-coding negotiation and an HTTP caching strategy. They run in the host
|
|
6
|
-
process, outside function/middleware execution — trusted or sandboxed alike —
|
|
7
|
-
so they can see the client address, keep counters across requests and touch
|
|
8
|
-
the transport, which route middleware by design cannot
|
|
9
|
-
([middleware](MIDDLEWARE.md), [function security](FUNCTION-SECURITY.md)).
|
|
10
|
-
|
|
11
|
-
Everything here is optional and off by default. A project with no `policies`
|
|
12
|
-
key and no `profiles` key behaves exactly as before: no policy module is
|
|
13
|
-
compiled and the request path is unchanged. Each policy has its own page:
|
|
14
|
-
|
|
15
|
-
| Policy | Page | What it does |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| `throttle` | [throttle](policies/throttle.md) | Sliding-window request budget with `RateLimit`/`RateLimit-Policy` headers, 429 and `Retry-After` |
|
|
18
|
-
| `agents` | [agents](policies/agents.md) | Deny or allow by `User-Agent`, from bundled lists, project files and bounded patterns |
|
|
19
|
-
| `security` | [security](policies/security.md) | OWASP Secure Headers Project response headers, with per-header `set`/`unset` |
|
|
20
|
-
| `compression` | [compression](policies/compression.md) | `Accept-Encoding` negotiation; assets precompressed at snapshot time |
|
|
21
|
-
| `cache` | [cache](policies/cache.md) | Named strategies from RFC 9111/5861/8246/9213 plus an origin memory cache |
|
|
22
|
-
|
|
23
|
-
The design and the reasoning behind each choice are in the
|
|
24
|
-
[extensions spike](archive/2026-09-19/SPIKE-EXTENSIONS.md). Operators who need behavior the
|
|
25
|
-
declarative block cannot express pass host [plugins](PLUGINS.md) in code; plugins
|
|
26
|
-
are never named in YAML.
|
|
27
|
-
|
|
28
|
-
## Declaring policies
|
|
29
|
-
|
|
30
|
-
```yaml
|
|
31
|
-
version: "1"
|
|
32
|
-
policies: # project defaults; every key optional
|
|
33
|
-
profile: hardened # built-in, or a name under `profiles`
|
|
34
|
-
throttle: { quota: 60, window: 60 }
|
|
35
|
-
routes:
|
|
36
|
-
/api/lookup/{id}:
|
|
37
|
-
function: { source: functions/lookup.mjs }
|
|
38
|
-
policies:
|
|
39
|
-
throttle: { quota: 10, window: 60 }
|
|
40
|
-
cache: false
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`policies` at the top level sets project defaults; `routes.<path>.policies`
|
|
44
|
-
adjusts them for one route. Two route-level short forms exist. `auth`
|
|
45
|
-
expands to `policies.extensions.auth` when the project declares an auth
|
|
46
|
-
[extension](EXTENSIONS.md), carrying the same keys minus `required`;
|
|
47
|
-
`required: false` documents the intent and emits no policy. It accepts
|
|
48
|
-
`required`, `role`, `permission`, `verified`, `freshWithinSeconds` and
|
|
49
|
-
`onDeny` and nothing else — `role` is singular, and there is no `roles`. Like
|
|
50
|
-
`cache` below, it is refused rather than silently ignored in three cases: when
|
|
51
|
-
the project declares no `extensions.auth`, when the route also sets
|
|
52
|
-
`policies.extensions.auth` (use one form), and when the route sets
|
|
53
|
-
`policies.extensions: false`. `cache: {strategy, maxAge, ...}` expands to
|
|
54
|
-
`policies.cache` the same way — the compiler merges it into that route's
|
|
55
|
-
`policies` before anything else reads the project, so `routes`, `audit` and
|
|
56
|
-
`explain` see only the expanded long form, and it is refused alongside a
|
|
57
|
-
direct `policies.cache` on the same route (use one form):
|
|
58
|
-
|
|
59
|
-
```yaml
|
|
60
|
-
routes:
|
|
61
|
-
/feed:
|
|
62
|
-
function: { source: functions/feed.mjs }
|
|
63
|
-
cache: { strategy: swr, maxAge: 30 } # expands to policies: { cache: {...} }
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Both accept the same keys: `profile` plus one entry
|
|
67
|
-
per policy, each either an object or `false`. Unknown keys fail validation, as
|
|
68
|
-
everywhere in the project format. The
|
|
69
|
-
[field reference](YAML-REFERENCE.md) lists every accepted field with its
|
|
70
|
-
bounds; the [JSON Schema](../schemas/urlcode.schema.json) is the source.
|
|
71
|
-
|
|
72
|
-
Values are behavior, never infrastructure: a quota is a number a proxy or CDN
|
|
73
|
-
can restate, while trusted proxy ranges, storage URLs and vendor rule
|
|
74
|
-
identifiers stay in operator configuration. That is what keeps a project
|
|
75
|
-
portable between a laptop, a container and an adapter.
|
|
76
|
-
|
|
77
|
-
## Where policies run
|
|
78
|
-
|
|
79
|
-
Policies run once the route is known and before its contract is checked. A
|
|
80
|
-
denied agent or an exhausted budget is answered without reading a body or
|
|
81
|
-
starting the sandbox. The self-hosted pipeline:
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
socket limits → admission → body read
|
|
85
|
-
→ runtime.handle(): match → enabled/expires
|
|
86
|
-
→ plugins onRequest (outermost, first plugin first)
|
|
87
|
-
→ agents → throttle → cache lookup (request phase; may short-circuit)
|
|
88
|
-
→ methods (405) → request.body checks
|
|
89
|
-
→ native handler | guest chain
|
|
90
|
-
→ YAML response.headers
|
|
91
|
-
→ cache store → throttle headers → security → compression (response phase)
|
|
92
|
-
→ plugins onResponse (reverse order, first plugin last)
|
|
93
|
-
→ writeResponse
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Request order is fixed: agents first because a denial is the cheapest
|
|
97
|
-
outcome, then throttle, then the cache lookup, so a denied or refused request
|
|
98
|
-
neither counts against a budget nor touches the cache. Response order is the
|
|
99
|
-
cache store, then the throttle's `RateLimit` headers (after the store, so a
|
|
100
|
-
cached copy is never stamped with one client's remaining budget), then
|
|
101
|
-
security headers, then compression last so every header it depends on is
|
|
102
|
-
already final. YAML `response.headers` are applied by the runtime before the
|
|
103
|
-
response phase, so an explicit header always beats a profile default.
|
|
104
|
-
|
|
105
|
-
A result produced by a request-phase policy (an agent denial, a throttle
|
|
106
|
-
refusal, a cache hit) skips that policy's own response hook and passes through
|
|
107
|
-
the others: a hit is not stored twice but still carries the client's
|
|
108
|
-
rate-limit headers, and a denial is not stored because its status is not
|
|
109
|
-
cacheable. A throttle refusal carries its own `RateLimit` and `Retry-After`
|
|
110
|
-
headers. A plugin short-circuit ran before any policy, so it skips the
|
|
111
|
-
response hook of every policy that has a request phase; security headers and
|
|
112
|
-
compression still apply to it.
|
|
113
|
-
|
|
114
|
-
Two responses bypass the response phase entirely: the `405` the runtime returns
|
|
115
|
-
for an undeclared method, and any error the runtime throws (404 for no match,
|
|
116
|
-
410 for an expired route, 413/415 from body checks, sandbox 502/504). Those
|
|
117
|
-
receive the runtime's standard headers only. Policies with an `onError` hook
|
|
118
|
-
and plugin `onError` hooks observe thrown errors; they cannot change them.
|
|
119
|
-
|
|
120
|
-
Adapters call `handle()` directly and delegate socket limits and admission to
|
|
121
|
-
the provider; the policy order inside `handle()` is the same. The Cloudflare
|
|
122
|
-
Worker runs the same request and response order with the two policies it can
|
|
123
|
-
carry.
|
|
124
|
-
|
|
125
|
-
## Portability and the per-target table
|
|
126
|
-
|
|
127
|
-
A project is portable when a second person can run it elsewhere and get the
|
|
128
|
-
same declared behavior or an explicit refusal. A target that cannot honor a
|
|
129
|
-
policy refuses activation naming the route and the policy, exactly as adapters
|
|
130
|
-
refuse functions and middleware; nothing degrades silently. One exception is
|
|
131
|
-
stated rather than hidden: a policy the platform itself already provides is
|
|
132
|
-
**delegated**, meaning accepted and dropped, and reported as such in the
|
|
133
|
-
inventory, so one YAML can serve a Node host and a serverless host without
|
|
134
|
-
edits. The self-hosted
|
|
135
|
-
message reads `/path declares policies.throttle, which the vercel target
|
|
136
|
-
cannot enforce`; the Cloudflare build reports
|
|
137
|
-
`/path: policies.throttle cannot be compiled for this target`.
|
|
138
|
-
|
|
139
|
-
| Policy | Self-hosted (`node`) | Vercel / AWS | Cloudflare build |
|
|
140
|
-
|---|---|---|---|
|
|
141
|
-
| `agents` | native | native | compiled into the artifact |
|
|
142
|
-
| `security` | native | native | compiled into the artifact |
|
|
143
|
-
| `throttle` | native, in-process counters | native only with `partition: route`; `client` and `client-route` refused because a client fans across instances and the budget would silently be quota × instances | refused |
|
|
144
|
-
| `compression` | native | delegated: the provider compresses | delegated |
|
|
145
|
-
| `cache` | native: headers plus origin memory cache | native | refused |
|
|
146
|
-
|
|
147
|
-
"Compiled" means the effective configuration for every route is validated at
|
|
148
|
-
build time and carried in the Worker artifact; the Worker has no filesystem and
|
|
149
|
-
no Node imports, so only modules free of both qualify. The Worker's client
|
|
150
|
-
identity is the platform's `cf-connecting-ip`; the serverless adapters use the
|
|
151
|
-
platform-set source address (`sourceIp` on Lambda, the leftmost
|
|
152
|
-
`X-Forwarded-For` entry the platform writes on Vercel). None of these read a
|
|
153
|
-
forwarded header a client could have set.
|
|
154
|
-
|
|
155
|
-
The cross-request state a policy keeps (throttle counters, the origin cache) is
|
|
156
|
-
per runtime instance on every target, never shared between replicas or
|
|
157
|
-
serverless instances. Given identical request bytes and project, every target
|
|
158
|
-
answers with the same status and headers; only that state may differ.
|
|
159
|
-
|
|
160
|
-
## Merge semantics
|
|
161
|
-
|
|
162
|
-
The effective configuration for a route is built from four layers, each
|
|
163
|
-
merged over the one below:
|
|
164
|
-
|
|
165
|
-
1. the project `profile` (built-in or custom),
|
|
166
|
-
2. the project `policies` keys,
|
|
167
|
-
3. the route `profile`, if the route names one,
|
|
168
|
-
4. the route `policies` keys.
|
|
169
|
-
|
|
170
|
-
Within a layer, `false` removes a policy declared below it; an object merges
|
|
171
|
-
shallowly over what is there, so a route can tighten one number without
|
|
172
|
-
restating the rest. Only top-level keys of each policy merge: a route that
|
|
173
|
-
writes `agents: { deny: [crawlers] }` replaces the whole `deny` array, not one
|
|
174
|
-
entry of it.
|
|
175
|
-
|
|
176
|
-
```yaml
|
|
177
|
-
policies:
|
|
178
|
-
profile: hardened
|
|
179
|
-
throttle: { quota: 5 } # hardened's window and partition remain
|
|
180
|
-
profiles:
|
|
181
|
-
mine:
|
|
182
|
-
security: { headers: oshp-no-csp }
|
|
183
|
-
routes:
|
|
184
|
-
/feed:
|
|
185
|
-
policies:
|
|
186
|
-
profile: mine # merges over the project layer, not instead of it
|
|
187
|
-
throttle: false # removed for this route only
|
|
188
|
-
cache: { strategy: swr, maxAge: 3 }
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Here `/feed` ends up with `security: { headers: oshp-no-csp }`, the
|
|
192
|
-
`hardened` agents and compression entries, no throttle, and the route's cache
|
|
193
|
-
entry. Any other route gets `hardened` with `throttle.quota` at 5.
|
|
194
|
-
|
|
195
|
-
`profiles` is a top-level map of reusable policy sets, each a `policies`
|
|
196
|
-
object without a `profile` key. A custom profile whose name matches a built-in
|
|
197
|
-
shadows it, so a project can redefine `hardened` and every reference to it
|
|
198
|
-
resolves to the project's version. Naming an unknown profile fails validation.
|
|
199
|
-
|
|
200
|
-
When any policy is declared anywhere in the project, every route is compiled
|
|
201
|
-
against the effective table; a route with nothing effective simply has empty
|
|
202
|
-
chains. When none is declared, no policy code runs.
|
|
203
|
-
|
|
204
|
-
## Further pages
|
|
205
|
-
|
|
206
|
-
| Page | Sections |
|
|
207
|
-
|---|---|
|
|
208
|
-
| [The built-in `hardened` profile and hardening guidance](policies/hardened.md) | The built-in `hardened` profile; Hardened configuration guidance |
|
|
209
|
-
| [The policy contract and your own patterns](policies/contract.md) | The policy contract in TypeScript; Supplying your own patterns |
|
|
210
|
-
| [Client identity, inventory and logging](policies/operations.md) | Client identity and `--trusted-proxies`; What `routes` and `audit` report; Logging |
|
|
211
|
-
| [Interoperability between policies](policies/interoperability.md) | Interoperability |
|
package/docs/PRERENDER.md
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
# Prerendering pages into a native static project
|
|
2
|
-
|
|
3
|
-
Render a project's function and middleware routes once, at build time, into a
|
|
4
|
-
project whose routes are all native `page` handlers. The published site answers
|
|
5
|
-
from prevalidated byte buffers: no guest code runs to serve a request at all,
|
|
6
|
-
so whatever execution mode the source routes used — trusted by default, or
|
|
7
|
-
isolated QuickJS/WASM where a route declares `sandbox: true` — along with its
|
|
8
|
-
deadline and memory budget where sandboxed, is not on the request path.
|
|
9
|
-
|
|
10
|
-
The shared orchestration ships as a build helper, `@jimhoyd/urlcode/prerender`, and the
|
|
11
|
-
runnable recipe is [`examples/prerender`](../examples/prerender/README.md), which
|
|
12
|
-
consumes it. Both are covered by `test/prerender.test.ts`. Everything here uses
|
|
13
|
-
the existing runtime: prerendering adds no field to route YAML, no CLI command
|
|
14
|
-
and no capability.
|
|
15
|
-
|
|
16
|
-
## Why render ahead of time
|
|
17
|
-
|
|
18
|
-
A function response body is readable by middleware through `text()`. A native
|
|
19
|
-
`page`, `static`, `download`, `redirect` or `respond` body is not — that
|
|
20
|
-
opacity is a deliberate contract, not a gap, and it is what keeps native file
|
|
21
|
-
serving free of guest code. See [middleware](MIDDLEWARE.md).
|
|
22
|
-
|
|
23
|
-
So a shared template cannot be wrapped around a native file at request time. The
|
|
24
|
-
answer is to apply the template while the content is still a function response,
|
|
25
|
-
and publish the result:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
function + template middleware ──render once──▶ HTML file ──▶ page route
|
|
29
|
-
(trusted or sandboxed, build time) (no guest code)
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The alternative — reading Markdown through `next().text()` on a native route —
|
|
33
|
-
is not supported and should not be attempted. Prepare content at build time.
|
|
34
|
-
|
|
35
|
-
## The build helper
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
import {prerenderPages, assertNativeProject, pageFileName} from '@jimhoyd/urlcode/prerender';
|
|
39
|
-
|
|
40
|
-
const rendered = await prerenderPages('./render-source', './out/pages', {
|
|
41
|
-
origin: 'https://docs.example', // what a page sees as its own origin
|
|
42
|
-
});
|
|
43
|
-
// rendered.pages → [{path: '/guide', file: 'guide.html', bytes: 531}, …]
|
|
44
|
-
// rendered.fixtures → byte-for-byte GET and empty HEAD cases, ready to extend
|
|
45
|
-
// rendered.count, rendered.bytes, rendered.directory
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`prerenderPages` owns everything that is easy to get wrong and nothing that is
|
|
49
|
-
site-specific. It activates the source project, selects the active literal GET
|
|
50
|
-
function routes, renders each one through its middleware, checks the status and
|
|
51
|
-
content type, enforces the budgets, derives and validates a safe output filename,
|
|
52
|
-
writes the files and closes the runtime — then hands back metadata. It does not
|
|
53
|
-
write a project, choose response headers, copy assets or compile content: the
|
|
54
|
-
caller assembles a project, or a generated include, from `pages` and `fixtures`.
|
|
55
|
-
|
|
56
|
-
`assertNativeProject(project, {allow})` activates a project and proves it cannot
|
|
57
|
-
execute guest code to answer a request: every route is one of the allowed native
|
|
58
|
-
handlers and none carries middleware. `allow` defaults to `['page', 'static',
|
|
59
|
-
'download']`, which is what a real site serves; narrow it to `['page']` for a
|
|
60
|
-
page-only artifact. Run it on the **final assembled site**, not only on the
|
|
61
|
-
rendered pages, so what you deploy is what was checked.
|
|
62
|
-
|
|
63
|
-
| Option | Default | Meaning |
|
|
64
|
-
|---|---|---|
|
|
65
|
-
| `origin` | `http://localhost` | HTTP(S) origin a render sees; no path or credentials |
|
|
66
|
-
| `fileName` | `pageFileName` | Route path to filename; the path and the result are both validated either way |
|
|
67
|
-
| `ignoreUnrenderable` | `false` | Allow source routes this build will not render |
|
|
68
|
-
| `maxPages` | 500 | Pages in one render |
|
|
69
|
-
| `maxPageBytes` | 512 KiB | Bytes per rendered page |
|
|
70
|
-
| `maxTotalBytes` | 32 MiB | Bytes across the whole render |
|
|
71
|
-
| `log` | none | `{event: 'prerendered', path, file, bytes}` per page |
|
|
72
|
-
|
|
73
|
-
## What the helper guarantees
|
|
74
|
-
|
|
75
|
-
The runtime enforces its own protections when a generated project activates:
|
|
76
|
-
asset declarations reject absolute paths, traversal, dot segments, symlinks,
|
|
77
|
-
hardlinks and nonregular files, and static trees refuse `urlcode.yaml`,
|
|
78
|
-
`package.json`, `package-lock.json`, hidden entries, `node_modules` and
|
|
79
|
-
`.pem/.key/.p12/.pfx/.env` files. See [assets](ASSETS.md).
|
|
80
|
-
|
|
81
|
-
Those checks fail a deployment. The helper fails the build earlier and more
|
|
82
|
-
specifically:
|
|
83
|
-
|
|
84
|
-
- **Filenames are validated, never trusted.** `pageFileName` maps one route path
|
|
85
|
-
to one flat name: segments joined with `~`, which cannot occur in a segment, so
|
|
86
|
-
`/a/b` (`a~b.html`) and `/a-b` (`a-b.html`) are distinct rather than a silent
|
|
87
|
-
collision, and the mapping stays injective for every accepted path. Dots,
|
|
88
|
-
underscores and mixed case are fine, so a docs URL like `/docs/ASSETS.md`
|
|
89
|
-
works. Parameters, wildcards, traversal and dot segments are rejected. A custom
|
|
90
|
-
`fileName` hook is allowed — hashing the route is a reasonable choice — but its
|
|
91
|
-
result goes through the same check: a flat name, no leading dot, not a
|
|
92
|
-
protected name, `.html`, and unique **case-insensitively**, because on macOS
|
|
93
|
-
and Windows two names differing only in case are one file.
|
|
94
|
-
- **Directories may not overlap.** In either direction: a build must not write
|
|
95
|
-
into the reviewed source, nor read a source nested inside its output.
|
|
96
|
-
- **Nothing is written until everything renders.** Pages are held in memory and
|
|
97
|
-
written only after the last one passes, and the pages directory must not
|
|
98
|
-
already exist — it is created, along with any missing parents, only once every
|
|
99
|
-
render has succeeded, so a failed build creates nothing at all. An existing
|
|
100
|
-
pages directory is refused with an error carrying `code: 'EEXIST'`. If your
|
|
101
|
-
artifact has a root above that directory, claiming it is yours: check it before
|
|
102
|
-
calling, and let the helper create it as a parent after the render.
|
|
103
|
-
- **Every render is checked.** A non-200 status, a content type that is not
|
|
104
|
-
`text/html`, an empty body, an oversized body or an exceeded aggregate budget
|
|
105
|
-
fails the build instead of publishing a file.
|
|
106
|
-
- **Bytes are preserved.** The response body is a byte array, not a string. It is
|
|
107
|
-
kept as a `Buffer` through the file and its fixture alike, so a multi-byte
|
|
108
|
-
character is never re-encoded or truncated.
|
|
109
|
-
- **Skipping is explicit.** By default a source route the build would not render
|
|
110
|
-
fails it, because silently rendering a subset publishes an incomplete site that
|
|
111
|
-
looks whole. Pass `ignoreUnrenderable` when a mixed project is intended.
|
|
112
|
-
- **The runtime is always closed.** In a `finally`, so a failing build exits
|
|
113
|
-
instead of hanging on its worker threads.
|
|
114
|
-
|
|
115
|
-
The helper is operator build tooling. It runs in Node with normal filesystem
|
|
116
|
-
access because it is not guest code; it does not itself widen a source route's
|
|
117
|
-
declared execution mode — a route with `sandbox: true` still renders isolated,
|
|
118
|
-
with no filesystem, and no host-code fallback is introduced for it. It is a
|
|
119
|
-
separate package export from the runtime for that reason. Review it as you
|
|
120
|
-
review any deployment tooling.
|
|
121
|
-
|
|
122
|
-
## Assembling a site
|
|
123
|
-
|
|
124
|
-
What the helper returns is deliberately not a project, because that is the part
|
|
125
|
-
every site does differently. A small site writes one `page` route per file, as
|
|
126
|
-
[`examples/prerender`](../examples/prerender/README.md) does in about twenty
|
|
127
|
-
lines. A larger one copies the rendered pages next to its own assets, adds
|
|
128
|
-
`static` and `download` routes and response security headers, keeps a committed
|
|
129
|
-
entry point and writes only a generated include, then extends `fixtures` with its
|
|
130
|
-
own cases before asserting the whole thing is native:
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
const rendered = await prerenderPages(renderSource, 'project/public/pages');
|
|
134
|
-
for (const page of rendered.pages)
|
|
135
|
-
config.routes[page.path] = {page: {file: `public/pages/${page.file}`}, response: {headers: security}};
|
|
136
|
-
await writeFile('project/generated/routes.yaml', stringify(config));
|
|
137
|
-
await writeFile('project/tests/requests.json', JSON.stringify([...rendered.fixtures, ...ownCases]));
|
|
138
|
-
await assertNativeProject('project');
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Applying your own `response.headers` is expected; the helper never chooses them
|
|
142
|
-
for you and never discards them.
|
|
143
|
-
|
|
144
|
-
A site that renders straight into the tree it serves, rather than into a staging
|
|
145
|
-
project, needs no copy step at all — point `prerenderPages` at the pages
|
|
146
|
-
directory inside the serving project, keeping the render source outside it.
|
|
147
|
-
|
|
148
|
-
## Limits worth knowing before you design a site
|
|
149
|
-
|
|
150
|
-
| Limit | Value | Where |
|
|
151
|
-
|---|---|---|
|
|
152
|
-
| Function modules per snapshot | 127 | source project; the render splits into passes |
|
|
153
|
-
| Function module source bytes | 1 MiB each, 4 MiB total per snapshot | source project; the render splits into passes |
|
|
154
|
-
| Function/middleware response body | 1 MiB default (`--max-response-bytes`) | render step |
|
|
155
|
-
| Rendered page bytes | 512 KiB (`maxPageBytes`) | helper |
|
|
156
|
-
| Rendered pages, total bytes | 500, 32 MiB (`maxPages`, `maxTotalBytes`) | helper |
|
|
157
|
-
| Middleware entries per route | 16 | source project |
|
|
158
|
-
| Asset file size | 16 MiB | generated project |
|
|
159
|
-
| Total unique asset bytes | 64 MiB | generated project |
|
|
160
|
-
| Static entries traversed | 10,000 | generated project |
|
|
161
|
-
| Directory depth | 20 | generated project |
|
|
162
|
-
|
|
163
|
-
Startup snapshots asset bytes in memory, and a reload can briefly hold two
|
|
164
|
-
snapshots. A large site is bounded by the generated project's memory, not by the
|
|
165
|
-
render step. For collections beyond these budgets, publish to an external asset
|
|
166
|
-
service and redirect; provider asset adapters are not implemented.
|
|
167
|
-
|
|
168
|
-
## Function budgets
|
|
169
|
-
|
|
170
|
-
The first two rows above are the sandbox's snapshot budgets: at most 127 guest
|
|
171
|
-
modules and 4 MiB of module source in one snapshot. They are deliberate — part
|
|
172
|
-
of what [function security](FUNCTION-SECURITY.md) promises about sandboxed guest
|
|
173
|
-
code — and the render step does not relax them for trusted generated content.
|
|
174
|
-
Serving a project that crosses either still fails at startup, naming the module
|
|
175
|
-
that crossed it:
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
ConfigError: Function source limit exceeded: /pages/reference.mjs (12841 bytes)
|
|
179
|
-
brings the snapshot to 4196103 bytes, over the total limit of 4194304 bytes
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
`prerenderPages` does not inherit that as a page ceiling. Before rendering it
|
|
183
|
-
measures each route's module closure, reading sources only, and packs the routes
|
|
184
|
-
into **passes** that each stay inside the budgets. It then builds one runtime per
|
|
185
|
-
pass, holding only that pass's snapshot, and renders that pass's pages. A render
|
|
186
|
-
that needs more than one pass logs `{event: 'prerender-passes', passes}`.
|
|
187
|
-
|
|
188
|
-
Nothing about the contract changes: all passes render before anything is
|
|
189
|
-
written, into one output directory that must not already exist, so a failure in
|
|
190
|
-
the last pass leaves no partial artifact — the same atomicity a single pass has.
|
|
191
|
-
Output filenames are checked for collision across passes, and `maxPages`,
|
|
192
|
-
`maxTotalBytes` and the returned fixtures count the whole render, not a pass.
|
|
193
|
-
|
|
194
|
-
Two consequences worth knowing:
|
|
195
|
-
|
|
196
|
-
- **A module shared by every page is paid for in every pass.** A template
|
|
197
|
-
middleware is counted once per pass, not once per render, so it costs bytes
|
|
198
|
-
against each pass's budget.
|
|
199
|
-
- **One route must still fit one snapshot.** A single route whose own modules
|
|
200
|
-
and their imports exceed the budgets cannot be split, and fails with the
|
|
201
|
-
collector's message. That is a route to make smaller, not a pass to add.
|
|
202
|
-
|
|
203
|
-
The `urlcode-docs` site rendered 62 documentation pages this way. That
|
|
204
|
-
repository has since been deleted, so no link is given; the runnable
|
|
205
|
-
version of the same pattern is [`examples/prerender`](../examples/prerender/README.md).
|
|
206
|
-
|
|
207
|
-
## Larger sites: generating the source project
|
|
208
|
-
|
|
209
|
-
The example keeps page content as reviewed literal `args` in YAML, which stays
|
|
210
|
-
readable and lets `dev` serve the site live. A site with hundreds of pages
|
|
211
|
-
instead generates its source project from host-prepared content: the build reads
|
|
212
|
-
its Markdown or data, compiles and sanitizes it in Node, writes a temporary
|
|
213
|
-
project whose routes carry that HTML as literal arguments, renders it with the
|
|
214
|
-
helper, and discards the temporary project.
|
|
215
|
-
|
|
216
|
-
That keeps every property intact — content is still reviewed input, guest code
|
|
217
|
-
still reads nothing from disk, and the published artifact is still inert. Two
|
|
218
|
-
things to hold onto:
|
|
219
|
-
|
|
220
|
-
- **Generated YAML is operator input.** It is written by your build, from your
|
|
221
|
-
content, and reviewed like any other deployment artifact. Route YAML never
|
|
222
|
-
gains the ability to name host code or a callback.
|
|
223
|
-
- **Sanitize before rendering, not after.** The template escapes the values it
|
|
224
|
-
interpolates, but content injected as raw HTML is published as written. Whatever
|
|
225
|
-
produces that HTML owns its safety.
|
|
226
|
-
|
|
227
|
-
The `urlcode-docs` site built itself this way before that repository was
|
|
228
|
-
retired. It was a working integration, not a deployment or performance claim.
|
|
229
|
-
|
|
230
|
-
## What this is not
|
|
231
|
-
|
|
232
|
-
Not a static-site generator: no Markdown, no sanitizer, no asset pipeline, no
|
|
233
|
-
incremental or watch build, no link checking, no sitemap. Not a way to make
|
|
234
|
-
native bodies readable. Not a template engine — the template is ordinary
|
|
235
|
-
middleware you write. Not a CLI command: prerendering is a step inside a build
|
|
236
|
-
that already runs JavaScript, so the helper is a library. Content compilation,
|
|
237
|
-
sanitization, search, asset assembly and deployment stay in the application.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Trusted build-time functions and middleware use ordinary Node imports, including
|
|
241
|
-
npm packages and dynamic imports. The pass planner applies source-graph budgets
|
|
242
|
-
only to `sandbox: true` routes; it does not parse trusted modules as sandbox
|
|
243
|
-
code. Page-count and output-byte budgets still apply to both modes. After
|
|
244
|
-
rendering, emit native page/file routes without middleware, then use the static
|
|
245
|
-
build target. Static hosting cannot execute middleware on incoming requests.
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Project direction
|
|
2
|
-
|
|
3
|
-
This page states the product boundary: what URLCode is, what it is not, how
|
|
4
|
-
application projects relate to the runtime, and the license it is released
|
|
5
|
-
under. It describes intent and
|
|
6
|
-
boundaries, not a delivery schedule; the [roadmap](../ROADMAP.md) owns sequence
|
|
7
|
-
and the [readiness register](RELEASE-READINESS.md) owns what is proven.
|
|
8
|
-
|
|
9
|
-
## Design principle: declarative-first
|
|
10
|
-
|
|
11
|
-
> Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
|
|
12
|
-
|
|
13
|
-
This is the default for human authors, coding agents, examples and framework
|
|
14
|
-
changes. Before writing application code, check the installed version's declarative
|
|
15
|
-
primitives, YAML configuration, policies, supported extensions and reusable recipes
|
|
16
|
-
or templates. Prefer the highest-level supported abstraction that satisfies the
|
|
17
|
-
requirement; do not recreate framework behavior in a lower-level handler.
|
|
18
|
-
|
|
19
|
-
Custom functions or middleware remain valid for application-specific behavior the
|
|
20
|
-
framework cannot express. Keep that code focused, explain the missing capability,
|
|
21
|
-
and report reusable gaps to the owning repository. Never invent YAML fields or
|
|
22
|
-
bypass target limits, sandbox isolation or operator grants to avoid custom code.
|
|
23
|
-
|
|
24
|
-
## What URLCode is
|
|
25
|
-
|
|
26
|
-
A portable runtime for programmable URL behavior. A project declares its public
|
|
27
|
-
URL surface in YAML, adds JavaScript only where declarative handlers are
|
|
28
|
-
not enough, and runs the same definition locally, in a container, or on operator
|
|
29
|
-
infrastructure. That JavaScript runs trusted, in the host process, like any
|
|
30
|
-
other project code, unless the project isolates a specific route with
|
|
31
|
-
`sandbox: true` (docs/SPIKE-DEFAULT-TRUST-MODEL.md) — a judgment call the
|
|
32
|
-
project makes per route, not a default the runtime imposes on all guest code.
|
|
33
|
-
The project format is deliberately bounded so a runtime can
|
|
34
|
-
validate it, inspect it, test it and eventually carry it across hosting providers.
|
|
35
|
-
|
|
36
|
-
Git owns route definitions and code. Operators own credentials, storage and
|
|
37
|
-
capability grants. Application data stays in the operator's systems.
|
|
38
|
-
|
|
39
|
-
## What URLCode is not
|
|
40
|
-
|
|
41
|
-
- **Not a URL shortener.** Core is redirects, validated responses, request
|
|
42
|
-
functions, middleware, pages, static assets and downloads. Stored short
|
|
43
|
-
links are not a core handler; the `urlcode-dynamic-link` extension package
|
|
44
|
-
that owned them has been retired and unpublished.
|
|
45
|
-
- **Not a general Node web framework.** There is no framework code to write
|
|
46
|
-
for routing, validation, middleware wiring or policies — those are declared
|
|
47
|
-
in YAML and enforced by the runtime. Function/middleware code that needs
|
|
48
|
-
isolation from the host (untrusted input, an unreviewed contribution, a
|
|
49
|
-
particularly sensitive secret) opts into `sandbox: true`, which runs it
|
|
50
|
-
inside WASM isolation with no ambient filesystem, network or Node APIs.
|
|
51
|
-
Behavior that cannot be expressed in the bounded contract of a sandboxed
|
|
52
|
-
route is rejected rather than emulated.
|
|
53
|
-
- **Not a hosting account system.** There is no end-user identity, billing or
|
|
54
|
-
public account surface. Management is a private operator API.
|
|
55
|
-
- **Not a provider configuration format.** Provider infrastructure settings do
|
|
56
|
-
not belong in route behavior YAML. A project must remain runnable on a laptop
|
|
57
|
-
or in a single container wherever else it is deployed.
|
|
58
|
-
|
|
59
|
-
## Application projects
|
|
60
|
-
|
|
61
|
-
Applications built on URLCode are ordinary consumers of the public runtime.
|
|
62
|
-
They exist to prove the contract is sufficient in practice, and they get no
|
|
63
|
-
private fork or privileged capability. If an application needs something the
|
|
64
|
-
runtime cannot express, that is a gap in the public contract to close in the
|
|
65
|
-
open, not a reason for a special path. See the [roadmap](../ROADMAP.md).
|
|
66
|
-
|
|
67
|
-
## Why: your AI should build your application, not your framework
|
|
68
|
-
|
|
69
|
-
Coding agents are good at infrastructure, so they build it every time: routing,
|
|
70
|
-
sessions, validation, middleware, security headers, static serving, redirects,
|
|
71
|
-
webhooks, admin plumbing, deployment glue, tests. The application the person
|
|
72
|
-
asked for arrives last, and the person then owns twenty thousand lines instead
|
|
73
|
-
of two thousand. Cheap generation makes unnecessary code cheap to create and
|
|
74
|
-
expensive to keep.
|
|
75
|
-
|
|
76
|
-
URLCode's answer is the one databases gave a generation ago. Nobody asks a
|
|
77
|
-
model to write B-tree traversal; it writes `SELECT * FROM customers WHERE id = ?`
|
|
78
|
-
and the database owns the machinery. One level up, a route should read
|
|
79
|
-
|
|
80
|
-
```yaml
|
|
81
|
-
/admin:
|
|
82
|
-
auth: { role: admin }
|
|
83
|
-
function: { source: functions/admin.mjs }
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
and the runtime should own how. The agent describes what; URLCode owns how.
|
|
87
|
-
YAML is not the innovation and neither is the runtime. The innovation is a
|
|
88
|
-
small, deterministic vocabulary that is optimized for two readers at once: the
|
|
89
|
-
person who opens `urlcode.yaml`, and the agent that writes it.
|
|
90
|
-
|
|
91
|
-
Three tests keep this from becoming a YAML replacement for every framework:
|
|
92
|
-
|
|
93
|
-
- **The boundary test.** Is an agent repeatedly generating this code across
|
|
94
|
-
unrelated projects? If yes, it is a candidate primitive, policy, recipe or
|
|
95
|
-
extension. If no, it stays application code.
|
|
96
|
-
- **The feature test.** Does this reduce what the agent has to know, generate,
|
|
97
|
-
debug or maintain? If yes, it belongs on the roadmap. A feature that exists
|
|
98
|
-
because other web frameworks have it does not.
|
|
99
|
-
- **The evidence test.** The framework grows from measured repetition, not
|
|
100
|
-
from a list of things applications might need.
|
|
101
|
-
|
|
102
|
-
The metric that matters is the **application-specific code ratio**: of the
|
|
103
|
-
lines an agent generated, how many are the idea and how many are plumbing. A
|
|
104
|
-
traditional build might be 2,900 lines of business logic inside 18,400; the
|
|
105
|
-
same application on URLCode should be the same 2,900 inside a few thousand.
|
|
106
|
-
Until a reproducible benchmark shows that ratio, the thesis is a hypothesis,
|
|
107
|
-
and [the roadmap](../ROADMAP.md) puts the benchmark before the features.
|
|
108
|
-
|
|
109
|
-
## License
|
|
110
|
-
|
|
111
|
-
The runtime is free and open-source software under the
|
|
112
|
-
[Apache License 2.0](../LICENSE). Contribution terms follow from the same
|
|
113
|
-
license; see [contributing](../CONTRIBUTING.md).
|
|
114
|
-
|
|
115
|
-
The license covers the runtime source in this repository. It grants no rights in
|
|
116
|
-
any application's own routes, content or data, and it is not a warranty or a
|
|
117
|
-
production-readiness claim — those are set by the
|
|
118
|
-
[readiness register](RELEASE-READINESS.md) and [security policy](../SECURITY.md).
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# Provider conformance and deployment evidence
|
|
2
|
-
|
|
3
|
-
The synthetic project in [`examples/provider-conformance`](../examples/provider-conformance)
|
|
4
|
-
exercises the common declarative runtime subset: redirect status/location, dropped
|
|
5
|
-
incoming queries, validated mapped queries, encoded path components, constant
|
|
6
|
-
responses, HEAD, method refusal, request-body limits and missing routes. It has
|
|
7
|
-
no bindings, function code, customer data or provider infrastructure configuration.
|
|
8
|
-
|
|
9
|
-
`test/provider-verification.test.ts` replays the same 12 versioned cases through
|
|
10
|
-
an actual local self-hosted HTTP server, a local Vercel Node handler, AWS payload
|
|
11
|
-
v2 and a freshly built Cloudflare artifact. This establishes local adapter
|
|
12
|
-
behavior only. It does not exercise provider ingress, deployment configuration,
|
|
13
|
-
TLS termination, production DNS, scaling or provider accounts.
|
|
14
|
-
|
|
15
|
-
## Record a deployment observation
|
|
16
|
-
|
|
17
|
-
First deploy the synthetic fixture into a disposable environment you own using
|
|
18
|
-
the [AWS](AWS.md), [Vercel](VERCEL.md) or [Cloudflare](CLOUDFLARE.md) adapter. Do not point this runner at a
|
|
19
|
-
third-party service or an unrelated production application: it sends three POST
|
|
20
|
-
requests to fixture paths, whose declared behavior has no side effects.
|
|
21
|
-
|
|
22
|
-
```js
|
|
23
|
-
import { verifyProviderDeployment } from '@jimhoyd/urlcode';
|
|
24
|
-
const evidence = await verifyProviderDeployment(
|
|
25
|
-
'vercel',
|
|
26
|
-
'https://your-owned-fixture.example',
|
|
27
|
-
{ timeoutMs: 3000, gitCommit: 'your-commit-id', release: 'your-release-id' }
|
|
28
|
-
);
|
|
29
|
-
console.log(JSON.stringify(evidence, null, 2));
|
|
30
|
-
if (!evidence.pass) process.exitCode = 1;
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The CLI form is `urlcode verify-provider --target vercel --origin https://owned-fixture.example [--timeout-ms 3000] [--release label] [--git-commit sha]`, where `--release` and `--git-commit` are the caller-supplied labels recorded in the report and `--timeout-ms` is the per-request deadline.
|
|
34
|
-
|
|
35
|
-
Targets are `self-hosted`, `aws`, `vercel` and `cloudflare`. The caller must supply
|
|
36
|
-
an HTTPS origin without credentials, path, query or fragment. TLS verification
|
|
37
|
-
is mandatory. The runner does not provision resources, read credentials, follow
|
|
38
|
-
redirects or fetch redirect destinations. Every request has an absolute deadline
|
|
39
|
-
(default 3 seconds, configurable 50–10,000 ms); the suite has a 60-second deadline.
|
|
40
|
-
Responses stop at 64 KiB and headers at 16 KiB. It sends at most 12 requests,
|
|
41
|
-
sequentially, using identity encoding. Timeout, oversized body and TLS failures
|
|
42
|
-
produce failed findings. Reports include no arbitrary response body/header data.
|
|
43
|
-
|
|
44
|
-
`runProviderConformance(target, transport, options)` supports local adapter
|
|
45
|
-
replays through an explicit callback. The transport receives an AbortSignal;
|
|
46
|
-
callbacks must honor it to release their own resources. The runner can bound
|
|
47
|
-
waiting for a custom callback, but cannot terminate arbitrary caller code.
|
|
48
|
-
Its report always says `evidence: "local-adapter"` and
|
|
49
|
-
`providerVerification: "unverified"`.
|
|
50
|
-
|
|
51
|
-
## Evidence interpretation
|
|
52
|
-
|
|
53
|
-
Reports have `schemaVersion: 1`, `fixtureVersion: 1`, target, origin, timestamp,
|
|
54
|
-
a fixture-case SHA-256, caller-supplied release/Git labels, request count, overall
|
|
55
|
-
`pass` and a finding for every case. Findings report expected/observed status
|
|
56
|
-
and which assertion failed. The case digest identifies probe expectations;
|
|
57
|
-
it is not an artifact or deployed configuration digest.
|
|
58
|
-
|
|
59
|
-
Live HTTP reports say `evidence: "deployment-http"` and
|
|
60
|
-
`providerVerification: "observed"`, including failed attempts. **Observed does
|
|
61
|
-
not mean passing**: inspect `pass` and every finding. Target/provider identity,
|
|
62
|
-
ownership and release labels are caller assertions, not independent attestation.
|
|
63
|
-
Save reports alongside a Git revision and deployment identity in the operator's
|
|
64
|
-
release records; review origins and labels before publishing them.
|
|
65
|
-
|
|
66
|
-
No real AWS, Vercel or Cloudflare deployment evidence is checked in. Provider
|
|
67
|
-
provisioning and deployment verification remain pending operator-owned accounts
|
|
68
|
-
and explicit deployment URLs. CI passing must never be reported as an actual
|
|
69
|
-
provider deployment result, independent security review, or soak/recovery proof.
|
|
70
|
-
|
|
71
|
-
## Remaining transport differences
|
|
72
|
-
|
|
73
|
-
AWS payload v2 coalesces repeated header values; its adapter conservatively
|
|
74
|
-
interprets comma-separated non-cookie headers as repeats. Vercel's local Node
|
|
75
|
-
adapter can observe raw header counts; real ingress may normalize first.
|
|
76
|
-
Cloudflare's Fetch interface can expose already-coalesced values and normalized
|
|
77
|
-
URLs. Encoded slashes, malformed URL syntax, repeated scalar headers and multiple
|
|
78
|
-
Set-Cookie delivery require target-specific edge checks beyond this common suite.
|
|
79
|
-
The local adapter regression suites retain those implementation-level checks.
|
|
80
|
-
|
|
81
|
-
This fixture intentionally has no policy settings. A passing common-subset
|
|
82
|
-
report does not establish compression, distributed throttling, caching, agent
|
|
83
|
-
policy updates or project-specific guarantees. Use project-specific deployment
|
|
84
|
-
verification and production operational checks in addition to this small suite.
|