@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
- package/.claude/skills/urlcode-operations/SKILL.md +108 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +195 -255
- package/ROADMAP.md +143 -15
- package/SECURITY.md +31 -9
- package/dist/BUILD-MANIFEST.json +72 -47
- package/dist/adapters.js +4 -23
- package/dist/agent-lists.js +1 -1
- package/dist/agents-guide.js +113 -0
- package/dist/authoring-files.js +60 -0
- package/dist/authoring.js +11 -1
- package/dist/aws.js +4 -3
- package/dist/build-cloudflare.js +11 -24
- package/dist/build-static.js +134 -0
- package/dist/bulk.js +37 -0
- package/dist/capabilities.js +262 -0
- package/dist/capability-query.js +71 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +165 -34
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +9 -17
- package/dist/compliance-rules/privacy.js +7 -18
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/compliance.js +6 -8
- package/dist/conditions.js +88 -0
- package/dist/config.js +69 -6
- package/dist/context.js +155 -0
- package/dist/ecosystem-cli.js +88 -0
- package/dist/egress.js +98 -0
- package/dist/examples.js +92 -0
- package/dist/explain-cli.js +64 -0
- package/dist/explain.js +131 -0
- package/dist/extensions.js +231 -0
- package/dist/function-sources.js +49 -5
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +40 -6
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/manifest.js +109 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +7 -21
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/cache.js +8 -3
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +56 -15
- package/dist/prerender.js +100 -41
- package/dist/project-tests.js +3 -3
- package/dist/provider-verification.js +92 -0
- package/dist/proxy.js +44 -0
- package/dist/readiness.js +34 -11
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +45 -7
- package/dist/runtime.js +164 -64
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/schema-query.js +62 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -0
- package/dist/trusted-functions.js +210 -0
- package/dist/types/adapters.d.ts +7 -4
- package/dist/types/agent-lists.d.ts +0 -1
- package/dist/types/agents-guide.d.ts +17 -0
- package/dist/types/authoring-files.d.ts +10 -0
- package/dist/types/aws.d.ts +3 -1
- package/dist/types/build-cloudflare.d.ts +1 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +64 -0
- package/dist/types/capability-query.d.ts +24 -0
- package/dist/types/catalog.d.ts +65 -0
- package/dist/types/client-address.d.ts +0 -1
- package/dist/types/compliance-rules/baseline.d.ts +1 -9
- package/dist/types/compliance-rules/privacy.d.ts +1 -4
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance-rules/strict.d.ts +0 -5
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +21 -2
- package/dist/types/context.d.ts +66 -0
- package/dist/types/ecosystem-cli.d.ts +17 -0
- package/dist/types/egress.d.ts +46 -0
- package/dist/types/examples.d.ts +50 -0
- package/dist/types/explain-cli.d.ts +11 -0
- package/dist/types/explain.d.ts +95 -0
- package/dist/types/extensions.d.ts +177 -0
- package/dist/types/function-sources.d.ts +9 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +36 -6
- package/dist/types/init-with.d.ts +30 -0
- package/dist/types/interchange-cli.d.ts +16 -0
- package/dist/types/interchange.d.ts +42 -0
- package/dist/types/manifest.d.ts +79 -0
- package/dist/types/match.d.ts +1 -0
- package/dist/types/mcp-authoring.d.ts +92 -0
- package/dist/types/mcp.d.ts +12 -0
- package/dist/types/observability.d.ts +3 -14
- package/dist/types/operator-host.d.ts +8 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/policies/agents.d.ts +0 -2
- package/dist/types/policies/compression.d.ts +2 -0
- package/dist/types/policies/security.d.ts +0 -1
- package/dist/types/policy.d.ts +15 -4
- package/dist/types/project-tests.d.ts +3 -2
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +10 -3
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +11 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/schema-query.d.ts +12 -0
- package/dist/types/server.d.ts +1 -4
- package/dist/types/signals.d.ts +25 -0
- package/dist/types/site.d.ts +0 -1
- package/dist/types/tooling.d.ts +115 -0
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +71 -7
- package/dist/types/typescript-authoring.d.ts +12 -0
- package/dist/types/vercel.d.ts +3 -1
- package/dist/types/verify-deployment.d.ts +47 -0
- package/dist/types.js +37 -5
- package/dist/typescript-authoring.js +142 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +232 -15
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +3 -2
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +192 -0
- package/docs/CAPACITY.md +129 -32
- package/docs/CI.md +142 -0
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/COMPLIANCE.md +6 -9
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +398 -0
- package/docs/FRAMEWORK.md +198 -0
- package/docs/FUNCTION-SECURITY.md +129 -32
- package/docs/INSTALL.md +45 -12
- package/docs/INTERCHANGE.md +134 -0
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +29 -16
- package/docs/MONITORING.md +2 -19
- package/docs/NEXT-PHASE-PLAN.md +98 -0
- package/docs/NEXT-STEPS.md +634 -0
- package/docs/OBSERVABILITY.md +11 -18
- package/docs/OPEN-DECISIONS.md +212 -0
- package/docs/OPERATIONAL-PROOF.md +30 -31
- package/docs/OPERATIONS.md +29 -35
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +23 -309
- package/docs/PRERENDER.md +41 -1
- package/docs/PROJECT-DIRECTION.md +75 -8
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +87 -34
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +57 -35
- package/docs/RELEASE-SECURITY.md +116 -7
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +8 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +41 -1
- package/docs/SPECIFICATION.md +150 -29
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
- package/docs/SPIKE-CORE-LAYERING.md +337 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +6 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
- package/docs/SPIKE-MONOREPO.md +322 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/STATIC.md +94 -0
- package/docs/TOOLING.md +295 -0
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +82 -0
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/USABILITY-REVIEW.md +129 -0
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +205 -0
- package/docs/YAML-GUIDE.md +15 -479
- package/docs/YAML-REFERENCE.md +143 -22
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/contract.md +52 -0
- package/docs/policies/hardened.md +56 -0
- package/docs/policies/interoperability.md +169 -0
- package/docs/policies/operations.md +45 -0
- package/docs/yaml/assets.md +36 -0
- package/docs/yaml/conditions.md +20 -0
- package/docs/yaml/functions.md +160 -0
- package/docs/yaml/middleware.md +29 -0
- package/docs/yaml/organization.md +74 -0
- package/docs/yaml/policies.md +37 -0
- package/docs/yaml/redirects.md +64 -0
- package/docs/yaml/responses.md +57 -0
- package/docs/yaml/site.md +24 -0
- package/examples/assets/example.yaml +17 -0
- package/examples/aws/example.yaml +20 -0
- package/examples/cloudflare/example.yaml +19 -0
- package/examples/compliance/example.yaml +11 -0
- package/examples/conditions/README.md +12 -0
- package/examples/conditions/example.yaml +19 -0
- package/examples/conditions/tests/requests.json +13 -0
- package/examples/conditions/urlcode.yaml +24 -0
- package/examples/cookbook/README.md +8 -4
- package/examples/cookbook/example.yaml +17 -0
- package/examples/cookbook/functions/catalog.mjs +3 -0
- package/examples/cookbook/functions/fail.mjs +4 -0
- package/examples/cookbook/functions/items.mjs +3 -0
- package/examples/cookbook/functions/profile.mjs +3 -0
- package/examples/cookbook/functions/resource.mjs +3 -0
- package/examples/cookbook/functions/status.mjs +3 -0
- package/examples/cookbook/middleware/auth.mjs +48 -0
- package/examples/cookbook/middleware/body.mjs +15 -0
- package/examples/cookbook/middleware/bucket.mjs +29 -0
- package/examples/cookbook/middleware/cors.mjs +21 -0
- package/examples/cookbook/middleware/debug.mjs +13 -0
- package/examples/cookbook/middleware/envelope.mjs +11 -0
- package/examples/cookbook/middleware/errors.mjs +11 -0
- package/examples/cookbook/middleware/etag.mjs +18 -0
- package/examples/cookbook/middleware/locale.mjs +20 -0
- package/examples/cookbook/middleware/maintenance.mjs +10 -0
- package/examples/cookbook/middleware/methods.mjs +15 -0
- package/examples/cookbook/middleware/negotiate.mjs +20 -0
- package/examples/cookbook/middleware/referer.mjs +12 -0
- package/examples/cookbook/middleware/request-id.mjs +16 -0
- package/examples/cookbook/route-index.json +676 -0
- package/examples/cookbook/routes/middleware.yaml +126 -0
- package/examples/cookbook/tests/requests.json +526 -0
- package/examples/cookbook/urlcode.yaml +1 -0
- package/examples/egress/README.md +22 -0
- package/examples/egress/example.yaml +19 -0
- package/examples/egress/urlcode.yaml +19 -0
- package/examples/extensions/README.md +7 -0
- package/examples/extensions/example.yaml +21 -0
- package/examples/extensions/urlcode.yaml +25 -0
- package/examples/monitoring/example.yaml +8 -0
- package/examples/prerender/README.md +2 -2
- package/examples/prerender/example.yaml +16 -0
- package/examples/provider-conformance/README.md +12 -0
- package/examples/provider-conformance/example.yaml +14 -0
- package/examples/provider-conformance/urlcode.yaml +34 -0
- package/examples/tunnel/example.yaml +8 -0
- package/examples/vercel/example.yaml +19 -0
- package/llms-full.txt +3084 -0
- package/llms.txt +61 -21
- package/package.json +36 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
- package/recipes/authenticated-json-api/README.md +51 -0
- package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
- package/recipes/authenticated-json-api/recipe.yaml +34 -0
- package/recipes/authenticated-json-api/tests/requests.json +39 -0
- package/recipes/authenticated-json-api/urlcode.yaml +12 -0
- package/recipes/contact-form/README.md +25 -0
- package/recipes/contact-form/functions/contact.mjs +17 -0
- package/recipes/contact-form/recipe.yaml +33 -0
- package/recipes/contact-form/tests/requests.json +47 -0
- package/recipes/contact-form/urlcode.yaml +18 -0
- package/recipes/cors-api/README.md +16 -0
- package/recipes/cors-api/functions/items.mjs +3 -0
- package/recipes/cors-api/middleware/cors.mjs +21 -0
- package/recipes/cors-api/recipe.yaml +26 -0
- package/recipes/cors-api/tests/requests.json +65 -0
- package/recipes/cors-api/urlcode.yaml +12 -0
- package/recipes/health-page/README.md +13 -0
- package/recipes/health-page/recipe.yaml +23 -0
- package/recipes/health-page/tests/requests.json +36 -0
- package/recipes/health-page/urlcode.yaml +19 -0
- package/recipes/json-api/README.md +6 -0
- package/recipes/json-api/functions/echo.mjs +3 -0
- package/recipes/json-api/recipe.yaml +25 -0
- package/recipes/json-api/tests/requests.json +34 -0
- package/recipes/json-api/urlcode.yaml +12 -0
- package/recipes/middleware/README.md +34 -0
- package/recipes/middleware/functions/catalog.mjs +3 -0
- package/recipes/middleware/functions/fail.mjs +4 -0
- package/recipes/middleware/functions/items.mjs +3 -0
- package/recipes/middleware/functions/profile.mjs +3 -0
- package/recipes/middleware/functions/resource.mjs +3 -0
- package/recipes/middleware/functions/status.mjs +3 -0
- package/recipes/middleware/middleware/auth.mjs +48 -0
- package/recipes/middleware/middleware/body.mjs +15 -0
- package/recipes/middleware/middleware/bucket.mjs +29 -0
- package/recipes/middleware/middleware/cors.mjs +21 -0
- package/recipes/middleware/middleware/debug.mjs +13 -0
- package/recipes/middleware/middleware/envelope.mjs +11 -0
- package/recipes/middleware/middleware/errors.mjs +11 -0
- package/recipes/middleware/middleware/etag.mjs +18 -0
- package/recipes/middleware/middleware/locale.mjs +20 -0
- package/recipes/middleware/middleware/maintenance.mjs +10 -0
- package/recipes/middleware/middleware/methods.mjs +15 -0
- package/recipes/middleware/middleware/negotiate.mjs +20 -0
- package/recipes/middleware/middleware/referer.mjs +12 -0
- package/recipes/middleware/middleware/request-id.mjs +16 -0
- package/recipes/middleware/public/guide.txt +1 -0
- package/recipes/middleware/recipe.yaml +50 -0
- package/recipes/middleware/tests/requests.json +528 -0
- package/recipes/middleware/urlcode.yaml +127 -0
- package/recipes/protected-download/README.md +22 -0
- package/recipes/protected-download/files/report.txt +1 -0
- package/recipes/protected-download/recipe.yaml +31 -0
- package/recipes/protected-download/tests/requests.json +32 -0
- package/recipes/protected-download/urlcode.yaml +15 -0
- package/recipes/redirect/README.md +7 -0
- package/recipes/redirect/recipe.yaml +25 -0
- package/recipes/redirect/tests/requests.json +19 -0
- package/recipes/redirect/urlcode.yaml +9 -0
- package/recipes/static-plus-api/README.md +15 -0
- package/recipes/static-plus-api/functions/info.mjs +3 -0
- package/recipes/static-plus-api/public/assets/index.html +3 -0
- package/recipes/static-plus-api/public/assets/site.css +1 -0
- package/recipes/static-plus-api/public/index.html +8 -0
- package/recipes/static-plus-api/recipe.yaml +29 -0
- package/recipes/static-plus-api/tests/requests.json +56 -0
- package/recipes/static-plus-api/urlcode.yaml +23 -0
- package/recipes/typescript/README.md +8 -0
- package/recipes/typescript/functions/hello.ts +5 -0
- package/recipes/typescript/recipe.yaml +23 -0
- package/recipes/typescript/tests/requests.json +18 -0
- package/recipes/typescript/urlcode.yaml +5 -0
- package/recipes/webhook-receiver/README.md +20 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +27 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +23 -0
- package/schemas/recipe.schema.json +139 -0
- package/schemas/urlcode.schema.json +659 -110
- package/skills/urlcode/SKILL.md +119 -0
- package/starters/default/.github/workflows/urlcode.yml +23 -0
- package/starters/default/.mcp.json +12 -0
- package/starters/default/AGENTS.md +79 -0
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -41
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -36
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -9
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -561
- package/docs/MANAGEMENT-SECURITY.md +0 -82
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# Function execution: trusted by default, sandboxed opt-in
|
|
2
|
+
|
|
3
|
+
`function` and `middleware` routes run **trusted and unsandboxed by default**:
|
|
4
|
+
in the host process, with full Node, filesystem and network access, exactly
|
|
5
|
+
like any other project code (docs/SPIKE-DEFAULT-TRUST-MODEL.md). This is a
|
|
6
|
+
deliberate, maintainer-decided reversal of alpha.2's blanket sandbox — see
|
|
7
|
+
that spike document for the full rationale. It is a call the project makes,
|
|
8
|
+
not a property the runtime can verify: URLCode cannot know whether your code
|
|
9
|
+
is safe to trust, only whether you asked for isolation.
|
|
10
|
+
|
|
11
|
+
Declare `sandbox: true` on a route when its code specifically warrants
|
|
12
|
+
isolation: it processes input from a source the project doesn't fully trust
|
|
13
|
+
(a third-party webhook payload, for example), it is a contribution nobody on
|
|
14
|
+
the team has reviewed, or it handles a secret sensitive enough that a bug in
|
|
15
|
+
that one route should not be able to reach the rest of the process or the
|
|
16
|
+
filesystem. A sandboxed route runs in QuickJS inside WebAssembly, in a
|
|
17
|
+
separate worker thread, with none of the host access described below — its
|
|
18
|
+
guarantees are unchanged from every earlier release and are described in
|
|
19
|
+
full in the rest of this document. Absence of `sandbox` (or `sandbox: false`)
|
|
20
|
+
means trusted; there is no separate `unsafe`/`trusted` field to opt back into
|
|
21
|
+
the old sandboxed-by-default behavior — set `sandbox: true` per route instead.
|
|
22
|
+
|
|
23
|
+
**Either way, binding grants are unaffected.** Trusting a route's code by
|
|
24
|
+
default does not grant it any `env`/`secrets` it was not explicitly declared
|
|
25
|
+
in YAML and approved by an operator policy pinned to the project revision
|
|
26
|
+
(see "Granting selected bindings" below). A trusted function only *can* do
|
|
27
|
+
more with Node once it runs — it does not receive anything more than a
|
|
28
|
+
sandboxed one would.
|
|
29
|
+
|
|
30
|
+
This is a claim about `context`/`context.secrets` injection, not an
|
|
31
|
+
access-control guarantee on trusted code. The binding grant governs only what
|
|
32
|
+
URLCode hands a route through `context`; it does not restrict what trusted
|
|
33
|
+
(non-`sandbox`) code can independently do, because that code has full Node
|
|
34
|
+
access by design. A trusted function can read `process.env`, open files or
|
|
35
|
+
make network calls on its own regardless of what its route was or was not
|
|
36
|
+
granted — withholding a binding grant limits what URLCode gives the code
|
|
37
|
+
through `context`, not what the code itself, running with full Node access,
|
|
38
|
+
can go and get. A sandboxed route has no such independent access: the guest
|
|
39
|
+
API is all it has, so its binding grant *is* effectively its whole reach into
|
|
40
|
+
the environment. Trusted code's reach is not bounded that way; treat the
|
|
41
|
+
grant as scoping `context`, not as scoping the process.
|
|
42
|
+
|
|
43
|
+
## What "sandboxed" (`sandbox: true`) still guarantees
|
|
9
44
|
|
|
10
45
|
- Function sources are parsed/snapshotted without importing them into Node.
|
|
11
46
|
- Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
|
|
@@ -22,9 +57,64 @@ is no `unsafe`, `trusted` or automatic host-execution fallback.
|
|
|
22
57
|
Operator grants are exact-name, route-scoped and pinned to configuration/source.
|
|
23
58
|
|
|
24
59
|
The guest API is intentionally narrower than Node or full Fetch; see the
|
|
25
|
-
[implemented contract](SPECIFICATION.md).
|
|
26
|
-
or binary/stream APIs must be rewritten
|
|
27
|
-
|
|
60
|
+
[implemented contract](SPECIFICATION.md). A function moving from trusted to
|
|
61
|
+
`sandbox: true` that uses Node/network or binary/stream APIs must be rewritten
|
|
62
|
+
for the supported guest profile, or stay trusted. Redirects need none of this
|
|
63
|
+
machinery either way.
|
|
64
|
+
|
|
65
|
+
This engine — worker spawning, the module-allowlist walk, the two-layer
|
|
66
|
+
deadline, `maxBytes` and response-shape validation — is one implementation
|
|
67
|
+
shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
|
|
68
|
+
the public primitive an extension package uses to run a project-supplied hook
|
|
69
|
+
through this exact isolation when the project's own config declares
|
|
70
|
+
`sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
|
|
71
|
+
and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
|
|
72
|
+
through that entry point; there is no separate, weaker sandbox for extensions
|
|
73
|
+
to reach for, and no "trusted" mode exported there — trusted execution needs
|
|
74
|
+
no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
|
|
75
|
+
|
|
76
|
+
## What the trusted default can and can't do
|
|
77
|
+
|
|
78
|
+
A trusted route (no `sandbox`, or `sandbox: false`) has none of the guest
|
|
79
|
+
restrictions above:
|
|
80
|
+
|
|
81
|
+
- Full Node built-ins, `process`, the filesystem, `fetch`, sockets, workers
|
|
82
|
+
and npm packages are available, exactly as in any other Node module.
|
|
83
|
+
- Module resolution is ordinary Node ESM resolution: bare specifiers, dynamic
|
|
84
|
+
`import()` and node_modules all work. There is no dependency-graph allowlist
|
|
85
|
+
and no per-module/total source-size budget (function-sources.ts's
|
|
86
|
+
`MODULE_LIMIT`/`MODULE_BYTE_LIMIT`/`TOTAL_BYTE_LIMIT` apply only to what a
|
|
87
|
+
sandboxed snapshot bundles).
|
|
88
|
+
- Node's own module cache is shared across invocations and across the whole
|
|
89
|
+
process; there is no fresh heap per call. Module-level state persists
|
|
90
|
+
between requests exactly like an ordinary long-running Node server, so a
|
|
91
|
+
trusted function that mutates shared/global state affects later requests
|
|
92
|
+
the way hand-written server code would.
|
|
93
|
+
- There is no worker-thread deadline that force-terminates a stuck call. A
|
|
94
|
+
trusted invocation races a configurable timeout, but that race can only
|
|
95
|
+
reject the *call*; it cannot preempt code that blocks the event loop
|
|
96
|
+
synchronously. See [capacity](CAPACITY.md) for what this means for one slow
|
|
97
|
+
or hung trusted route's effect on the rest of the process.
|
|
98
|
+
- A snapshot reload re-imports a trusted route's own entry file fresh (each
|
|
99
|
+
reload gets its own cache-busted module registration), so editing the
|
|
100
|
+
`source` file a route declares and reloading picks up the change, the same
|
|
101
|
+
as the sandboxed pool rebuilding from scratch. A file that entry file
|
|
102
|
+
merely *imports* is not similarly busted: Node's own module cache is
|
|
103
|
+
keyed by the resolved URL of that import statement, which this runtime
|
|
104
|
+
does not rewrite, so an edited dependency two files deep from the route
|
|
105
|
+
keeps serving its old content until the process restarts. Restructure a
|
|
106
|
+
route so the code you expect to hot-reload is the declared entry file
|
|
107
|
+
itself, or restart rather than reload after editing a trusted route's
|
|
108
|
+
dependencies. A `sandbox: true` route has no such gap: reload always
|
|
109
|
+
rebuilds its whole snapshot, dependencies included.
|
|
110
|
+
|
|
111
|
+
What does **not** change with trust: `args` are still exactly the validated
|
|
112
|
+
values the route declares (never raw request input), and `env`/`secrets` are
|
|
113
|
+
still exactly what the route's YAML requests and an operator policy grants,
|
|
114
|
+
pinned to the project revision — trust changes where code runs, not what
|
|
115
|
+
it is handed *through `context`*. It does not change what the code can go get
|
|
116
|
+
on its own once it is running; see "binding grants are unaffected" above for
|
|
117
|
+
that distinction.
|
|
28
118
|
|
|
29
119
|
## Granting selected bindings
|
|
30
120
|
|
|
@@ -63,26 +153,28 @@ urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
|
|
|
63
153
|
|
|
64
154
|
`dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
|
|
65
155
|
The JavaScript API accepts an equivalent operator-supplied `permissions` object.
|
|
66
|
-
Every config
|
|
67
|
-
|
|
68
|
-
|
|
156
|
+
Every config change invalidates the grant, and so does a module change within
|
|
157
|
+
what the approval digest actually hashes: for a sandboxed route, its
|
|
158
|
+
middleware/function sources and their full dependency graph; for a trusted
|
|
159
|
+
route, only its own entry-file source (see the next paragraph — a trusted
|
|
160
|
+
route's transitive dependencies are explicitly **not** part of that digest).
|
|
161
|
+
Inspect/review the new revision before updating the operator file. Policies
|
|
162
|
+
are read at startup, not hot-reloaded. A failed development candidate leaves
|
|
163
|
+
the previous approved snapshot running.
|
|
69
164
|
|
|
70
165
|
Granting a secret deliberately makes it available to every middleware and function
|
|
71
|
-
in that route
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
filesystem handle, database path or management token. Native link data changes do
|
|
84
|
-
not authorize new code or bindings. Management requires a separate operator CLI
|
|
85
|
-
or token-protected listener. See [dynamic links](DYNAMIC-LINKS.md).
|
|
166
|
+
in that route, trusted or sandboxed alike. A sandboxed route's middleware
|
|
167
|
+
sources and their full dependency graph are included in the approval digest,
|
|
168
|
+
as before; a trusted route's own entry-file source is included too, so
|
|
169
|
+
changing that file's content invalidates the grant, but a change to a helper
|
|
170
|
+
module it merely imports does not by itself (see function-sources.ts's
|
|
171
|
+
`collectTrustedSources`) — a known, documented gap versus the sandboxed path's
|
|
172
|
+
full dependency-graph hashing: a trusted route's grant scope is entry-file-only,
|
|
173
|
+
not transitive. Either way, code can include any granted data
|
|
174
|
+
in its HTTP response: neither the sandbox nor the trusted default promises
|
|
175
|
+
secrecy from code that was explicitly authorized to read a value. Minimize
|
|
176
|
+
grants, use scoped/short-lived credentials and revoke/restart when needed.
|
|
177
|
+
Other routes get none of that context.
|
|
86
178
|
|
|
87
179
|
## Next capability work
|
|
88
180
|
|
|
@@ -90,9 +182,14 @@ Outbound requests need a host-owned broker with explicit destination/method
|
|
|
90
182
|
allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
|
|
91
183
|
redirect revalidation, deadlines and byte/concurrency limits. Application YAML
|
|
92
184
|
must not grant those permissions. Persistent state needs similarly scoped access.
|
|
93
|
-
Until such brokers are implemented and tested, these capabilities are unavailable
|
|
94
|
-
Provider adapters must preserve
|
|
95
|
-
|
|
185
|
+
Until such brokers are implemented and tested, these capabilities are unavailable
|
|
186
|
+
to a *sandboxed* route. Provider adapters must preserve a `sandbox: true`
|
|
187
|
+
route's isolation or reject deployment; they cannot silently downgrade a
|
|
188
|
+
route that explicitly asked for the sandbox into unrestricted Node execution.
|
|
189
|
+
(A trusted route, by contrast, already has unrestricted Node execution by
|
|
190
|
+
design on the self-hosted target — see "What the trusted default can and
|
|
191
|
+
can't do" above; non-Node targets refuse `function`/`middleware` entirely,
|
|
192
|
+
trusted or sandboxed, since neither execution mode exists there.)
|
|
96
193
|
|
|
97
194
|
## Verification and remaining risk
|
|
98
195
|
|
package/docs/INSTALL.md
CHANGED
|
@@ -6,34 +6,54 @@ pick whichever fits how you already manage tools. All of them require
|
|
|
6
6
|
TypeScript source (`dist/`, with declarations; see [TypeScript](TYPESCRIPT.md)),
|
|
7
7
|
so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
|
|
8
8
|
|
|
9
|
-
Live short-link storage additionally needs a Node build carrying the patched
|
|
10
|
-
SQLite WAL fix. Run `urlcode doctor` after installing and check `liveLinks`;
|
|
11
|
-
everything except live links works on any supported build. See
|
|
12
|
-
[dynamic links](DYNAMIC-LINKS.md#node-build-requirement).
|
|
13
|
-
|
|
14
9
|
## npm
|
|
15
10
|
|
|
16
11
|
```sh
|
|
17
|
-
npm install --global urlcode
|
|
12
|
+
npm install --global @jimhoyd/urlcode
|
|
18
13
|
urlcode --help
|
|
19
14
|
```
|
|
20
15
|
|
|
21
|
-
Project-local, which is what an application repository should normally pin
|
|
16
|
+
Project-local, which is what an application repository should normally pin.
|
|
17
|
+
Which dependency list it belongs in depends on how the project uses URLCode:
|
|
22
18
|
|
|
23
19
|
```sh
|
|
24
|
-
|
|
20
|
+
# Using URLCode as a tool: validate, test and build in CI, never imported by
|
|
21
|
+
# the code that serves requests.
|
|
22
|
+
npm install --save-dev @jimhoyd/urlcode
|
|
25
23
|
npx urlcode validate
|
|
24
|
+
|
|
25
|
+
# Embedding the runtime (see TYPESCRIPT.md): the application imports
|
|
26
|
+
# @jimhoyd/urlcode at startup, so it must survive `npm ci --omit=dev`.
|
|
27
|
+
npm install --save @jimhoyd/urlcode
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
A devDependency is absent from a production install, so an application that
|
|
31
|
+
imports `createRuntime`, `startServer`, `prerenderPages` or any other
|
|
32
|
+
[embedding entry point](TYPESCRIPT.md) fails at startup on a missing module if it
|
|
33
|
+
is installed with `--save-dev`. An application should also pin an **exact**
|
|
34
|
+
version rather than a range: the compiled Cloudflare artifact format is tied to
|
|
35
|
+
the runtime version that reads it.
|
|
36
|
+
|
|
28
37
|
## Homebrew
|
|
29
38
|
|
|
30
39
|
```sh
|
|
31
40
|
brew tap jimhoyd-com/urlcode
|
|
41
|
+
brew trust jimhoyd-com/urlcode
|
|
32
42
|
brew install urlcode
|
|
33
43
|
```
|
|
34
44
|
|
|
45
|
+
Homebrew refuses to load a formula from a third-party tap until you trust it,
|
|
46
|
+
so without the middle line the install stops with `Refusing to load formula …
|
|
47
|
+
from untrusted tap`. Trusting a tap means agreeing to run code from this
|
|
48
|
+
repository, the same as with the install script; `brew trust --formula
|
|
49
|
+
jimhoyd-com/urlcode/urlcode` limits it to this one formula.
|
|
50
|
+
|
|
35
51
|
The tap's formula is generated from the published tarball for each release and
|
|
36
|
-
attached to the GitHub release as `urlcode.rb`.
|
|
52
|
+
attached to the GitHub release as `urlcode.rb`. Homebrew verifies the tarball's
|
|
53
|
+
SHA-256 against the formula before installing.
|
|
54
|
+
|
|
55
|
+
Verified on macOS (arm64) against the 0.3.0 tap: fetch verified, installed into
|
|
56
|
+
`/opt/homebrew/Cellar/urlcode/0.3.0`.
|
|
37
57
|
|
|
38
58
|
## Install script
|
|
39
59
|
|
|
@@ -45,7 +65,7 @@ It downloads the release tarball, verifies its SHA-256 against the release's
|
|
|
45
65
|
`SHA256SUMS`, and installs with npm. Options:
|
|
46
66
|
|
|
47
67
|
```sh
|
|
48
|
-
curl -fsSL .../install.sh | sh -s -- --version 0.
|
|
68
|
+
curl -fsSL .../install.sh | sh -s -- --version 0.3.0 --prefix "$HOME/.local"
|
|
49
69
|
```
|
|
50
70
|
|
|
51
71
|
`--prefix` avoids needing privileges for a global npm directory; add
|
|
@@ -54,8 +74,14 @@ for that moment: to inspect first, download it, read it, then run it.
|
|
|
54
74
|
|
|
55
75
|
## Container
|
|
56
76
|
|
|
77
|
+
No image is published yet: the release job's GHCR step is gated behind the
|
|
78
|
+
`PUBLISH_CONTAINER` repository variable and has not run, so there is nothing at
|
|
79
|
+
`ghcr.io/jimhoyd-com/urlcode` to pull. Build it from a release checkout:
|
|
80
|
+
|
|
57
81
|
```sh
|
|
58
|
-
|
|
82
|
+
git clone --branch v0.3.0 https://github.com/jimhoyd-com/urlcode.git
|
|
83
|
+
docker build -t urlcode:0.3.0 urlcode
|
|
84
|
+
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" urlcode:0.3.0 \
|
|
59
85
|
serve --project /project --host 0.0.0.0
|
|
60
86
|
```
|
|
61
87
|
|
|
@@ -74,13 +100,20 @@ make dev
|
|
|
74
100
|
A clone runs the TypeScript source directly (`node src/cli.ts`, Node 22.18+),
|
|
75
101
|
with no build step; see [local development](LOCAL-DEVELOPMENT.md).
|
|
76
102
|
|
|
103
|
+
Three Node versions appear around the project, and they are not a contradiction:
|
|
104
|
+
the installed package runs on Node 22.13 or newer (`engines`), running the
|
|
105
|
+
TypeScript source from a clone needs 22.18 or newer because it relies on Node's
|
|
106
|
+
built-in type stripping, the release workflow's npm trusted publishing needs
|
|
107
|
+
22.14 or newer, and the container image pins Node 26. Only the first number
|
|
108
|
+
constrains a deployment of the published tarball.
|
|
109
|
+
|
|
77
110
|
## Verify what you installed
|
|
78
111
|
|
|
79
112
|
Releases carry Sigstore provenance signed by the release workflow. Before
|
|
80
113
|
trusting a downloaded artifact:
|
|
81
114
|
|
|
82
115
|
```sh
|
|
83
|
-
gh attestation verify urlcode-0.
|
|
116
|
+
gh attestation verify jimhoyd-urlcode-0.3.0.tgz --repo jimhoyd-com/urlcode \
|
|
84
117
|
--signer-workflow jimhoyd-com/urlcode/.github/workflows/release.yml
|
|
85
118
|
```
|
|
86
119
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Redirect interchange and bulk authoring
|
|
2
|
+
|
|
3
|
+
`importRoutes` and `exportRoutes` return a conversion report without writing files,
|
|
4
|
+
activating a runtime, reading bindings, loading function sources or running code.
|
|
5
|
+
The CLI can preview the report before creating a new output file. Failed reports
|
|
6
|
+
never contain a partial document or output; duplicate paths never overwrite a row.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { importRoutes, exportRoutes } from '@jimhoyd/urlcode';
|
|
10
|
+
const report = await importRoutes({
|
|
11
|
+
format: 'csv', source: 'migration.csv',
|
|
12
|
+
text: 'path,url,status\n/old,https://example.test/new,301\n'
|
|
13
|
+
});
|
|
14
|
+
if (!report.ok) throw new Error(JSON.stringify(report.diagnostics));
|
|
15
|
+
// report.output is URLCode project YAML; report.document is the validated project.
|
|
16
|
+
const csv = await exportRoutes({ format: 'csv', document: report.document });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each report has `ok`, `lossless`, `routeCount` and `diagnostics`. Diagnostics name
|
|
20
|
+
an input source, physical CSV/text row or JSON/YAML array index where available,
|
|
21
|
+
a path where appropriate, a code, severity and explanation. Destinations and
|
|
22
|
+
credentials are not echoed in validation errors. Output is sorted by literal
|
|
23
|
+
path, independent of input order. Import does not merge an existing project;
|
|
24
|
+
normal project loading rejects collisions across includes when output is added.
|
|
25
|
+
|
|
26
|
+
## CLI usage
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
# Bulk row formats can be inferred from the input extension or named explicitly.
|
|
30
|
+
urlcode import csv migration.csv --out routes.yaml --dry-run
|
|
31
|
+
urlcode import csv migration.csv --out routes.yaml
|
|
32
|
+
urlcode import migration.json --report json --dry-run
|
|
33
|
+
|
|
34
|
+
# Provider migrations require explicit acknowledgment of the reported differences.
|
|
35
|
+
urlcode import netlify _redirects --out imported.yaml --dry-run
|
|
36
|
+
urlcode import netlify _redirects --out imported.yaml --accept-provider-differences
|
|
37
|
+
urlcode export --target netlify --project ./project --out _redirects --accept-provider-differences
|
|
38
|
+
urlcode export --target cloudflare --project ./project --dry-run
|
|
39
|
+
urlcode export --target vercel --project ./project --dry-run
|
|
40
|
+
urlcode export --target netlify-toml --project ./project --dry-run
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
General import syntax is `urlcode import [format] FILE`; `--format` explicitly
|
|
44
|
+
overrides the positional format or extension. Exports use `--target` for a
|
|
45
|
+
provider format, or `--format csv|json|yaml` for row data. `--out FILE` creates a
|
|
46
|
+
new file with mode 0600 and never overwrites one. `--dry-run` validates and
|
|
47
|
+
returns a report without creating the requested output. `--report json` always
|
|
48
|
+
emits the report. Failures exit nonzero and leave no converted output.
|
|
49
|
+
|
|
50
|
+
Reports include source/row diagnostics and the successful candidate output.
|
|
51
|
+
Acknowledged provider conversions always emit a report with their warnings,
|
|
52
|
+
even when `--out` is provided; raw provider output is never silently printed as
|
|
53
|
+
if it were lossless. Project export flattens validated includes, but refuses
|
|
54
|
+
other unsupported project behavior instead of dropping it. For a large input
|
|
55
|
+
that needs a complete sharded project, use [bulk import](BULK.md).
|
|
56
|
+
|
|
57
|
+
## Supported forms
|
|
58
|
+
|
|
59
|
+
| Format | Input/output subset |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `csv` | Header exactly `path,url,status`; optional empty status defaults to 302; quoted fields and escaped quotes |
|
|
62
|
+
| `json` | Array of `{path,url,status?}`; status must be a number |
|
|
63
|
+
| `yaml` | The same row array in strict URLCode YAML syntax; not a full project file |
|
|
64
|
+
| `netlify` | `_redirects`: literal source, absolute HTTP(S) destination, optional status (default 301) |
|
|
65
|
+
| `cloudflare` | Pages `_redirects`: same columns, default 302; 2,000 static rules, 1,000 characters per rule |
|
|
66
|
+
| `vercel` | `vercel.json` containing only `redirects`; literal `source`, absolute `destination`, and either boolean `permanent` (308/307) or `statusCode` |
|
|
67
|
+
| `netlify-toml` | Only `[[redirects]]` blocks with `from`, `to`, optional numeric `status` (default 301); unescaped double-quoted strings |
|
|
68
|
+
|
|
69
|
+
All statuses are restricted to 301, 302, 303, 307 and 308. Literal ASCII paths
|
|
70
|
+
and absolute HTTP(S) destinations are required. The existing schema and semantic
|
|
71
|
+
compiler validate resulting routes. No provider pattern compiler is introduced.
|
|
72
|
+
|
|
73
|
+
The TOML subset deliberately rejects general TOML constructs, build settings,
|
|
74
|
+
escapes, inline comments, nested conditions, force flags and other tables. A full
|
|
75
|
+
`netlify.toml` must be separated into a redirects-only input by the operator. This
|
|
76
|
+
avoids silently discarding build settings or reinterpreting unsupported syntax.
|
|
77
|
+
|
|
78
|
+
Functions, assets, middleware, conditions, parameters, query maps/allowlists,
|
|
79
|
+
headers, policies, includes and any other extra route/project fields cannot be
|
|
80
|
+
exported by this subset. Relative destinations, wildcards, provider placeholder
|
|
81
|
+
syntax, rewrites and duplicate paths fail. Use a runtime adapter for richer
|
|
82
|
+
behavior. CSV/JSON/YAML round-trips are lossless within the declared simple subset.
|
|
83
|
+
|
|
84
|
+
## Provider semantics require explicit acknowledgment
|
|
85
|
+
|
|
86
|
+
Provider conversions fail by default. `acceptProviderDifferences: true` (CLI
|
|
87
|
+
`--accept-provider-differences`) permits a **non-lossless migration candidate**;
|
|
88
|
+
it does not suppress invalid rules, discard unsupported fields or claim exact
|
|
89
|
+
behavior. Every such successful report retains a warning and `lossless: false`.
|
|
90
|
+
|
|
91
|
+
URLCode drops incoming query parameters and defaults to GET/HEAD. Native
|
|
92
|
+
redirect systems can apply to other methods and have different normalization
|
|
93
|
+
and request-query behavior. Netlify automatically forwards queries for common
|
|
94
|
+
redirect statuses and can give existing files precedence. Cloudflare Pages
|
|
95
|
+
redirects override assets, but Pages Functions can bypass `_redirects`.
|
|
96
|
+
Review behavior for requests with queries, non-GET methods, slash/URL normalization
|
|
97
|
+
and conflicting assets/functions before deploying. The acknowledged subset is
|
|
98
|
+
literal GET/HEAD requests without these conflicts. No provider deployment was
|
|
99
|
+
performed as part of interchange unit tests.
|
|
100
|
+
|
|
101
|
+
Provider references checked 2026-09-17:
|
|
102
|
+
|
|
103
|
+
- [Netlify redirect options](https://docs.netlify.com/manage/routing/redirects/redirect-options/)
|
|
104
|
+
- [Netlify TOML configuration](https://docs.netlify.com/build/configure-builds/file-based-configuration/)
|
|
105
|
+
- [Cloudflare Pages redirects](https://developers.cloudflare.com/pages/configuration/redirects/)
|
|
106
|
+
- [Vercel redirect configuration](https://vercel.com/docs/project-configuration/vercel-json)
|
|
107
|
+
|
|
108
|
+
## Resource limits
|
|
109
|
+
|
|
110
|
+
Imports reject more than 32 MiB of input or 100,000 rows. Diagnostic collection
|
|
111
|
+
stops after 100 invalid rows; normal route compilation retains its deadline and
|
|
112
|
+
validation rules. Large output can still exceed the runtime YAML worker's heap
|
|
113
|
+
or deadline: conversion success does not promise activation of a single 100k
|
|
114
|
+
route YAML file. Split large migration inputs into independently reviewed files
|
|
115
|
+
and use explicit `includes`; aggregate project limits continue to apply.
|
|
116
|
+
|
|
117
|
+
Reports also expose bounded classification counts:
|
|
118
|
+
|
|
119
|
+
- `convertedRoutes`: routes actually returned in successful output; zero when
|
|
120
|
+
any error prevents output.
|
|
121
|
+
- `nativeEquivalentRoutes`: returned routes with no conversion warnings; always
|
|
122
|
+
zero for acknowledged provider conversions.
|
|
123
|
+
- `runtimeRequiredRoutes`: route diagnostics identifying behavior outside the
|
|
124
|
+
simple export subset.
|
|
125
|
+
- `unsupportedRows`: known invalid/duplicate input-row diagnostics.
|
|
126
|
+
- `providerDifferenceRoutes`: candidate routes subject to the provider semantic
|
|
127
|
+
warning; these overlap converted routes after acknowledgment.
|
|
128
|
+
- `fullyScanned`: false on parser/global failures or truncated diagnostics. Counts
|
|
129
|
+
then describe only examined input, never an inferred total.
|
|
130
|
+
|
|
131
|
+
These counts are not a partition of arbitrary malformed input. `routeCount`
|
|
132
|
+
retains its original meaning: routes in the candidate table, or rows
|
|
133
|
+
parsed before an early global failure. A failed report contains no candidate
|
|
134
|
+
output even if some rows were convertible.
|
package/docs/LOAD-TESTING.md
CHANGED
|
@@ -75,8 +75,8 @@ largest one that makes a number go green.
|
|
|
75
75
|
|
|
76
76
|
## What a passing run does not prove
|
|
77
77
|
|
|
78
|
-
- **GET and HEAD only.** Function routes with request bodies
|
|
79
|
-
|
|
78
|
+
- **GET and HEAD only.** Function routes with request bodies are not exercised.
|
|
79
|
+
`workload` states this in every report.
|
|
80
80
|
- **Redirects are not followed**, so a redirect's destination is never loaded.
|
|
81
81
|
- **One client, one host, no slow peers.** Tail latency under adversarial
|
|
82
82
|
clients, connection churn or packet loss is not measured.
|
|
@@ -87,5 +87,5 @@ largest one that makes a number go green.
|
|
|
87
87
|
runtime and application revisions, host, and command with any figure you keep.
|
|
88
88
|
|
|
89
89
|
`scripts/operational-drills.ts` covers the adjacent ground — mixed
|
|
90
|
-
native/function
|
|
91
|
-
|
|
90
|
+
native/function load, an invalid reload and rollback — as a local proof,
|
|
91
|
+
never a statement about production.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Middleware examples
|
|
2
|
+
|
|
3
|
+
Fourteen reusable middleware modules covering fifteen patterns (`auth.mjs`
|
|
4
|
+
exports both `bearer` and `basic`), each in
|
|
5
|
+
[`examples/cookbook/middleware`](../examples/cookbook/middleware) with a route in
|
|
6
|
+
[`routes/middleware.yaml`](../examples/cookbook/routes/middleware.yaml) and request
|
|
7
|
+
fixtures in the cookbook tests. The same modules ship as the `middleware`
|
|
8
|
+
[local recipe](RECIPES.md):
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
urlcode recipes add middleware --out ./my-middleware
|
|
12
|
+
urlcode test --project ./my-middleware
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
They cover what Express, Koa, Fastify, Hono, Next.js and edge runtimes usually
|
|
16
|
+
ship as middleware, rewritten for URLCode's [guest API](MIDDLEWARE.md): text/JSON
|
|
17
|
+
`Request`/`Response`, `Headers`, `context.inputs/args/env/secrets/state`, timers
|
|
18
|
+
and nothing else. There is no `crypto`, `URL`, `fetch`, storage or console, and
|
|
19
|
+
`context.state` dies with the request. Patterns that need any of those are listed
|
|
20
|
+
at the end so nobody wastes time porting them.
|
|
21
|
+
|
|
22
|
+
| Pattern | Module | Framework equivalent | Demonstrates |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| Bearer token gate | `auth.mjs` `bearer` | Hono `bearerAuth`, `express-bearer-token` | Early 401, `www-authenticate`, constant-time compare against an `env` binding |
|
|
25
|
+
| Basic authentication | `auth.mjs` `basic` | Hono `basicAuth`, `express-basic-auth` | Hand-written base64 (no `atob`), both checks always evaluated, `state.user` |
|
|
26
|
+
| CORS | `cors.mjs` | Express `cors`, Hono `cors` | `OPTIONS` answered before the handler, origin allowlist, `vary: origin` |
|
|
27
|
+
| Correlation id and timing | `request-id.mjs` | Hono `requestId`/`timing`, Express `response-time` | Validating a caller header, `server-timing`, coexisting with the runtime's own `x-request-id` |
|
|
28
|
+
| Maintenance switch | `maintenance.mjs` | Next.js and Netlify Edge maintenance examples | 503 with `retry-after`, bypass header, flipping behavior from a binding |
|
|
29
|
+
| Error boundary | `errors.mjs` | Koa `onerror`, Express error handlers | Catching a downstream throw, JSON 500 instead of a bare 502 |
|
|
30
|
+
| JSON envelope | `envelope.mjs` | Response transformers | Reading a function body once, passing native bodies through untouched |
|
|
31
|
+
| Content negotiation | `negotiate.mjs` | Express `res.format` | Parsing `accept` with q-values, 406, `vary: accept` |
|
|
32
|
+
| Method override | `methods.mjs` `override` | Express `method-override` | Bounded tunneling through POST, 405 with `allow` |
|
|
33
|
+
| ETag and 304 | `etag.mjs` | Express `etag`, Fastify `@fastify/etag` | FNV-1a weak tag, `if-none-match`, null-body 304 |
|
|
34
|
+
| A/B bucket | `bucket.mjs` | Vercel and Cloudflare A/B examples | Cookie parsing, `set-cookie`, replacing a native redirect |
|
|
35
|
+
| Locale redirect | `locale.mjs` | Next.js i18n middleware | `accept-language` ranking, allowlisted languages, `vary` |
|
|
36
|
+
| Referer allowlist | `referer.mjs` | Hotlink protection rules | Gating a native download without reading it |
|
|
37
|
+
| Body validation | `body.mjs` | `express-validator`, Fastify schemas | Single-use body, 422 error list, handoff through `state` |
|
|
38
|
+
| Debug echo | `debug.mjs` | Request loggers | Inspecting inputs, args and redacted headers when the console is silent |
|
|
39
|
+
|
|
40
|
+
## Reading the modules
|
|
41
|
+
|
|
42
|
+
Every module follows the shape in [middleware](MIDDLEWARE.md): read the request,
|
|
43
|
+
optionally return early, otherwise `await next()` once and return a `Response`.
|
|
44
|
+
Three habits recur and are worth copying:
|
|
45
|
+
|
|
46
|
+
- **Configuration lives in bindings.** Tokens, allowlists and switches are read
|
|
47
|
+
from `context.env`. The cookbook binds literal values so it runs without grants;
|
|
48
|
+
a deployed project uses `{secret: name}` for credentials and an operator grant.
|
|
49
|
+
- **Native bodies stay opaque.** `envelope`, `negotiate` and `etag` only rewrite a
|
|
50
|
+
body when the downstream response is a function response with a readable
|
|
51
|
+
content type. `bucket`, `locale` and `referer` wrap native redirects and
|
|
52
|
+
downloads without touching their bytes; to change the destination they return
|
|
53
|
+
a new `Response` instead.
|
|
54
|
+
- **Chains compose through `state`.** `/fragile` runs `request-id` before
|
|
55
|
+
`errors`, so the fallback JSON carries the correlation id. `/profile` parses
|
|
56
|
+
the body once in middleware and the function reads `context.state.body`.
|
|
57
|
+
|
|
58
|
+
## Limits these examples respect
|
|
59
|
+
|
|
60
|
+
- **No `crypto`.** The auth modules compare a shared token; they cannot verify
|
|
61
|
+
HMAC signatures, JWTs or password hashes. Signed URLs would be a runtime
|
|
62
|
+
feature, not a middleware example.
|
|
63
|
+
- **No cross-request state.** Rate limiting, caching, sessions and CSRF tokens
|
|
64
|
+
need storage the guest does not have. Throttling and cache headers exist as
|
|
65
|
+
native [policies](POLICIES.md) instead.
|
|
66
|
+
- **No logging target.** The guest console is a no-op; `debug.mjs` returns the
|
|
67
|
+
information to the caller instead, and only when both a binding and a header
|
|
68
|
+
ask for it. Remove that route before publishing a project.
|
|
69
|
+
- **Runtime headers win.** The runtime stamps `x-request-id` on every response,
|
|
70
|
+
which is why the tracing example uses `x-correlation-id`. YAML
|
|
71
|
+
`response.headers` also override middleware headers.
|
|
72
|
+
|
|
73
|
+
Each cookbook route has fixtures for its success path, its early responses, its
|
|
74
|
+
validation failures and every declared method, which is what `urlcode audit`
|
|
75
|
+
expects before it reports a middleware-wrapped route as covered.
|
package/docs/MIDDLEWARE.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Middleware is reusable JavaScript around any route handler. It is optional and
|
|
4
4
|
route-local; plain redirects and assets retain their native fast path when no
|
|
5
|
-
middleware is attached.
|
|
5
|
+
middleware is attached. Middleware runs trusted and unsandboxed by default,
|
|
6
|
+
in-process with full Node access, the same as a `function` route; add
|
|
7
|
+
`sandbox: true` on the route to run the whole chain isolated instead (see
|
|
8
|
+
[trust model and sandbox opt-in](FUNCTION-SECURITY.md)).
|
|
6
9
|
|
|
7
10
|
```yaml
|
|
8
11
|
version: "1"
|
|
@@ -28,6 +31,8 @@ Paths resolve from the project root. Up to 16 entries are allowed; each accepts
|
|
|
28
31
|
`source` and optional `export` (default `default`). Reuse a module across routes.
|
|
29
32
|
There are no global middleware settings or middleware-specific argument fields.
|
|
30
33
|
This is URLCode's small portable API, not Express/Node middleware compatibility.
|
|
34
|
+
Fourteen ready-to-copy patterns (auth, CORS, tracing, error boundary, ETag and
|
|
35
|
+
more) are in [middleware examples](MIDDLEWARE-EXAMPLES.md).
|
|
31
36
|
|
|
32
37
|
## Order and responses
|
|
33
38
|
|
|
@@ -47,37 +52,45 @@ reading one consumes it for downstream code. There is no `clone()` or streaming
|
|
|
47
52
|
API; pass parsed data through `context.state` when needed.
|
|
48
53
|
|
|
49
54
|
Function responses support the existing text/JSON guest API. To transform their
|
|
50
|
-
body, read it and return a new `Response`. Native redirect/respond/
|
|
55
|
+
body, read it and return a new `Response`. Native redirect/respond/page/static/
|
|
51
56
|
download bodies are opaque and cannot be read through `text()` or `json()`.
|
|
52
57
|
Returning the same native response preserves original bytes, including binary
|
|
53
58
|
files, ranges and HEAD lengths. You may add headers, but cannot change its
|
|
54
59
|
original status or existing native headers while preserving that body. To replace
|
|
55
60
|
status, destination or content, return a new `Response` instead. Replacement
|
|
56
|
-
responses follow the normal
|
|
61
|
+
responses follow the normal response size limits, and (on a `sandbox: true`
|
|
62
|
+
route) the guest's text/JSON constraints. To wrap a shared
|
|
57
63
|
template around file content, render it through a function at build time and
|
|
58
64
|
publish the result: see [prerendering](PRERENDER.md).
|
|
59
65
|
|
|
60
66
|
Route selection, enabled/expiry checks, methods and input/body validation run
|
|
61
67
|
before middleware. Their errors do not pass through the chain. A missing file
|
|
62
|
-
inside a selected static mount is a downstream 404 response.
|
|
63
|
-
errors (missing, disabled, expired or unavailable store) precede middleware. YAML
|
|
68
|
+
inside a selected static mount is a downstream 404 response. YAML
|
|
64
69
|
`response.headers` apply last and override matching middleware headers. Runtime
|
|
65
70
|
framing and asset metadata protections still apply.
|
|
66
71
|
|
|
67
|
-
##
|
|
72
|
+
## Trust, isolation and testing
|
|
68
73
|
|
|
69
|
-
The whole chain and handler run
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
The whole chain and handler run as one unit, in one execution mode, chosen by
|
|
75
|
+
the route's `sandbox` field — not a per-middleware-entry choice. By default
|
|
76
|
+
(`sandbox` false/absent) that means trusted, in-process execution with full
|
|
77
|
+
Node access and no fixed worker-pool ceiling. With `sandbox: true` it means
|
|
78
|
+
one fresh QuickJS/WASM guest with one memory budget and one deadline: no
|
|
79
|
+
Node, filesystem, shell, fetch or ambient environment, and modules limited to
|
|
80
|
+
this route's declared dependency graph. Either way, all middleware on a route
|
|
81
|
+
receive that route's approved bindings, so review the whole chain; source
|
|
82
|
+
changes invalidate grants. See [trust model and sandbox opt-in](FUNCTION-SECURITY.md).
|
|
74
83
|
|
|
75
|
-
Invalid responses and repeated `next()` calls fail with 502,
|
|
76
|
-
returns
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
Invalid responses and repeated `next()` calls fail with 502, and the deadline
|
|
85
|
+
returns 504 either way. A `sandbox: true` chain also sheds load with 503 when
|
|
86
|
+
the shared worker pool is exhausted, and cannot extend the deadline or catch
|
|
87
|
+
the outer worker termination; forgotten downstream work is still drained
|
|
88
|
+
within it. A trusted chain has no worker pool to exhaust (see
|
|
89
|
+
[capacity](CAPACITY.md)), but its deadline is a race against the call's own
|
|
90
|
+
promise rather than a forced kill — it cannot preempt code that blocks the
|
|
91
|
+
event loop synchronously.
|
|
79
92
|
|
|
80
93
|
Include explicit request fixtures for middleware-wrapped routes: test success,
|
|
81
94
|
early responses, validation failures and every configured method. Audit will
|
|
82
95
|
report missing coverage instead of assuming native handler behavior. Benchmark
|
|
83
|
-
with middleware enabled to measure its actual
|
|
96
|
+
with middleware enabled to measure its actual overhead, sandboxed or trusted.
|