@jimhoyd/urlcode 0.3.0
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/CONTRIBUTING.md +80 -0
- package/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +342 -0
- package/ROADMAP.md +248 -0
- package/SECURITY.md +40 -0
- package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
- package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
- package/data/agents/ai-crawlers.json +1240 -0
- package/data/agents/crawlers.json +10515 -0
- package/data/agents/index.d.ts +3 -0
- package/data/agents/index.js +2453 -0
- package/data/agents/monitoring.json +1520 -0
- package/data/agents/seo.json +3865 -0
- package/dist/BUILD-MANIFEST.json +60 -0
- package/dist/adapters.js +59 -0
- package/dist/agent-lists.js +59 -0
- package/dist/assets.js +129 -0
- package/dist/authoring.js +68 -0
- package/dist/aws.js +110 -0
- package/dist/build-cloudflare.js +164 -0
- package/dist/cli.js +185 -0
- package/dist/client-address.js +85 -0
- package/dist/cloudflare.js +165 -0
- package/dist/compliance-rules/baseline.js +98 -0
- package/dist/compliance-rules/privacy.js +38 -0
- package/dist/compliance-rules/shared.js +62 -0
- package/dist/compliance-rules/strict.js +61 -0
- package/dist/compliance.js +219 -0
- package/dist/config-worker.js +9 -0
- package/dist/config.js +172 -0
- package/dist/errors.js +8 -0
- package/dist/function-sources.js +54 -0
- package/dist/function-worker.js +105 -0
- package/dist/functions.js +174 -0
- package/dist/guest-api.js +114 -0
- package/dist/header-validation.js +18 -0
- package/dist/http-policy.js +74 -0
- package/dist/http-response.js +84 -0
- package/dist/index.js +11 -0
- package/dist/link-api.js +136 -0
- package/dist/link-cli.js +141 -0
- package/dist/link-events.js +76 -0
- package/dist/link-records.js +31 -0
- package/dist/link-store-worker.js +150 -0
- package/dist/link-store.js +250 -0
- package/dist/logging.js +22 -0
- package/dist/management-policy.js +41 -0
- package/dist/match.js +124 -0
- package/dist/observability.js +242 -0
- package/dist/plugins.js +74 -0
- package/dist/policies/agents.js +248 -0
- package/dist/policies/cache.js +297 -0
- package/dist/policies/compression.js +187 -0
- package/dist/policies/security.js +0 -0
- package/dist/policies/throttle.js +131 -0
- package/dist/policies.js +142 -0
- package/dist/policy.js +55 -0
- package/dist/prerender.js +165 -0
- package/dist/project-tests.js +23 -0
- package/dist/readiness.js +216 -0
- package/dist/router.js +166 -0
- package/dist/runtime.js +244 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +75 -0
- package/dist/server.js +270 -0
- package/dist/site.js +0 -0
- package/dist/sqlite-version.js +6 -0
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/agent-lists.d.ts +5 -0
- package/dist/types/assets.d.ts +13 -0
- package/dist/types/authoring.d.ts +2 -0
- package/dist/types/aws.d.ts +35 -0
- package/dist/types/build-cloudflare.d.ts +14 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client-address.d.ts +9 -0
- package/dist/types/cloudflare.d.ts +75 -0
- package/dist/types/compliance-rules/baseline.d.ts +12 -0
- package/dist/types/compliance-rules/privacy.d.ts +6 -0
- package/dist/types/compliance-rules/shared.d.ts +24 -0
- package/dist/types/compliance-rules/strict.d.ts +9 -0
- package/dist/types/compliance.d.ts +119 -0
- package/dist/types/config-worker.d.ts +1 -0
- package/dist/types/config.d.ts +21 -0
- package/dist/types/errors.d.ts +7 -0
- package/dist/types/function-sources.d.ts +21 -0
- package/dist/types/function-worker.d.ts +1 -0
- package/dist/types/functions.d.ts +92 -0
- package/dist/types/guest-api.d.ts +16 -0
- package/dist/types/header-validation.d.ts +2 -0
- package/dist/types/http-policy.d.ts +39 -0
- package/dist/types/http-response.d.ts +43 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/link-api.d.ts +30 -0
- package/dist/types/link-cli.d.ts +36 -0
- package/dist/types/link-events.d.ts +27 -0
- package/dist/types/link-records.d.ts +11 -0
- package/dist/types/link-store-worker.d.ts +1 -0
- package/dist/types/link-store.d.ts +130 -0
- package/dist/types/logging.d.ts +9 -0
- package/dist/types/management-policy.d.ts +9 -0
- package/dist/types/match.d.ts +81 -0
- package/dist/types/observability.d.ts +104 -0
- package/dist/types/plugins.d.ts +27 -0
- package/dist/types/policies/agents.d.ts +86 -0
- package/dist/types/policies/cache.d.ts +82 -0
- package/dist/types/policies/compression.d.ts +41 -0
- package/dist/types/policies/security.d.ts +37 -0
- package/dist/types/policies/throttle.d.ts +55 -0
- package/dist/types/policies.d.ts +36 -0
- package/dist/types/policy.d.ts +21 -0
- package/dist/types/prerender.d.ts +31 -0
- package/dist/types/project-tests.d.ts +13 -0
- package/dist/types/readiness.d.ts +121 -0
- package/dist/types/router.d.ts +11 -0
- package/dist/types/runtime.d.ts +91 -0
- package/dist/types/scaffold.d.ts +17 -0
- package/dist/types/server.d.ts +37 -0
- package/dist/types/site.d.ts +12 -0
- package/dist/types/sqlite-version.d.ts +1 -0
- package/dist/types/types.d.ts +332 -0
- package/dist/types/vercel.d.ts +12 -0
- package/dist/types.js +177 -0
- package/dist/vercel.js +65 -0
- package/docs/AI-AUTHORING.md +113 -0
- package/docs/ASSETS.md +106 -0
- package/docs/AWS.md +77 -0
- package/docs/BEST-PRACTICES.md +267 -0
- package/docs/CAPACITY.md +208 -0
- package/docs/CLOUDFLARE.md +110 -0
- package/docs/COMPLIANCE.md +242 -0
- package/docs/DYNAMIC-LINKS.md +561 -0
- package/docs/FUNCTION-SECURITY.md +113 -0
- package/docs/HTTP.md +129 -0
- package/docs/INSTALL.md +90 -0
- package/docs/LOAD-TESTING.md +91 -0
- package/docs/LOCAL-DEVELOPMENT.md +99 -0
- package/docs/MANAGEMENT-SECURITY.md +82 -0
- package/docs/MIDDLEWARE.md +83 -0
- package/docs/MONITORING.md +132 -0
- package/docs/OBSERVABILITY.md +229 -0
- package/docs/OPERATIONAL-PROOF.md +42 -0
- package/docs/OPERATIONS.md +207 -0
- package/docs/ORGANIZATION.md +135 -0
- package/docs/PERFORMANCE.md +72 -0
- package/docs/PLUGINS.md +234 -0
- package/docs/POLICIES.md +489 -0
- package/docs/PRERENDER.md +193 -0
- package/docs/PROJECT-DIRECTION.md +51 -0
- package/docs/READINESS.md +125 -0
- package/docs/README.md +58 -0
- package/docs/RELEASE-READINESS.md +95 -0
- package/docs/RELEASE-SECURITY.md +72 -0
- package/docs/RESILIENCE.md +160 -0
- package/docs/ROUTING.md +95 -0
- package/docs/SANDBOX-REVIEW.md +59 -0
- package/docs/SCAFFOLDING.md +72 -0
- package/docs/SECURITY-AUDIT.md +124 -0
- package/docs/SITE.md +150 -0
- package/docs/SPECIFICATION.md +232 -0
- package/docs/SPIKE-EXTENSIONS.md +475 -0
- package/docs/STANDARDS.md +303 -0
- package/docs/STARTERS.md +51 -0
- package/docs/TUNNELS.md +75 -0
- package/docs/TYPESCRIPT.md +102 -0
- package/docs/VERCEL.md +107 -0
- package/docs/YAML-GUIDE.md +521 -0
- package/docs/YAML-REFERENCE.md +328 -0
- package/docs/policies/agents.md +182 -0
- package/docs/policies/cache.md +139 -0
- package/docs/policies/compression.md +168 -0
- package/docs/policies/security.md +160 -0
- package/docs/policies/throttle.md +103 -0
- package/examples/assets/.env.example +1 -0
- package/examples/assets/.gitattributes +2 -0
- package/examples/assets/Makefile +30 -0
- package/examples/assets/README.md +18 -0
- package/examples/assets/functions/hello.mjs +3 -0
- package/examples/assets/gitignore.template +7 -0
- package/examples/assets/public/about.html +2 -0
- package/examples/assets/public/assets/example.txt +1 -0
- package/examples/assets/public/guide.txt +1 -0
- package/examples/assets/tests/requests.json +44 -0
- package/examples/assets/urlcode.yaml +27 -0
- package/examples/aws/README.md +13 -0
- package/examples/aws/handler.mjs +5 -0
- package/examples/aws/public/index.html +3 -0
- package/examples/aws/public/notes.txt +1 -0
- package/examples/aws/template.yaml +33 -0
- package/examples/aws/tests/requests.json +9 -0
- package/examples/aws/urlcode.yaml +19 -0
- package/examples/cloudflare/README.md +18 -0
- package/examples/cloudflare/package.json +14 -0
- package/examples/cloudflare/tests/requests.json +9 -0
- package/examples/cloudflare/urlcode.yaml +24 -0
- package/examples/cloudflare/wrangler.toml +5 -0
- package/examples/compliance/README.md +20 -0
- package/examples/compliance/rules.mjs +65 -0
- package/examples/cookbook/README.md +21 -0
- package/examples/cookbook/functions/choice.mjs +4 -0
- package/examples/cookbook/functions/echo.mjs +3 -0
- package/examples/cookbook/functions/hello.mjs +3 -0
- package/examples/cookbook/functions/text.mjs +3 -0
- package/examples/cookbook/middleware/headers.mjs +6 -0
- package/examples/cookbook/public/about.html +1 -0
- package/examples/cookbook/public/assets/index.html +1 -0
- package/examples/cookbook/public/assets/site.css +1 -0
- package/examples/cookbook/public/favicon.svg +1 -0
- package/examples/cookbook/public/guide.txt +1 -0
- package/examples/cookbook/public/llms.txt +6 -0
- package/examples/cookbook/routes/code.yaml +55 -0
- package/examples/cookbook/routes/files.yaml +17 -0
- package/examples/cookbook/routes/policies.yaml +28 -0
- package/examples/cookbook/routes/redirects.yaml +38 -0
- package/examples/cookbook/routes/responses.yaml +26 -0
- package/examples/cookbook/tests/requests.json +254 -0
- package/examples/cookbook/urlcode.yaml +22 -0
- package/examples/live-links/README.md +11 -0
- package/examples/live-links/tests/requests.json +6 -0
- package/examples/live-links/urlcode.yaml +16 -0
- package/examples/monitoring/blackbox-jobs.yaml +37 -0
- package/examples/monitoring/prometheus-rules.yaml +88 -0
- package/examples/monitoring/prometheus-scrape.yaml +20 -0
- package/examples/monitoring/vector.toml +65 -0
- package/examples/prerender/README.md +48 -0
- package/examples/prerender/functions/page.mjs +6 -0
- package/examples/prerender/middleware/template.mjs +29 -0
- package/examples/prerender/prerender.d.mts +4 -0
- package/examples/prerender/prerender.mjs +63 -0
- package/examples/prerender/tests/requests.json +39 -0
- package/examples/prerender/urlcode.yaml +50 -0
- package/examples/tunnel/dev-with-ngrok.sh +61 -0
- package/examples/vercel/README.md +13 -0
- package/examples/vercel/api/index.js +5 -0
- package/examples/vercel/package.json +8 -0
- package/examples/vercel/public/index.html +3 -0
- package/examples/vercel/public/notes.txt +1 -0
- package/examples/vercel/tests/requests.json +48 -0
- package/examples/vercel/urlcode.yaml +19 -0
- package/examples/vercel/vercel.json +10 -0
- package/llms.txt +46 -0
- package/package.json +123 -0
- package/schemas/urlcode.schema.json +1230 -0
- package/starters/default/.gitattributes +1 -0
- package/starters/default/Makefile +30 -0
- package/starters/default/README.md +34 -0
- package/starters/default/functions/hello.mjs +3 -0
- package/starters/default/gitignore.template +8 -0
- package/starters/default/middleware/headers.mjs +6 -0
- package/starters/default/routes/functions.yaml +20 -0
- package/starters/default/routes/marketing/links.yaml +7 -0
- package/starters/default/starter.json +5 -0
- package/starters/default/tests/requests.json +56 -0
- package/starters/default/urlcode.yaml +8 -0
|
@@ -0,0 +1,193 @@
|
|
|
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, so the
|
|
6
|
+
sandbox, its deadline and its memory budget are not on the request path at all.
|
|
7
|
+
|
|
8
|
+
The shared orchestration ships as a build helper, `@jimhoyd/urlcode/prerender`, and the
|
|
9
|
+
runnable recipe is [`examples/prerender`](../examples/prerender/README.md), which
|
|
10
|
+
consumes it. Both are covered by `test/prerender.test.ts`. Everything here uses
|
|
11
|
+
the existing runtime: prerendering adds no field to route YAML, no CLI command
|
|
12
|
+
and no capability.
|
|
13
|
+
|
|
14
|
+
## Why render ahead of time
|
|
15
|
+
|
|
16
|
+
A function response body is readable by middleware through `text()`. A native
|
|
17
|
+
`page`, `static`, `download`, `redirect`, `respond` or `link` body is not — that
|
|
18
|
+
opacity is a deliberate contract, not a gap, and it is what keeps native file
|
|
19
|
+
serving free of guest code. See [middleware](MIDDLEWARE.md).
|
|
20
|
+
|
|
21
|
+
So a shared template cannot be wrapped around a native file at request time. The
|
|
22
|
+
answer is to apply the template while the content is still a function response,
|
|
23
|
+
and publish the result:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
function + template middleware ──render once──▶ HTML file ──▶ page route
|
|
27
|
+
(sandbox, build time) (no sandbox)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The alternative — reading Markdown through `next().text()` on a native route —
|
|
31
|
+
is not supported and should not be attempted. Prepare content at build time.
|
|
32
|
+
|
|
33
|
+
## The build helper
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import {prerenderPages, assertNativeProject, pageFileName} from '@jimhoyd/urlcode/prerender';
|
|
37
|
+
|
|
38
|
+
const rendered = await prerenderPages('./render-source', './out/pages', {
|
|
39
|
+
origin: 'https://docs.example', // what a page sees as its own origin
|
|
40
|
+
});
|
|
41
|
+
// rendered.pages → [{path: '/guide', file: 'guide.html', bytes: 531}, …]
|
|
42
|
+
// rendered.fixtures → byte-for-byte GET and empty HEAD cases, ready to extend
|
|
43
|
+
// rendered.count, rendered.bytes, rendered.directory
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`prerenderPages` owns everything that is easy to get wrong and nothing that is
|
|
47
|
+
site-specific. It activates the source project, selects the active literal GET
|
|
48
|
+
function routes, renders each one through its middleware, checks the status and
|
|
49
|
+
content type, enforces the budgets, derives and validates a safe output filename,
|
|
50
|
+
writes the files and closes the runtime — then hands back metadata. It does not
|
|
51
|
+
write a project, choose response headers, copy assets or compile content: the
|
|
52
|
+
caller assembles a project, or a generated include, from `pages` and `fixtures`.
|
|
53
|
+
|
|
54
|
+
`assertNativeProject(project, {allow})` activates a project and proves it cannot
|
|
55
|
+
execute guest code to answer a request: every route is one of the allowed native
|
|
56
|
+
handlers and none carries middleware. `allow` defaults to `['page', 'static',
|
|
57
|
+
'download']`, which is what a real site serves; narrow it to `['page']` for a
|
|
58
|
+
page-only artifact. Run it on the **final assembled site**, not only on the
|
|
59
|
+
rendered pages, so what you deploy is what was checked.
|
|
60
|
+
|
|
61
|
+
| Option | Default | Meaning |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `origin` | `http://localhost` | HTTP(S) origin a render sees; no path or credentials |
|
|
64
|
+
| `fileName` | `pageFileName` | Route path to filename; the path and the result are both validated either way |
|
|
65
|
+
| `ignoreUnrenderable` | `false` | Allow source routes this build will not render |
|
|
66
|
+
| `maxPages` | 500 | Pages in one render |
|
|
67
|
+
| `maxPageBytes` | 512 KiB | Bytes per rendered page |
|
|
68
|
+
| `maxTotalBytes` | 32 MiB | Bytes across the whole render |
|
|
69
|
+
| `log` | none | `{event: 'prerendered', path, file, bytes}` per page |
|
|
70
|
+
|
|
71
|
+
## What the helper guarantees
|
|
72
|
+
|
|
73
|
+
The runtime enforces its own protections when a generated project activates:
|
|
74
|
+
asset declarations reject absolute paths, traversal, dot segments, symlinks,
|
|
75
|
+
hardlinks and nonregular files, and static trees refuse `urlcode.yaml`,
|
|
76
|
+
`package.json`, `package-lock.json`, hidden entries, `node_modules` and
|
|
77
|
+
`.pem/.key/.p12/.pfx/.env` files. See [assets](ASSETS.md).
|
|
78
|
+
|
|
79
|
+
Those checks fail a deployment. The helper fails the build earlier and more
|
|
80
|
+
specifically:
|
|
81
|
+
|
|
82
|
+
- **Filenames are validated, never trusted.** `pageFileName` maps one route path
|
|
83
|
+
to one flat name: segments joined with `~`, which cannot occur in a segment, so
|
|
84
|
+
`/a/b` (`a~b.html`) and `/a-b` (`a-b.html`) are distinct rather than a silent
|
|
85
|
+
collision, and the mapping stays injective for every accepted path. Dots,
|
|
86
|
+
underscores and mixed case are fine, so a docs URL like `/docs/ASSETS.md`
|
|
87
|
+
works. Parameters, wildcards, traversal and dot segments are rejected. A custom
|
|
88
|
+
`fileName` hook is allowed — hashing the route is a reasonable choice — but its
|
|
89
|
+
result goes through the same check: a flat name, no leading dot, not a
|
|
90
|
+
protected name, `.html`, and unique **case-insensitively**, because on macOS
|
|
91
|
+
and Windows two names differing only in case are one file.
|
|
92
|
+
- **Directories may not overlap.** In either direction: a build must not write
|
|
93
|
+
into the reviewed source, nor read a source nested inside its output.
|
|
94
|
+
- **Nothing is written until everything renders.** Pages are held in memory and
|
|
95
|
+
written only after the last one passes, and the pages directory must not
|
|
96
|
+
already exist — it is created, along with any missing parents, only once every
|
|
97
|
+
render has succeeded, so a failed build creates nothing at all. An existing
|
|
98
|
+
pages directory is refused with an error carrying `code: 'EEXIST'`. If your
|
|
99
|
+
artifact has a root above that directory, claiming it is yours: check it before
|
|
100
|
+
calling, and let the helper create it as a parent after the render.
|
|
101
|
+
- **Every render is checked.** A non-200 status, a content type that is not
|
|
102
|
+
`text/html`, an empty body, an oversized body or an exceeded aggregate budget
|
|
103
|
+
fails the build instead of publishing a file.
|
|
104
|
+
- **Bytes are preserved.** The response body is a byte array, not a string. It is
|
|
105
|
+
kept as a `Buffer` through the file and its fixture alike, so a multi-byte
|
|
106
|
+
character is never re-encoded or truncated.
|
|
107
|
+
- **Skipping is explicit.** By default a source route the build would not render
|
|
108
|
+
fails it, because silently rendering a subset publishes an incomplete site that
|
|
109
|
+
looks whole. Pass `ignoreUnrenderable` when a mixed project is intended.
|
|
110
|
+
- **The runtime is always closed.** In a `finally`, so a failing build exits
|
|
111
|
+
instead of hanging on its worker threads.
|
|
112
|
+
|
|
113
|
+
The helper is operator build tooling. It runs in Node with normal filesystem
|
|
114
|
+
access because it is not guest code; nothing here gives the sandbox a filesystem,
|
|
115
|
+
and no host-code fallback is introduced. It is a separate package export from the
|
|
116
|
+
runtime for that reason. Review it as you review any deployment tooling.
|
|
117
|
+
|
|
118
|
+
## Assembling a site
|
|
119
|
+
|
|
120
|
+
What the helper returns is deliberately not a project, because that is the part
|
|
121
|
+
every site does differently. A small site writes one `page` route per file, as
|
|
122
|
+
[`examples/prerender`](../examples/prerender/README.md) does in about twenty
|
|
123
|
+
lines. A larger one copies the rendered pages next to its own assets, adds
|
|
124
|
+
`static` and `download` routes and response security headers, keeps a committed
|
|
125
|
+
entry point and writes only a generated include, then extends `fixtures` with its
|
|
126
|
+
own cases before asserting the whole thing is native:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const rendered = await prerenderPages(renderSource, 'project/public/pages');
|
|
130
|
+
for (const page of rendered.pages)
|
|
131
|
+
config.routes[page.path] = {page: {file: `public/pages/${page.file}`}, response: {headers: security}};
|
|
132
|
+
await writeFile('project/generated/routes.yaml', stringify(config));
|
|
133
|
+
await writeFile('project/tests/requests.json', JSON.stringify([...rendered.fixtures, ...ownCases]));
|
|
134
|
+
await assertNativeProject('project');
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Applying your own `response.headers` is expected; the helper never chooses them
|
|
138
|
+
for you and never discards them.
|
|
139
|
+
|
|
140
|
+
A site that renders straight into the tree it serves, rather than into a staging
|
|
141
|
+
project, needs no copy step at all — point `prerenderPages` at the pages
|
|
142
|
+
directory inside the serving project, keeping the render source outside it.
|
|
143
|
+
|
|
144
|
+
## Limits worth knowing before you design a site
|
|
145
|
+
|
|
146
|
+
| Limit | Value | Where |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| Function/middleware response body | 1 MiB default (`--max-response-bytes`) | render step |
|
|
149
|
+
| Rendered page bytes | 512 KiB (`maxPageBytes`) | helper |
|
|
150
|
+
| Rendered pages, total bytes | 500, 32 MiB (`maxPages`, `maxTotalBytes`) | helper |
|
|
151
|
+
| Middleware entries per route | 16 | source project |
|
|
152
|
+
| Asset file size | 16 MiB | generated project |
|
|
153
|
+
| Total unique asset bytes | 64 MiB | generated project |
|
|
154
|
+
| Static entries traversed | 10,000 | generated project |
|
|
155
|
+
| Directory depth | 20 | generated project |
|
|
156
|
+
|
|
157
|
+
Startup snapshots asset bytes in memory, and a reload can briefly hold two
|
|
158
|
+
snapshots. A large site is bounded by the generated project's memory, not by the
|
|
159
|
+
render step. For collections beyond these budgets, publish to an external asset
|
|
160
|
+
service and redirect; provider asset adapters are not implemented.
|
|
161
|
+
|
|
162
|
+
## Larger sites: generating the source project
|
|
163
|
+
|
|
164
|
+
The example keeps page content as reviewed literal `args` in YAML, which stays
|
|
165
|
+
readable and lets `dev` serve the site live. A site with hundreds of pages
|
|
166
|
+
instead generates its source project from host-prepared content: the build reads
|
|
167
|
+
its Markdown or data, compiles and sanitizes it in Node, writes a temporary
|
|
168
|
+
project whose routes carry that HTML as literal arguments, renders it with the
|
|
169
|
+
helper, and discards the temporary project.
|
|
170
|
+
|
|
171
|
+
That keeps every property intact — content is still reviewed input, guest code
|
|
172
|
+
still reads nothing from disk, and the published artifact is still inert. Two
|
|
173
|
+
things to hold onto:
|
|
174
|
+
|
|
175
|
+
- **Generated YAML is operator input.** It is written by your build, from your
|
|
176
|
+
content, and reviewed like any other deployment artifact. Route YAML never
|
|
177
|
+
gains the ability to name host code or a callback.
|
|
178
|
+
- **Sanitize before rendering, not after.** The template escapes the values it
|
|
179
|
+
interpolates, but content injected as raw HTML is published as written. Whatever
|
|
180
|
+
produces that HTML owns its safety.
|
|
181
|
+
|
|
182
|
+
The [urlcode-docs showcase](https://github.com/jimhoyd-com/urlcode-docs) builds
|
|
183
|
+
its site this way. That is a working integration, not a deployment or performance
|
|
184
|
+
claim.
|
|
185
|
+
|
|
186
|
+
## What this is not
|
|
187
|
+
|
|
188
|
+
Not a static-site generator: no Markdown, no sanitizer, no asset pipeline, no
|
|
189
|
+
incremental or watch build, no link checking, no sitemap. Not a way to make
|
|
190
|
+
native bodies readable. Not a template engine — the template is ordinary
|
|
191
|
+
middleware you write. Not a CLI command: prerendering is a step inside a build
|
|
192
|
+
that already runs JavaScript, so the helper is a library. Content compilation,
|
|
193
|
+
sanitization, search, asset assembly and deployment stay in the application.
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
## What URLCode is
|
|
10
|
+
|
|
11
|
+
A portable runtime for programmable URL behavior. A project declares its public
|
|
12
|
+
URL surface in YAML, adds isolated JavaScript only where declarative handlers are
|
|
13
|
+
not enough, and runs the same definition locally, in a container, or on operator
|
|
14
|
+
infrastructure. The project format is deliberately bounded so a runtime can
|
|
15
|
+
validate it, inspect it, test it and eventually carry it across hosting providers.
|
|
16
|
+
|
|
17
|
+
Git owns route definitions and code. Operators own credentials, storage and
|
|
18
|
+
capability grants. Application data stays in the operator's systems.
|
|
19
|
+
|
|
20
|
+
## What URLCode is not
|
|
21
|
+
|
|
22
|
+
- **Not a URL shortener.** Short links are one handler beside redirects,
|
|
23
|
+
validated responses, request functions, middleware, pages, static assets and
|
|
24
|
+
downloads. The [live-link store](DYNAMIC-LINKS.md) is optional and single-host.
|
|
25
|
+
- **Not a general Node web framework.** Guest code runs inside WASM isolation
|
|
26
|
+
with no ambient filesystem, network or Node APIs. Behavior that cannot be
|
|
27
|
+
expressed in the bounded contract is rejected rather than emulated.
|
|
28
|
+
- **Not a hosting account system.** There is no end-user identity, billing or
|
|
29
|
+
public account surface. Management is a private operator API.
|
|
30
|
+
- **Not a provider configuration format.** Provider infrastructure settings do
|
|
31
|
+
not belong in route behavior YAML. A project must remain runnable on a laptop
|
|
32
|
+
or in a single container wherever else it is deployed.
|
|
33
|
+
|
|
34
|
+
## Application projects
|
|
35
|
+
|
|
36
|
+
Applications built on URLCode are ordinary consumers of the public runtime.
|
|
37
|
+
They exist to prove the contract is sufficient in practice, and they get no
|
|
38
|
+
private fork or privileged capability. If an application needs something the
|
|
39
|
+
runtime cannot express, that is a gap in the public contract to close in the
|
|
40
|
+
open, not a reason for a special path. See the [roadmap](../ROADMAP.md).
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
The runtime is free and open-source software under the
|
|
45
|
+
[Apache License 2.0](../LICENSE). Contribution terms follow from the same
|
|
46
|
+
license; see [contributing](../CONTRIBUTING.md).
|
|
47
|
+
|
|
48
|
+
The license covers the runtime source in this repository. It grants no rights in
|
|
49
|
+
any application's own routes, content or data, and it is not a warranty or a
|
|
50
|
+
production-readiness claim — those are set by the
|
|
51
|
+
[readiness register](RELEASE-READINESS.md) and [security policy](../SECURITY.md).
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Test every route, then measure it
|
|
2
|
+
|
|
3
|
+
Alpha.5 includes a local coverage gate and an assertion-aware project benchmark.
|
|
4
|
+
These validate a local snapshot, not the reachability of external redirect
|
|
5
|
+
services or the correctness of an entire production deployment.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
urlcode routes --project ../my-links
|
|
9
|
+
urlcode audit --project ../my-links --expect-routes 2
|
|
10
|
+
urlcode benchmark --project ../my-links --requests 1000 --concurrency 2 --max-p95-ms 50
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
All three activate/validate the project with the same isolated runtime and use
|
|
14
|
+
local environment loading like `test`. Pass an external `--policy` for explicitly
|
|
15
|
+
authorized bindings. No destination redirects are followed, credentials are not
|
|
16
|
+
printed, and no remote load-test target is accepted.
|
|
17
|
+
|
|
18
|
+
## Inventory and count reconciliation
|
|
19
|
+
|
|
20
|
+
`routes` reports each configured route's pattern, handler, exact allowed methods
|
|
21
|
+
and active/disabled/expired state. It includes routes from YAML includes. A
|
|
22
|
+
parameter pattern is one route; its possible URLs are not a finite route count.
|
|
23
|
+
A static mount is one route, even when it contains many files.
|
|
24
|
+
|
|
25
|
+
`audit --expect-routes N` compares N with the total configured count. Its summary
|
|
26
|
+
separately counts active, disabled and expired routes and groups by handler.
|
|
27
|
+
A mismatch exits nonzero. Keep N reviewed in your application CI so accidentally
|
|
28
|
+
removing a route cannot silently reduce the test workload. Change it intentionally
|
|
29
|
+
when adding/removing routes; do not calculate the expected value from the same YAML.
|
|
30
|
+
|
|
31
|
+
## Generated checks plus explicit examples
|
|
32
|
+
|
|
33
|
+
The audit generates GET/HEAD checks for concrete native redirects, declared
|
|
34
|
+
responses, pages/downloads and every snapshotted static file. It checks status,
|
|
35
|
+
redirect Location, file metadata/length and declared bodies where applicable.
|
|
36
|
+
It also checks literal disabled/expired routes for 404/410. Generated checks are
|
|
37
|
+
contract consistency checks; they cannot decide whether your intended destination
|
|
38
|
+
or content is correct. Keep independent expected outcomes in fixtures too.
|
|
39
|
+
|
|
40
|
+
Functions, parameterized routes, required inputs/bodies and non-GET/HEAD methods
|
|
41
|
+
need fixtures in `tests/requests.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
[
|
|
45
|
+
{"path":"/hello/Ada","status":200,"expectBody":"{\"message\":\"Hello, Ada!\"}"},
|
|
46
|
+
{"path":"/hello/Ada","method":"HEAD","status":200,"expectBody":""},
|
|
47
|
+
{"path":"/go","status":302,"expectHeaders":{"location":"https://example.com/"}},
|
|
48
|
+
{"path":"/go","method":"POST","status":405,"expectHeaders":{"allow":"GET, HEAD"}},
|
|
49
|
+
{"path":"/missing","status":404}
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each case may supply `method`, string-valued `headers`, a text `body`, expected
|
|
54
|
+
`status`, string-valued `expectHeaders`, and exact UTF-8 `expectBody`. Status is
|
|
55
|
+
required. A passing case needs at least one body/header assertion to count toward
|
|
56
|
+
coverage; status-only successes appear in `unassertedCases`. Choose assertions
|
|
57
|
+
that verify your intended business result, not just a generic header. Fixtures
|
|
58
|
+
are limited to 10,000 cases/16 MiB; checked response bodies to 16 MiB. Requests have
|
|
59
|
+
10-second transport timeouts. Failures do not stop subsequent checks. Status 0
|
|
60
|
+
means a transport/response-limit failure. Output reports case numbers/statuses,
|
|
61
|
+
not response bodies, header values or fixture URLs that may contain private data.
|
|
62
|
+
|
|
63
|
+
Coverage uses the route that actually matched. A literal route shadowing a
|
|
64
|
+
parameter example cannot count toward parameter coverage. Each active route and
|
|
65
|
+
allowed method needs a passing normal response (below 400); an intentionally
|
|
66
|
+
error-valued native `respond` check can cover its declared outcome. A negative
|
|
67
|
+
fixture alone cannot prove a function works normally. Disabled/expired routes
|
|
68
|
+
are counted separately and excluded from active coverage requirements. Inactive
|
|
69
|
+
parameter patterns still need explicit negative fixtures to exercise them.
|
|
70
|
+
|
|
71
|
+
`ready: true` requires a nonempty active project, matching expected count (when
|
|
72
|
+
supplied), zero failed checks and no uncovered active route/method combinations.
|
|
73
|
+
It means this local gate passed, not that all branches, parameter values or assets
|
|
74
|
+
have independent business assertions. Function routes intentionally serving only
|
|
75
|
+
errors cannot satisfy normal-response coverage in this release. Time-dependent
|
|
76
|
+
expiry is evaluated at audit start; avoid running a gate exactly at expiry.
|
|
77
|
+
|
|
78
|
+
`urlcode test` runs only explicit fixtures. `audit` adds generated native checks,
|
|
79
|
+
counts and coverage. Both execute locally and never follow redirect destinations.
|
|
80
|
+
Audits run sequentially to avoid mistaking worker saturation for a routing failure.
|
|
81
|
+
|
|
82
|
+
## Benchmark your actual project
|
|
83
|
+
|
|
84
|
+
The benchmark cycles generated checks and explicit successful GET/HEAD fixtures.
|
|
85
|
+
POST/PUT/PATCH/DELETE/OPTIONS and expected error cases are excluded. Function
|
|
86
|
+
GET/HEAD handlers still execute: use synthetic test data and reviewed bindings.
|
|
87
|
+
The workload is case-weighted, not a simulation of real user traffic. A short run
|
|
88
|
+
may not reach every case; compare `exercisedWorkloadCases` with `workloadCases`.
|
|
89
|
+
|
|
90
|
+
Output includes requested/completed count, assertion failures, status histogram,
|
|
91
|
+
startup time, throughput, p50/p95/p99 response time, process RSS, Node and OS.
|
|
92
|
+
Any wrong status/header/body, incomplete run or exceeded `--max-p95-ms` budget
|
|
93
|
+
exits nonzero. Warmup is zero and is reported explicitly. Client/server share one
|
|
94
|
+
process; RSS and latency are local measurements, not server-only production SLAs.
|
|
95
|
+
|
|
96
|
+
Defaults: 1,000 requests, concurrency 2, 30-second scheduling budget. Bounds:
|
|
97
|
+
1–100,000 requests, 1–32 concurrent requests, `--seconds` 1–300. In-flight requests
|
|
98
|
+
may finish after the scheduling budget, bounded by their timeout. Higher function
|
|
99
|
+
concurrency can legitimately cause 503 because the default pool has two workers.
|
|
100
|
+
Choose a latency budget from repeatable measurements on your intended host.
|
|
101
|
+
The existing synthetic `npm run benchmark -- 10000` is still a separate scale test.
|
|
102
|
+
|
|
103
|
+
## What a release should prove
|
|
104
|
+
|
|
105
|
+
| Check | Evidence to require |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Counts and coverage | Reviewed expected count; every active route/method covered; disabled/expired routes accounted for |
|
|
108
|
+
| Correct happy paths | Exact redirect destinations/status/query handling; function bodies/headers; representative parameter and asset examples |
|
|
109
|
+
| Invalid inputs | Missing/duplicate/wrong-type inputs; malformed paths/encoding; wrong methods; bad JSON/media type; oversized bodies |
|
|
110
|
+
| Response contracts | HEAD empty bodies, Allow headers, cookies, cache policy, download names/MIME; ETag/304 and range/206/416 fixtures |
|
|
111
|
+
| Configuration changes | Invalid candidate keeps last-good routes; valid reload updates behavior; removed routes are intentional |
|
|
112
|
+
| Code containment | Runtime security suite passes; no ambient filesystem/network access; grants narrow and revision-pinned |
|
|
113
|
+
| Capacity and failure | Representative mix and concurrency; low errors and repeatable latency; timeouts, overload recovery and memory over sustained runs |
|
|
114
|
+
| Deployment | Fresh install; real HTTPS/domain/health smoke; rollback; shutdown; logs/alerts; explicitly authorized destination reachability checks |
|
|
115
|
+
|
|
116
|
+
The runtime suite covers many generic protocol/security/reload cases. Apps must
|
|
117
|
+
supply their own business and boundary fixtures. Automated remote destination
|
|
118
|
+
health, redirect-chain/loop analysis, DNS/TLS checks, sustained soak/load profiles,
|
|
119
|
+
coverage by function branch and historical performance comparison remain planned.
|
|
120
|
+
Run the local audit in CI now; do not label a passing local audit “production certified.”
|
|
121
|
+
|
|
122
|
+
Routes with middleware need explicit request fixtures with meaningful response
|
|
123
|
+
assertions for every active method. Audit cannot infer their behavior from the
|
|
124
|
+
underlying redirect or asset handler, so it does not generate native checks for
|
|
125
|
+
those routes. The route inventory includes a middleware count.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# URLCode documentation
|
|
2
|
+
|
|
3
|
+
Start with [project direction](PROJECT-DIRECTION.md) for what URLCode is, what it
|
|
4
|
+
is not, how application projects fit, and the license.
|
|
5
|
+
|
|
6
|
+
For runtime 0.2.0. Use documentation pinned to your runtime revision.
|
|
7
|
+
`version: "1"` is the stable project-format contract for this release line.
|
|
8
|
+
|
|
9
|
+
| Goal | Start here |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Install the CLI | [Installation](INSTALL.md) |
|
|
12
|
+
| Generate placeholders from YAML | [Scaffolding](SCAFFOLDING.md) |
|
|
13
|
+
| Write YAML with examples | [YAML guide and recipes](YAML-GUIDE.md) |
|
|
14
|
+
| Look up every accepted field | [Generated field reference](YAML-REFERENCE.md), [JSON Schema](../schemas/urlcode.schema.json) |
|
|
15
|
+
| Let an AI build routes | [AI authoring guide](AI-AUTHORING.md), [llms.txt](../llms.txt) |
|
|
16
|
+
| Run examples | [25-route cookbook](../examples/cookbook/README.md), [prerender recipe](../examples/prerender/README.md), [small starter](STARTERS.md) |
|
|
17
|
+
| Explore a standalone application | [URLCode Shortener demo](https://github.com/jimhoyd-com/urlcode-shortener), [build retrospective](https://github.com/jimhoyd-com/urlcode-shortener/blob/main/docs/BUILD-RETROSPECTIVE.md) |
|
|
18
|
+
| Explore a static docs-site integration | [URLCode Docs project](https://github.com/jimhoyd-com/urlcode-docs), [build retrospective](https://github.com/jimhoyd-com/urlcode-docs/blob/main/docs/BUILD-RETROSPECTIVE.md) — synced from this repository, which remains the source of truth |
|
|
19
|
+
| Create/update short links live | [Dynamic links, storage and management API](DYNAMIC-LINKS.md) |
|
|
20
|
+
| Understand exact behavior | [Specification](SPECIFICATION.md), [routing](ROUTING.md), [HTTP](HTTP.md) |
|
|
21
|
+
| Reuse code around routes | [Middleware](MIDDLEWARE.md) |
|
|
22
|
+
| Throttle, block agents, set security headers, compress or cache from YAML | [Policies](POLICIES.md): [throttle](policies/throttle.md), [agents](policies/agents.md), [security](policies/security.md), [compression](policies/compression.md), [cache](policies/cache.md) |
|
|
23
|
+
| Add host behavior in operator code | [Plugins](PLUGINS.md) |
|
|
24
|
+
| Use the API from TypeScript | [TypeScript: shipped declarations, exports, build and fidelity](TYPESCRIPT.md) |
|
|
25
|
+
| Check declared configuration against standards-referenced rules | [Compliance](COMPLIANCE.md) |
|
|
26
|
+
| See how the runtime conforms to the standards it uses | [Standards audit](STANDARDS.md) |
|
|
27
|
+
| Serve pages/files/downloads | [Assets](ASSETS.md) |
|
|
28
|
+
| Generate robots.txt, sitemap.xml, favicon, security.txt and llms.txt | [Site conventions](SITE.md) |
|
|
29
|
+
| Publish a site with no request-time guest code | [Prerendering helper and recipe](PRERENDER.md) |
|
|
30
|
+
| Keep code and YAML readable | [Organization and readability practices](BEST-PRACTICES.md) |
|
|
31
|
+
| Organize YAML across folders | [Organization](ORGANIZATION.md) |
|
|
32
|
+
| Work locally | [Local development](LOCAL-DEVELOPMENT.md) |
|
|
33
|
+
| Share a local project publicly | [Tunnels](TUNNELS.md) |
|
|
34
|
+
| Watch a deployment | [Monitoring](MONITORING.md) |
|
|
35
|
+
| Wire your own monitoring or scrape metrics | [Observability](OBSERVABILITY.md) |
|
|
36
|
+
| Load test a deployment | [Load testing](LOAD-TESTING.md) |
|
|
37
|
+
| Deploy to Vercel | [Vercel adapter](VERCEL.md) |
|
|
38
|
+
| Deploy to AWS Lambda | [AWS adapter](AWS.md) |
|
|
39
|
+
| Deploy to Cloudflare Workers | [Cloudflare target](CLOUDFLARE.md) |
|
|
40
|
+
| Handle secrets/untrusted code | [Function security](FUNCTION-SECURITY.md) |
|
|
41
|
+
| Prove responses and counts | [Readiness](READINESS.md) |
|
|
42
|
+
| Estimate concurrency/memory | [Capacity and limits](CAPACITY.md), [measurements](PERFORMANCE.md) |
|
|
43
|
+
| Prepare for overload/DDoS/recovery | [Resilience playbook](RESILIENCE.md) |
|
|
44
|
+
| Deploy and roll back | [Operations](OPERATIONS.md) |
|
|
45
|
+
| Review security findings and gaps | [Internal security audit](SECURITY-AUDIT.md) |
|
|
46
|
+
| Assess release readiness | [Evidence and open gates](RELEASE-READINESS.md) |
|
|
47
|
+
| See unfinished work | [Roadmap](../ROADMAP.md) |
|
|
48
|
+
| Read the design behind policies, plugins and templates | [Extensions spike](SPIKE-EXTENSIONS.md) |
|
|
49
|
+
|
|
50
|
+
Examples are educational unless backed by the runnable cookbook/fixtures.
|
|
51
|
+
Infrastructure limits are deployment settings, not fields to invent in route YAML.
|
|
52
|
+
|
|
53
|
+
## Security and acceptance
|
|
54
|
+
|
|
55
|
+
- [Independent sandbox review package](SANDBOX-REVIEW.md)
|
|
56
|
+
- [Private management credentials and atomic audit](MANAGEMENT-SECURITY.md)
|
|
57
|
+
- [Operational drills and deployment acceptance](OPERATIONAL-PROOF.md)
|
|
58
|
+
- [Candidate signing and release security](RELEASE-SECURITY.md)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Release readiness
|
|
2
|
+
|
|
3
|
+
Status: stable 0.1 self-hosted release. Production approval remains specific to
|
|
4
|
+
the workload and deployment environment.
|
|
5
|
+
This register describes the current public runtime, not future promises.
|
|
6
|
+
Use the contract and docs from the same pinned commit as your installed runtime.
|
|
7
|
+
|
|
8
|
+
## What is aligned
|
|
9
|
+
|
|
10
|
+
- One portable YAML project, explicit includes, seven mutually exclusive handlers,
|
|
11
|
+
per-route middleware and consistent request/response validation.
|
|
12
|
+
- One starter with a function route first and an ordinary redirect second.
|
|
13
|
+
Clone urlcode-template or use `urlcode init`; neither requires a database.
|
|
14
|
+
- Native handlers avoid user-code execution unless middleware is attached.
|
|
15
|
+
- Live links are mutable records behind a declared route. Git owns definitions;
|
|
16
|
+
an optional external SQLite store owns records. This is single-host storage.
|
|
17
|
+
- Untrusted functions run in isolated QuickJS/WASM with no ambient filesystem,
|
|
18
|
+
network or Node APIs. Host bindings require external revision-pinned approval.
|
|
19
|
+
- The runtime is released under Apache-2.0.
|
|
20
|
+
|
|
21
|
+
## Regression evidence
|
|
22
|
+
|
|
23
|
+
`npm run verify` is the lint, syntax/schema-reference and unit/HTTP regression gate.
|
|
24
|
+
`npm run test:package` installs the packed artifact and exercises initialized apps,
|
|
25
|
+
route audits, bounded benchmarks, assets and live links. CI runs Node 22/24/26 on
|
|
26
|
+
Linux/macOS/Windows and tests the container under resource restrictions.
|
|
27
|
+
|
|
28
|
+
| Area | Covered behavior | Practical limit |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Routing and HTTP | Exact/parameter/static precedence, methods, inputs, assets, middleware and response assertions | Stable 0.1 contract; unsupported semantics reject rather than emulate |
|
|
31
|
+
| Isolation | Sandbox capability/permission boundaries, deadlines, memory and invalid outputs | Not an independent security assessment or multi-tenant service certification |
|
|
32
|
+
| Overload | Function/store queue caps; HTTP admission saturation, separate bounded probe budget, health availability and recovery after upload completion/disconnect | 64 application requests default; no fairness, upstream DDoS protection or end-to-end deadline |
|
|
33
|
+
| Worker replacement | Repeated guest deadlines shed load and the pool returns to service after backoff, rather than latching off for the life of the process | Store-connection replacement shares this logic but its failure branch has no automated test; a crash there is covered by reasoning and review only |
|
|
34
|
+
| Persistence | Committed writes visible to independent readers; concurrent CAS, restart and abrupt writer exit | SQLite on one host; no distributed availability |
|
|
35
|
+
| Shutdown | Full accepted store queue drains; new work rejects; repeated close shares completion | Existing deadlines can still fail; uncertain writes must be reconciled |
|
|
36
|
+
| Management | Token boundaries, body limits, origin rejection, conditional mutations, endpoint-specific Allow headers | Private operator API, not public end-user account management |
|
|
37
|
+
| Activation/recovery | Invalid reload retains last-good snapshot; corrupt revision metadata rejects activation | No deployment orchestration or automatic database repair |
|
|
38
|
+
| Packaging | Packed installation and starter examples tested; sensitive files excluded | No published npm/Homebrew release or provider adapter guarantee |
|
|
39
|
+
|
|
40
|
+
`npm audit --omit=dev` now runs in CI and fails the build on any runtime advisory;
|
|
41
|
+
development-only advisories are reported without blocking. Dependabot proposes npm,
|
|
42
|
+
GitHub Actions and base-image updates weekly. Actions and the container base image
|
|
43
|
+
are pinned by immutable SHA/digest, so a rebuild cannot silently change the runtime.
|
|
44
|
+
A passing audit is a dated check against known advisories, not proof of safety.
|
|
45
|
+
|
|
46
|
+
## Gates before production approval
|
|
47
|
+
|
|
48
|
+
These remain open. Record workload, runtime/app/image revisions, environment,
|
|
49
|
+
commands, results and owner for each exercise; do not convert a passing local
|
|
50
|
+
benchmark into a universal throughput claim.
|
|
51
|
+
|
|
52
|
+
1. Independent review of host/sandbox boundaries and authenticated management.
|
|
53
|
+
2. Sustained load and soak on intended hardware, through the actual TLS/proxy
|
|
54
|
+
path: successful throughput, tail latency, 503/504 rate, RSS and recovery.
|
|
55
|
+
`urlcode benchmark --target` measures a running deployment through its real
|
|
56
|
+
path and separates shed responses from transport errors; see
|
|
57
|
+
[load testing](LOAD-TESTING.md). The tool is GET/HEAD only and caps at 300
|
|
58
|
+
seconds, so it does not by itself close this gate.
|
|
59
|
+
Include mixed native/function/live-link workloads and slow peers.
|
|
60
|
+
3. A real backup/restore drill on a separate host/volume, with record/version
|
|
61
|
+
reconciliation and measured recovery time/data loss. Keep SQLite WAL files
|
|
62
|
+
consistent; copying a live main database file alone is not a backup.
|
|
63
|
+
4. Kill/restart, resource exhaustion, disk-full and rolling deployment/rollback
|
|
64
|
+
exercises with the chosen supervisor, ingress and persistent storage.
|
|
65
|
+
5. Alerting and ownership for sustained errors, latency, readiness, dropped logs,
|
|
66
|
+
disk space, restarts and backups. Pick service objectives for the actual app.
|
|
67
|
+
6. Stable-release support commitments before packaging/public reuse claims.
|
|
68
|
+
Private security reporting and the current support baseline are documented in
|
|
69
|
+
SECURITY.md.
|
|
70
|
+
|
|
71
|
+
License selection is resolved: URLCode is released under the Apache License 2.0,
|
|
72
|
+
`package.json` declares it, and the repository carries the full license text.
|
|
73
|
+
The remaining gates above are engineering and operational, not legal.
|
|
74
|
+
|
|
75
|
+
The full free-product roadmap additionally includes bulk interchange tooling,
|
|
76
|
+
installers/Homebrew, provider adapters and reusable templates/signals. Those
|
|
77
|
+
features are not implemented merely because
|
|
78
|
+
YAML has a portable design. Node process/container hosting is the supported
|
|
79
|
+
execution target today; AWS/Vercel/Cloudflare adapters and guest network/realtime
|
|
80
|
+
capabilities remain future work. See [roadmap](../ROADMAP.md).
|
|
81
|
+
|
|
82
|
+
The [internal security audit](SECURITY-AUDIT.md) records reproduced findings, fixes
|
|
83
|
+
and prioritized gaps. Its regressions supplement these gates; they do not replace
|
|
84
|
+
independent assessment or real deployment exercises.
|
|
85
|
+
|
|
86
|
+
## Hardening follow-up
|
|
87
|
+
|
|
88
|
+
Implemented: bounded YAML workers and aggregate source budgets, cooperative route
|
|
89
|
+
compilation deadline, loopback-only management, scoped/expiring/revocable operator
|
|
90
|
+
credentials, atomic SQLite mutation audits, executable local/CI operational drills,
|
|
91
|
+
and a main-only candidate signing/SBOM workflow.
|
|
92
|
+
|
|
93
|
+
Still required: [independent review](SANDBOX-REVIEW.md), [actual deployment proof](OPERATIONAL-PROOF.md),
|
|
94
|
+
and publication/support arrangements. The Apache-2.0 license and the 0.2.0 self-hosted
|
|
95
|
+
release do not close the security and deployment gates. See [release process](RELEASE-SECURITY.md).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Candidate and release security process
|
|
2
|
+
|
|
3
|
+
URLCode 0.2.0 is the Apache-2.0 self-hosted baseline; licensing is defined
|
|
4
|
+
in [LICENSE](../LICENSE). Two workflows share one audited build path and differ
|
|
5
|
+
only in what they do with its output:
|
|
6
|
+
|
|
7
|
+
- `candidate.yml` is dispatched manually on main and retains a **signed build
|
|
8
|
+
candidate** for 30 days without publishing it. Use it to review a commit
|
|
9
|
+
before tagging it.
|
|
10
|
+
- `release.yml` runs on a `v*` tag whose commit is already on main, and publishes
|
|
11
|
+
a GitHub release. Publication to npm and GHCR is opt-in per repository variable
|
|
12
|
+
(`PUBLISH_NPM`, `PUBLISH_CONTAINER`), so a release can be artifacts-only.
|
|
13
|
+
|
|
14
|
+
Neither workflow is a statement that a release is production-ready; see
|
|
15
|
+
[release readiness](RELEASE-READINESS.md).
|
|
16
|
+
|
|
17
|
+
1. Report vulnerabilities privately through [GitHub advisories](https://github.com/jimhoyd-com/urlcode/security/advisories/new).
|
|
18
|
+
The maintainer triages impact, confirms affected exact revisions, coordinates a
|
|
19
|
+
fix/retest privately, and publishes an advisory with upgrade guidance when safe.
|
|
20
|
+
Current reviewed main is the security support baseline; no guaranteed SLA/backports.
|
|
21
|
+
2. Change dependencies through protected PRs. Review upstream provenance/advisories,
|
|
22
|
+
lockfile integrity and tests. Direct dependencies are exact, npm installs use
|
|
23
|
+
`npm ci`, CI actions use full commit SHAs, and container bases use SHA-256 digests.
|
|
24
|
+
Dependabot proposes updates; it does not authorize merging. Never silently
|
|
25
|
+
refresh dependencies during a candidate build.
|
|
26
|
+
3. After protected main checks pass, a maintainer manually dispatches
|
|
27
|
+
`.github/workflows/candidate.yml` **on main**, or pushes a `v<version>` tag to
|
|
28
|
+
run `.github/workflows/release.yml`. The release workflow refuses a tag whose
|
|
29
|
+
commit is not an ancestor of main, refuses a tag that disagrees with
|
|
30
|
+
`package.json`, and refuses to build a private or unlicensed package. It reads and validates the digest-pinned Node image directly from Dockerfile,
|
|
31
|
+
installs without lifecycle scripts, verifies, package-tests and runs local drills.
|
|
32
|
+
It packs the runtime and creates a CycloneDX dependency SBOM plus a manifest
|
|
33
|
+
recording source commit, lockfile hash, engine versions and artifact hashes.
|
|
34
|
+
The package contains `dist/`, produced inside that build from the tagged
|
|
35
|
+
TypeScript sources by Node's type stripping (`scripts/build.ts`); `dist` is
|
|
36
|
+
never committed. The manifest records the Node version that stripped it, the
|
|
37
|
+
locked TypeScript version and a SHA-256 per emitted file
|
|
38
|
+
(`dist/BUILD-MANIFEST.json`), so a download can be verified by running
|
|
39
|
+
`npm run build` on the tagged commit with that Node version and comparing its
|
|
40
|
+
`dist/` file by file. The build strips types and rewrites specifier
|
|
41
|
+
extensions; it never bundles, minifies or transforms syntax, so every line of
|
|
42
|
+
`dist/x.js` is the corresponding line of `src/x.ts`. CI's `build-fidelity`
|
|
43
|
+
job builds twice and diffs the trees, so the transform is known to be
|
|
44
|
+
deterministic before a tag is cut.
|
|
45
|
+
4. The pinned official `actions/attest` action signs provenance for the package,
|
|
46
|
+
SBOM and manifest using short-lived GitHub OIDC/Sigstore credentials. No long-lived
|
|
47
|
+
signing key is stored. Signing permissions exist only in this manual job; build
|
|
48
|
+
commands run in a container without passing GitHub tokens. Candidate files are
|
|
49
|
+
retained as GitHub Actions artifacts for 30 days. A release additionally attaches
|
|
50
|
+
them to the GitHub release, and publishes to npm with `--provenance` and to GHCR
|
|
51
|
+
when those repository variables are enabled.
|
|
52
|
+
5. Download the candidate for the intended commit and verify **each file**, e.g.
|
|
53
|
+
`gh attestation verify urlcode-0.2.0.tgz --repo jimhoyd-com/urlcode --signer-workflow jimhoyd-com/urlcode/.github/workflows/candidate.yml --source-ref refs/heads/main --source-digest APPROVED_COMMIT_SHA`
|
|
54
|
+
(use `release.yml` as the signer workflow for a tagged release).
|
|
55
|
+
Check the verified provenance's source commit against the approved commit, and
|
|
56
|
+
compare package/SBOM hashes with the signed manifest. A signature establishes
|
|
57
|
+
provenance, not safety, reproducible bytes or production approval. See
|
|
58
|
+
[GitHub verification](https://cli.github.com/manual/gh_attestation_verify) and
|
|
59
|
+
[the official attestation action](https://github.com/actions/attest).
|
|
60
|
+
6. Before registry publication, assign patch/release ownership and retention, and
|
|
61
|
+
document version/support policy. Before claiming hostile multi-tenant or
|
|
62
|
+
deployment-specific readiness, close independent-review and deployment gates. Keep last-good verified artifacts and compatible policies;
|
|
63
|
+
rehearse rollback. Never overwrite a published artifact/version or downgrade a
|
|
64
|
+
management writer past its audit/security baseline.
|
|
65
|
+
|
|
66
|
+
The SBOM describes npm dependencies; it is not a complete OS/container SBOM. Hosted
|
|
67
|
+
runners and action runtimes remain platform-controlled. Digest pins improve supply
|
|
68
|
+
chain integrity but do not prove byte-for-byte reproducibility or engine safety.
|
|
69
|
+
Signing verification must be demonstrated on a successful main workflow run before
|
|
70
|
+
claiming a candidate has been signed. Workflow definition alone is not that evidence:
|
|
71
|
+
`release.yml` has never been executed, so no release has yet been produced or signed
|
|
72
|
+
by it, and the npm and GHCR publication paths are unproven until a real tag runs.
|