@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,113 @@
|
|
|
1
|
+
# Untrusted function execution
|
|
2
|
+
|
|
3
|
+
Application code is untrusted even when it came from your own Git repository.
|
|
4
|
+
A compromised dependency, template or contribution must not inherit the URLCode
|
|
5
|
+
server's authority. Alpha.2 replaces alpha.1's Node execution entirely. There
|
|
6
|
+
is no `unsafe`, `trusted` or automatic host-execution fallback.
|
|
7
|
+
|
|
8
|
+
## Boundaries enforced now
|
|
9
|
+
|
|
10
|
+
- Function sources are parsed/snapshotted without importing them into Node.
|
|
11
|
+
- Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
|
|
12
|
+
exposed to the guest. Request/response/context use a JSON/string boundary.
|
|
13
|
+
- No `process`, `require`, Node built-ins, filesystem, shell, sockets, fetch,
|
|
14
|
+
WebSocket, workers, native extensions or ambient environment is available.
|
|
15
|
+
- Module resolution is restricted to the route's declared middleware and function relative JavaScript
|
|
16
|
+
dependency graphs inside the project. Symlink escapes, remote/bare imports and
|
|
17
|
+
dynamic imports in source fail. Runtime-created imports cannot broaden access.
|
|
18
|
+
- A fresh guest heap/module state per invocation prevents state crossing requests.
|
|
19
|
+
- 32 MiB guest heap, 512 KiB stack, source/input/output/header limits, bounded
|
|
20
|
+
concurrency, guest interruption and an independent worker termination deadline.
|
|
21
|
+
- External bindings are denied by default. Project YAML cannot self-authorize.
|
|
22
|
+
Operator grants are exact-name, route-scoped and pinned to configuration/source.
|
|
23
|
+
|
|
24
|
+
The guest API is intentionally narrower than Node or full Fetch; see the
|
|
25
|
+
[implemented contract](SPECIFICATION.md). Existing functions using Node/network
|
|
26
|
+
or binary/stream APIs must be rewritten for the supported profile or wait for a
|
|
27
|
+
reviewed capability implementation. Redirects need none of this machinery.
|
|
28
|
+
|
|
29
|
+
## Granting selected bindings
|
|
30
|
+
|
|
31
|
+
An application may request a named binding in YAML, but only an operator can
|
|
32
|
+
approve it. Inspect what the app requests without executing any module:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
urlcode permissions --project /srv/my-links
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This prints a proposed JSON shape with `version: 1`, `projectSha256` and `routes`.
|
|
39
|
+
It grants nothing. Review the code/configuration and keep only necessary bindings.
|
|
40
|
+
Save the policy **outside the application checkout**, in an operator-controlled
|
|
41
|
+
file; never let application authors or deployment artifacts overwrite it.
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"version": 1,
|
|
46
|
+
"projectSha256": "REPLACE_WITH_THE_REVIEWED_PROJECT_DIGEST",
|
|
47
|
+
"routes": {
|
|
48
|
+
"/customer/{id}": {
|
|
49
|
+
"env": ["API_MODE"],
|
|
50
|
+
"secrets": ["customer_api_key"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The placeholder deliberately does not validate. Use the actual digest produced
|
|
57
|
+
by inspection. Then, with values securely injected into the process:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
urlcode validate --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
|
|
61
|
+
urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
|
|
65
|
+
The JavaScript API accepts an equivalent operator-supplied `permissions` object.
|
|
66
|
+
Every config/module change invalidates the grant; inspect/review the new revision
|
|
67
|
+
before updating the operator file. Policies are read at startup, not hot-reloaded.
|
|
68
|
+
A failed development candidate leaves the previous approved snapshot running.
|
|
69
|
+
|
|
70
|
+
Granting a secret deliberately makes it available to every middleware and function
|
|
71
|
+
in that route. Middleware sources and their dependencies are included in the
|
|
72
|
+
approval digest; changes invalidate grants. The whole chain shares one fresh
|
|
73
|
+
guest heap and one execution deadline. Code can
|
|
74
|
+
include any granted data in its HTTP response. A sandbox cannot promise secrecy
|
|
75
|
+
from code authorized to read a value. Minimize grants, use scoped/short-lived
|
|
76
|
+
credentials and revoke/restart when needed. Other routes get none of that context.
|
|
77
|
+
|
|
78
|
+
## Native live-link storage
|
|
79
|
+
|
|
80
|
+
The optional `link` handler can read an explicitly operator-bound collection.
|
|
81
|
+
Its database is outside the project and public serving opens it read-only. This
|
|
82
|
+
is a native handler, not a guest capability: functions/middleware receive no SQL,
|
|
83
|
+
filesystem handle, database path or management token. Native link data changes do
|
|
84
|
+
not authorize new code or bindings. Management requires a separate operator CLI
|
|
85
|
+
or token-protected listener. See [dynamic links](DYNAMIC-LINKS.md).
|
|
86
|
+
|
|
87
|
+
## Next capability work
|
|
88
|
+
|
|
89
|
+
Outbound requests need a host-owned broker with explicit destination/method
|
|
90
|
+
allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
|
|
91
|
+
redirect revalidation, deadlines and byte/concurrency limits. Application YAML
|
|
92
|
+
must not grant those permissions. Persistent state needs similarly scoped access.
|
|
93
|
+
Until such brokers are implemented and tested, these capabilities are unavailable.
|
|
94
|
+
Provider adapters must preserve this boundary or reject deployment; they cannot
|
|
95
|
+
silently replace sandbox execution with unrestricted Node functions.
|
|
96
|
+
|
|
97
|
+
## Verification and remaining risk
|
|
98
|
+
|
|
99
|
+
Tests attempt constructor/eval escapes, Node/filesystem/shell/network imports,
|
|
100
|
+
runtime-created imports, cross-request prototype/state pollution, oversized
|
|
101
|
+
allocations, loops, unauthorized secret requests and stale/repo-local policies.
|
|
102
|
+
These are regression tests, not a proof of complete security.
|
|
103
|
+
|
|
104
|
+
The URLCode host, parser, QuickJS/WASM engine, native runtime and dependencies
|
|
105
|
+
remain trusted computing components that need patching and review. Guest heap
|
|
106
|
+
limits do not cap all host/WASM RSS; use OS/container memory/CPU/PID limits as an
|
|
107
|
+
additional layer. Native engine bugs or resource exhaustion remain residual risks.
|
|
108
|
+
For a public arbitrary-code/multi-tenant service, require independent security
|
|
109
|
+
review plus process/VM-level isolation and operational controls before launch.
|
|
110
|
+
Do not advertise this release as an audited hostile multi-tenant hosting platform.
|
|
111
|
+
|
|
112
|
+
Implementation references: [QuickJS/WASM project](https://github.com/justjake/quickjs-emscripten)
|
|
113
|
+
and its [runtime isolation/limits API](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSRuntime.md).
|
package/docs/HTTP.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# HTTP request and response configuration
|
|
2
|
+
|
|
3
|
+
Implemented in alpha.4. This is a documented HTTP subset, not a promise that every
|
|
4
|
+
HTTP feature is configurable. It builds on [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html).
|
|
5
|
+
|
|
6
|
+
```yaml
|
|
7
|
+
version: "1"
|
|
8
|
+
routes:
|
|
9
|
+
/echo:
|
|
10
|
+
methods: [POST]
|
|
11
|
+
request:
|
|
12
|
+
body:
|
|
13
|
+
required: true
|
|
14
|
+
maxBytes: 16384
|
|
15
|
+
contentTypes: [application/json]
|
|
16
|
+
format: json
|
|
17
|
+
function:
|
|
18
|
+
source: functions/echo.mjs
|
|
19
|
+
response:
|
|
20
|
+
headers:
|
|
21
|
+
Cache-Control: no-store
|
|
22
|
+
X-App: my-links
|
|
23
|
+
/go:
|
|
24
|
+
redirect:
|
|
25
|
+
url: https://example.com
|
|
26
|
+
status: 302
|
|
27
|
+
response:
|
|
28
|
+
headers:
|
|
29
|
+
Cache-Control: public, max-age=60
|
|
30
|
+
/status:
|
|
31
|
+
respond:
|
|
32
|
+
status: 200
|
|
33
|
+
json: {ok: true}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`functions/echo.mjs`:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
export default async function echo(request) {
|
|
40
|
+
return Response.json(await request.json());
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Requests
|
|
45
|
+
|
|
46
|
+
Keep ordinary routes short: omit `methods` to accept GET and HEAD. Set
|
|
47
|
+
`methods: [POST]` for a POST-only handler, or `methods: [GET, HEAD, POST]` for all
|
|
48
|
+
three. Use uppercase method names. Explicit lists replace the defaults; GET does
|
|
49
|
+
not implicitly add HEAD when a list is supplied. The schema advertises the same
|
|
50
|
+
default as the runtime. No declaration is needed for the default 302 redirect
|
|
51
|
+
status or default `Cache-Control: no-store` on functions/redirects.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
| Field | Behavior |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `methods` | Allowed methods, default GET/HEAD; exact lists, 405 plus Allow on mismatch |
|
|
57
|
+
| `parameters` | Required/defaulted/typed path, query and header inputs; see the specification |
|
|
58
|
+
| `request.body.required` | Reject an empty body with 400; default false |
|
|
59
|
+
| `request.body.maxBytes` | 0–1048576; tighter per-route budget, enforced while reading fixed/chunked bodies; 413 on overflow |
|
|
60
|
+
| `request.body.contentTypes` | Exact lowercase MIME essences for nonempty bodies; parameters ignored; mismatch/missing type returns 415 |
|
|
61
|
+
| `request.body.format` | `text`: validate UTF-8; `json`: validate UTF-8, JSON media type and JSON syntax; malformed input returns 400 |
|
|
62
|
+
|
|
63
|
+
The operator request limit remains an upper bound; YAML cannot raise it. A route
|
|
64
|
+
without body policy keeps the existing server limit. A configured body policy
|
|
65
|
+
rejects nonidentity Content-Encoding for nonempty bodies; no automatic decompression.
|
|
66
|
+
Empty optional bodies skip media/format checks. Inputs are validated before the
|
|
67
|
+
handler; the original body remains available through function `request.text()` or
|
|
68
|
+
`request.json()`. No YAML body interpolation or automatic argument binding.
|
|
69
|
+
Request header inputs use `parameters` with `in: header`; this is validation,
|
|
70
|
+
not arbitrary modification or forwarding of the incoming request.
|
|
71
|
+
|
|
72
|
+
## Responses
|
|
73
|
+
|
|
74
|
+
`response.headers` maps HTTP names to literal strings. Names are case insensitive;
|
|
75
|
+
duplicate spellings and invalid names/control characters fail activation. YAML
|
|
76
|
+
values replace the same handler headers, including all prior Set-Cookie values.
|
|
77
|
+
Only `Set-Cookie` accepts a list, producing separate header lines:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
response:
|
|
81
|
+
headers:
|
|
82
|
+
Cache-Control: no-store
|
|
83
|
+
Set-Cookie:
|
|
84
|
+
- "theme=light; Path=/; SameSite=Lax; Secure"
|
|
85
|
+
- "notice=seen; Path=/; HttpOnly; SameSite=Lax; Secure"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use functions for dynamic cookies; never commit session credentials or secret
|
|
89
|
+
values into header literals. Header configuration applies to handler responses,
|
|
90
|
+
including declared error statuses, but not runtime validation/errors (400, 404,
|
|
91
|
+
405, 413, 415, 500, etc.). Defaults remain `no-store`, `nosniff` and a request ID.
|
|
92
|
+
Header policy is bounded to 64 keys/16 KiB; merged function headers remain bounded.
|
|
93
|
+
|
|
94
|
+
Framing, hop-by-hop headers, Location, Allow, range/cache validators,
|
|
95
|
+
Content-Encoding, X-Request-ID and X-Content-Type-Options are reserved to the
|
|
96
|
+
runtime/handler. Configure redirect URLs/status on `redirect`; asset content type,
|
|
97
|
+
cache and disposition on its own handler. Asset metadata cannot be overridden by
|
|
98
|
+
`response.headers`. On functions/declared responses, Content-Type may be configured;
|
|
99
|
+
JSON declarations require a JSON type. No response header secret interpolation.
|
|
100
|
+
|
|
101
|
+
`respond` is an additional native handler (exactly one handler per route):
|
|
102
|
+
|
|
103
|
+
- `status`: 200–599, default 200; 206 and 304 are reserved for native asset semantics.
|
|
104
|
+
- `text`: literal UTF-8 body, default content type text/plain.
|
|
105
|
+
- `json`: any JSON-compatible YAML value, serialized with application/json.
|
|
106
|
+
- Omit both for an empty body; declaring both fails. Body limit is 1 MiB.
|
|
107
|
+
- Status 204/205 cannot declare a nonempty body. HEAD always suppresses the body.
|
|
108
|
+
|
|
109
|
+
Functions still return their own Response/status/body. YAML header policy does
|
|
110
|
+
not replace function status/body. Asset handlers retain conditional/HEAD/range
|
|
111
|
+
behavior described in [assets](ASSETS.md). Use OPTIONS explicitly if you need a
|
|
112
|
+
declared response; merely adding a header does not implement CORS preflight.
|
|
113
|
+
|
|
114
|
+
## Still outside this contract
|
|
115
|
+
|
|
116
|
+
Automatic CORS/preflight policy, cookie parsing/signing, authentication, body JSON
|
|
117
|
+
Schema validation, multipart/file uploads, streaming, content negotiation,
|
|
118
|
+
WebSocket upgrades and proxies are not implemented. Do not advertise these as
|
|
119
|
+
supported just because raw headers can be declared. Compression negotiation,
|
|
120
|
+
security-header profiles, per-client throttling, User-Agent policy and HTTP
|
|
121
|
+
caching strategies exist only as optional, off-by-default
|
|
122
|
+
[policies](POLICIES.md); a project that declares none keeps the identity-only
|
|
123
|
+
behavior described here, and YAML `response.headers` beat any header a policy
|
|
124
|
+
would add. Future features need their own portable semantics and tests; unknown
|
|
125
|
+
YAML fields fail.
|
|
126
|
+
|
|
127
|
+
Middleware runs after route/method/input/body validation and before YAML response
|
|
128
|
+
header overrides. See [middleware](MIDDLEWARE.md) for ordering and native body
|
|
129
|
+
preservation rules.
|
package/docs/INSTALL.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Installing URLCode
|
|
2
|
+
|
|
3
|
+
URLCode is a Node CLI. Every channel below installs the same published tarball;
|
|
4
|
+
pick whichever fits how you already manage tools. All of them require
|
|
5
|
+
**Node.js 22.13 or newer**. The tarball ships plain JavaScript built from the
|
|
6
|
+
TypeScript source (`dist/`, with declarations; see [TypeScript](TYPESCRIPT.md)),
|
|
7
|
+
so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
|
|
8
|
+
|
|
9
|
+
Live short-link storage additionally needs a Node build carrying the patched
|
|
10
|
+
SQLite WAL fix. Run `urlcode doctor` after installing and check `liveLinks`;
|
|
11
|
+
everything except live links works on any supported build. See
|
|
12
|
+
[dynamic links](DYNAMIC-LINKS.md#node-build-requirement).
|
|
13
|
+
|
|
14
|
+
## npm
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install --global urlcode
|
|
18
|
+
urlcode --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Project-local, which is what an application repository should normally pin:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install --save-dev urlcode
|
|
25
|
+
npx urlcode validate
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Homebrew
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
brew tap jimhoyd-com/urlcode
|
|
32
|
+
brew install urlcode
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The tap's formula is generated from the published tarball for each release and
|
|
36
|
+
attached to the GitHub release as `urlcode.rb`.
|
|
37
|
+
|
|
38
|
+
## Install script
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
curl -fsSL https://raw.githubusercontent.com/jimhoyd-com/urlcode/main/install.sh | sh
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
It downloads the release tarball, verifies its SHA-256 against the release's
|
|
45
|
+
`SHA256SUMS`, and installs with npm. Options:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
curl -fsSL .../install.sh | sh -s -- --version 0.2.0 --prefix "$HOME/.local"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`--prefix` avoids needing privileges for a global npm directory; add
|
|
52
|
+
`$PREFIX/bin` to `PATH`. Piping a script into a shell means trusting the source
|
|
53
|
+
for that moment: to inspect first, download it, read it, then run it.
|
|
54
|
+
|
|
55
|
+
## Container
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" ghcr.io/jimhoyd-com/urlcode:0.2.0 \
|
|
59
|
+
serve --project /project --host 0.0.0.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The image runs the same built runtime, `node /opt/urlcode/dist/cli.js`, as its
|
|
63
|
+
entry point. Pin the digest rather than a tag for a deployment, and give the
|
|
64
|
+
container its own resource limits. See [operations](OPERATIONS.md).
|
|
65
|
+
|
|
66
|
+
## From source
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
git clone https://github.com/jimhoyd-com/urlcode.git
|
|
70
|
+
cd urlcode
|
|
71
|
+
make dev
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A clone runs the TypeScript source directly (`node src/cli.ts`, Node 22.18+),
|
|
75
|
+
with no build step; see [local development](LOCAL-DEVELOPMENT.md).
|
|
76
|
+
|
|
77
|
+
## Verify what you installed
|
|
78
|
+
|
|
79
|
+
Releases carry Sigstore provenance signed by the release workflow. Before
|
|
80
|
+
trusting a downloaded artifact:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
gh attestation verify urlcode-0.2.0.tgz --repo jimhoyd-com/urlcode \
|
|
84
|
+
--signer-workflow jimhoyd-com/urlcode/.github/workflows/release.yml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A signature establishes where an artifact came from. It is not a statement that
|
|
88
|
+
the release is safe for your workload, and it is not a production-readiness
|
|
89
|
+
claim; see [release readiness](RELEASE-READINESS.md) and
|
|
90
|
+
[release security](RELEASE-SECURITY.md).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Load testing a deployment
|
|
2
|
+
|
|
3
|
+
`urlcode benchmark` sends a project's own workload at a running runtime and
|
|
4
|
+
reports throughput, tail latency and how many requests were refused. It answers
|
|
5
|
+
one question — *does this deployment meet its budget under this load* — and
|
|
6
|
+
nothing else. It is not a capacity model, not a soak harness and not a
|
|
7
|
+
substitute for the drills in [release readiness](RELEASE-READINESS.md).
|
|
8
|
+
|
|
9
|
+
## Measure the deployment, not a fresh local snapshot
|
|
10
|
+
|
|
11
|
+
By default the command starts a private runtime and loads that. Useful for a
|
|
12
|
+
regression budget in CI; useless for judging a deployment, because it bypasses
|
|
13
|
+
your TLS termination, proxy, network and host.
|
|
14
|
+
|
|
15
|
+
`--target` sends the same workload at something already running:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
urlcode benchmark --project . --target https://links.example \
|
|
19
|
+
--requests 5000 --concurrency 8 --warmup 100 --max-p95-ms 50
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The project is still required: it supplies the routes and fixtures that make up
|
|
23
|
+
the workload. Load goes to the target; the local runtime is only consulted for
|
|
24
|
+
the plan. `rssMiB` is `null` in this mode, because the memory of the machine
|
|
25
|
+
running the benchmark says nothing about the machine under test.
|
|
26
|
+
|
|
27
|
+
Point `--target` only at systems you operate. A load generator aimed at someone
|
|
28
|
+
else's host is an attack, whatever the intent.
|
|
29
|
+
|
|
30
|
+
## Warm up before measuring
|
|
31
|
+
|
|
32
|
+
`--warmup N` sends and discards N requests first. A cold asset snapshot, an
|
|
33
|
+
empty connection pool and a just-started function worker are not what a latency
|
|
34
|
+
budget is about. Warm-up traffic reaches the deployment and is excluded from
|
|
35
|
+
every statistic; `warmupRequests` records how many.
|
|
36
|
+
|
|
37
|
+
## Reading the report
|
|
38
|
+
|
|
39
|
+
| Field | Meaning |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `pass` | Every request completed, none failed, and `p95Ms` met `--max-p95-ms` if given. |
|
|
42
|
+
| `completed` / `complete` | How many requests ran. Short of `requested` means the `--seconds` budget ended the run first — the numbers describe a shorter run than you asked for. |
|
|
43
|
+
| `failed` | Responses that did not match the expectation for that case. |
|
|
44
|
+
| `transportErrors` | Connections that never produced a response: refused, reset or timed out. Distinct from a deployment deliberately refusing work. |
|
|
45
|
+
| `shedResponses` | 503 and 504 responses — admission, function-pool capacity or a deadline. Not errors so much as the runtime protecting itself. |
|
|
46
|
+
| `p50Ms` / `p95Ms` / `p99Ms` | Latency percentiles over completed requests. |
|
|
47
|
+
| `statuses` | Full status histogram, so a "pass" that is secretly all redirects is visible. |
|
|
48
|
+
| `rssMiB` | Local mode only. |
|
|
49
|
+
|
|
50
|
+
## A worked example
|
|
51
|
+
|
|
52
|
+
Against the default starter, which has one function route:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
$ urlcode serve --project starters/default --port 3456
|
|
56
|
+
$ urlcode benchmark --project starters/default --target http://127.0.0.1:3456 \
|
|
57
|
+
--requests 60 --warmup 10 --concurrency 4
|
|
58
|
+
pass: false failed: 6 shedResponses: 6 p95Ms: 6.7
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Six of sixty requests were shed. Not a bug: `serve` runs **2 function workers**
|
|
62
|
+
by default, so a concurrency of 4 exceeds the pool and the runtime returns 503
|
|
63
|
+
rather than queueing without bound. Raising the pool:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$ urlcode serve --project starters/default --port 3457 --workers 8
|
|
67
|
+
$ urlcode benchmark ... --concurrency 4
|
|
68
|
+
pass: true failed: 0 shedResponses: 0 statuses: {"200":24,"302":36}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
That is the loop this tool exists for: measure, read `shedResponses`, tune the
|
|
72
|
+
[capacity controls](CAPACITY.md), measure again. More workers cost memory and
|
|
73
|
+
CPU; the right number is the one your workload and host justify, not the
|
|
74
|
+
largest one that makes a number go green.
|
|
75
|
+
|
|
76
|
+
## What a passing run does not prove
|
|
77
|
+
|
|
78
|
+
- **GET and HEAD only.** Function routes with request bodies, live-link writes
|
|
79
|
+
and the management API are not exercised. `workload` states this in every report.
|
|
80
|
+
- **Redirects are not followed**, so a redirect's destination is never loaded.
|
|
81
|
+
- **One client, one host, no slow peers.** Tail latency under adversarial
|
|
82
|
+
clients, connection churn or packet loss is not measured.
|
|
83
|
+
- **Not a soak.** `--seconds` caps at 300. Memory drift, file-descriptor leaks
|
|
84
|
+
and log-volume growth need a long run watched through
|
|
85
|
+
[monitoring](MONITORING.md).
|
|
86
|
+
- **A number from one environment is not a claim about another.** Record the
|
|
87
|
+
runtime and application revisions, host, and command with any figure you keep.
|
|
88
|
+
|
|
89
|
+
`scripts/operational-drills.ts` covers the adjacent ground — mixed
|
|
90
|
+
native/function/live-link load, an invalid reload, rollback and a quiesced
|
|
91
|
+
backup/restore — as a local proof, never a statement about production.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Local development
|
|
2
|
+
|
|
3
|
+
Use Node.js 22.18+ and npm (CI targets Node 22, 24 and 26). The runtime is
|
|
4
|
+
written in TypeScript and runs from source with no build step: `npm run dev`
|
|
5
|
+
is `node src/cli.ts`, which Node runs through its own type stripping (that is
|
|
6
|
+
why a contributor needs 22.18, while an installed package still runs on 22.13).
|
|
7
|
+
`npm run typecheck` is the type gate and part of `npm run verify`.
|
|
8
|
+
`npm run build` emits the JavaScript in `dist/` that the package and container
|
|
9
|
+
ship, together with the declarations; `dist` is never committed. Make is an optional
|
|
10
|
+
shortcut layer; npm and the CLI work on Windows, macOS and Linux. No global
|
|
11
|
+
package install, hosting account, database or Docker is needed for the local loop.
|
|
12
|
+
|
|
13
|
+
## Try the runtime
|
|
14
|
+
|
|
15
|
+
From the runtime checkout, `make dev` installs locked dependencies if needed and
|
|
16
|
+
starts the function/redirect starter at http://127.0.0.1:3000. Without Make, run `npm ci`
|
|
17
|
+
once, then `npm run dev`. Dependency installation requires npm registry access;
|
|
18
|
+
the examples themselves work locally.
|
|
19
|
+
|
|
20
|
+
Try `/hello/Ada` (sandboxed function) and `/go` (redirect).
|
|
21
|
+
For pages/files/downloads, run `make dev PROJECT=examples/assets` instead. Edit the files in
|
|
22
|
+
`starters/default/` to experiment. `dev` watches configuration, source and assets;
|
|
23
|
+
invalid edits leave the last valid snapshot running. Ctrl+C drains and stops it.
|
|
24
|
+
Runtime source changes under `src/` require restarting the dev command; project
|
|
25
|
+
reload is not a runtime-code watcher.
|
|
26
|
+
|
|
27
|
+
## Own an application
|
|
28
|
+
|
|
29
|
+
Run `make init DEST=../my-links`, then
|
|
30
|
+
`make dev PROJECT=../my-links`. The CLI equivalents from the runtime checkout:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npm run init -- ../my-links
|
|
34
|
+
npm run dev -- --project ../my-links
|
|
35
|
+
npm run validate -- --project ../my-links
|
|
36
|
+
npm run test:project -- --project ../my-links
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
There is one starter, containing both examples. Initialization never overwrites
|
|
40
|
+
an existing directory. Once created, edits belong to your app repository; upgrading
|
|
41
|
+
the runtime does not regenerate them. Each starter has a Makefile for its own
|
|
42
|
+
`dev`, `serve`, `validate`, `test` and `doctor` commands. It uses an installed
|
|
43
|
+
`urlcode`, or an explicit runtime command:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
cd ../my-links
|
|
47
|
+
make dev URLCODE='node /path/to/urlcode/src/cli.ts'
|
|
48
|
+
# Without Make or a global install:
|
|
49
|
+
node /path/to/urlcode/src/cli.ts dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Command reference (runtime checkout)
|
|
53
|
+
|
|
54
|
+
| Make | npm | Purpose |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `make setup` | `npm ci` | Install exact dependencies; replaces node_modules |
|
|
57
|
+
| `make dev` | `npm run dev` | Watched function/redirect starter, local dotenv |
|
|
58
|
+
| `make validate` | `npm run validate` | Validate the app and local bindings |
|
|
59
|
+
| `make test-project` | `npm run test:project` | App HTTP assertions, redirects not followed |
|
|
60
|
+
| `make test` | `npm test` | Runtime unit, HTTP and sandbox tests |
|
|
61
|
+
| — | `npm run typecheck` | Strict TypeScript check of runtime, scripts, tests and benchmarks |
|
|
62
|
+
| `make verify` | `npm run verify` | Lint, type check, syntax/JSON checks and runtime tests |
|
|
63
|
+
| — | `npm run build` | Emit `dist/` (stripped JavaScript and declarations); never committed |
|
|
64
|
+
| `make test-package` | `npm run test:package` | Actual archive install and starter tests; registry access |
|
|
65
|
+
| `make serve` | `npm run serve` | Fixed snapshot, no watcher or dotenv |
|
|
66
|
+
| `make doctor` | `npm run doctor` | Runtime/platform details |
|
|
67
|
+
|
|
68
|
+
Run `make help` for shortcuts. `PROJECT` defaults to `starters/default`; `HOST`
|
|
69
|
+
to `127.0.0.1`; `PORT` to `3000`. Quote paths containing spaces:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
make dev PROJECT="../my-links demo" PORT=3001
|
|
73
|
+
npm run dev -- --project "../my-links demo" --port 3001
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Make automatically runs `npm ci` when its dependency marker is missing or older
|
|
77
|
+
than package metadata. Use `make setup` after manually changing node_modules.
|
|
78
|
+
Do not run setup concurrently with a running dev server or tests.
|
|
79
|
+
|
|
80
|
+
## Environment and troubleshooting
|
|
81
|
+
|
|
82
|
+
`.env.local` belongs in the selected app directory and is ignored by Git; process
|
|
83
|
+
environment values take precedence. No starter requires secret values. Do not
|
|
84
|
+
copy placeholder credentials into a working secret store. External env/secret
|
|
85
|
+
bindings still need an operator policy outside the app, pinned to its config/code.
|
|
86
|
+
Inspect and set it up using the [security guide](FUNCTION-SECURITY.md); pass it
|
|
87
|
+
through the CLI, for example `npm run dev -- --project ../my-links --policy /path/to/policy.json`.
|
|
88
|
+
Local convenience never bypasses the function sandbox or grants permissions.
|
|
89
|
+
|
|
90
|
+
- Port busy: change `PORT=3001` or pass `--port 3001` through npm.
|
|
91
|
+
- Missing Make: use the npm commands; Make is not a runtime dependency.
|
|
92
|
+
- Invalid edits: run validation for diagnostics; fix the project and the watcher retries.
|
|
93
|
+
- Changed runtime source: stop and restart dev, then `npm run typecheck`. Changed app source: reload is automatic.
|
|
94
|
+
- Missing dependency or wrong Node: check `node --version`, then `npm ci`.
|
|
95
|
+
- Need access from another device: explicitly use `HOST=0.0.0.0` or `--host 0.0.0.0`;
|
|
96
|
+
this exposes the development listener to your network. Loopback remains the default.
|
|
97
|
+
|
|
98
|
+
Production deployment uses `serve` behind the HTTPS setup described in
|
|
99
|
+
[operations](OPERATIONS.md). These shortcuts do not provision providers or select a license.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Private management and durable mutation audit
|
|
2
|
+
|
|
3
|
+
Management binds only `127.0.0.1` or `::1`. Use an authenticated private tunnel
|
|
4
|
+
(e.g. SSH/VPN with loopback forwarding); never publish its port through a public
|
|
5
|
+
proxy or container port mapping. This is operator management, not an end-user
|
|
6
|
+
account service. Browser Origin requests remain rejected.
|
|
7
|
+
|
|
8
|
+
## Individual credentials
|
|
9
|
+
|
|
10
|
+
Use `urlcode links api --project ./my-links --store /operator/links.sqlite
|
|
11
|
+
--auth-file /operator/management.json --host 127.0.0.1 --port 3001` (one line).
|
|
12
|
+
The policy is operator-owned, outside the application, at most 64 KiB and mode
|
|
13
|
+
600 on POSIX. Protect its parent directory and apply equivalent Windows ACLs.
|
|
14
|
+
It is not YAML and must never be placed in the application or Git.
|
|
15
|
+
|
|
16
|
+
Generate each token with `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`
|
|
17
|
+
on a trusted operator machine. Deliver it through your secret manager. Hash the
|
|
18
|
+
raw token using SHA-256 without a newline; the server expects `Authorization:
|
|
19
|
+
Bearer <token>`. Tokens must contain 43–256 base64url characters. Never use a
|
|
20
|
+
human password as a token. This synthetic policy illustrates the shape; replace
|
|
21
|
+
the hash with the real token hash and choose a short operational expiry:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"version": 1,
|
|
26
|
+
"credentials": [{
|
|
27
|
+
"id": "operator-alice",
|
|
28
|
+
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
29
|
+
"expires": "2026-10-01T00:00:00Z",
|
|
30
|
+
"collections": ["links"],
|
|
31
|
+
"actions": ["get", "list", "create", "update", "delete"]
|
|
32
|
+
}]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
At most 128 credentials; unique IDs and hashes; explicit collection and action
|
|
37
|
+
allowlists with no wildcards. Readers should receive only `get` and `list`.
|
|
38
|
+
An expired, removed or `"revoked": true` credential gets 401; a valid credential
|
|
39
|
+
outside its scope gets 403. The entire policy is validated on every authenticated
|
|
40
|
+
request. Malformed/unreadable policy fails closed with 503, without retaining an
|
|
41
|
+
old permissive copy. Write a replacement file with mode 600 and atomically rename
|
|
42
|
+
it over the policy. No restart is needed. Requests already authorized may finish;
|
|
43
|
+
revocation does not cancel an in-flight transaction. Credential administration is
|
|
44
|
+
an operator filesystem action, never exposed through this HTTP API.
|
|
45
|
+
|
|
46
|
+
Legacy `--token-file` remains available for local compatibility, with full access
|
|
47
|
+
to that listener's collection and actor `legacy-shared`. Prefer `--auth-file` for
|
|
48
|
+
attribution, expiry and revocation. They are mutually exclusive. These bearer
|
|
49
|
+
credentials do not establish verified human identity, MFA, SSO or session login.
|
|
50
|
+
Host administrators and custom embedding code remain trusted operators.
|
|
51
|
+
|
|
52
|
+
## Audit durability and recovery
|
|
53
|
+
|
|
54
|
+
The built-in SQLite writer creates an additive `urlcode_link_audit` table when
|
|
55
|
+
opening a store. Every successful create/update/delete (including CLI and embedded
|
|
56
|
+
store calls) records revision, UTC timestamp, actor, request ID, collection,
|
|
57
|
+
action and SHA-256 of the short code in the **same transaction** as the mutation.
|
|
58
|
+
The default local CLI actor is `local-operator`. Scoped HTTP management requires
|
|
59
|
+
an adapter explicitly declaring atomic audit support; the built-in store provides it.
|
|
60
|
+
|
|
61
|
+
An audit insert failure rolls back both mutation and revision. Conditional-write
|
|
62
|
+
conflicts and rejected requests produce no successful-mutation row. HTTP request
|
|
63
|
+
logs still describe failed/aborted requests and remain best effort. A client timeout
|
|
64
|
+
may follow a committed transaction: reconcile the revision and request ID before
|
|
65
|
+
retrying. A lost HTTP reply is not evidence that the transaction was rolled back.
|
|
66
|
+
|
|
67
|
+
Inspect the journal with an operator-only SQLite client, for example:
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
SELECT revision, timestamp, actor, request_id, collection, action, code_sha256
|
|
71
|
+
FROM urlcode_link_audit WHERE revision > 0 ORDER BY revision LIMIT 100;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
No token, URL, request body or raw short code is stored in the journal. Code hashes
|
|
75
|
+
are correlatable and guessable for short codes; restrict access accordingly.
|
|
76
|
+
This is a durable local transaction journal, **not tamper-evident external storage**.
|
|
77
|
+
An administrator with database access can change it. Export ordered revisions to
|
|
78
|
+
your protected collector/backup system and monitor lag, gaps and disk/WAL growth.
|
|
79
|
+
There is no automatic pruning: define retention and verify archival before any
|
|
80
|
+
operator purge. Keep audit records in backups. User_version remains 1 because
|
|
81
|
+
this table is additive; older readers work, but older writers do not emit audit
|
|
82
|
+
rows. Never roll a management writer back to an unaudited revision.
|