@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
package/docs/BULK.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Bulk redirect projects
|
|
2
|
+
|
|
3
|
+
Bulk import converts CSV, JSON or YAML redirect rows into an ordinary Git-owned
|
|
4
|
+
URLCode project. It delegates semantics and row diagnostics to the strict
|
|
5
|
+
[redirect interchange](INTERCHANGE.md) converter. Only literal paths and
|
|
6
|
+
absolute HTTP(S) redirect destinations are accepted; unsupported behavior and
|
|
7
|
+
duplicate paths fail rather than being discarded or overwritten.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
urlcode bulk-import csv redirects.csv --out ./imported --dry-run
|
|
11
|
+
urlcode bulk-import csv redirects.csv --out ./imported
|
|
12
|
+
urlcode validate --local --project ./imported
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
CSV requires the header `path,url,status`; status can be empty for 302. JSON and
|
|
16
|
+
YAML are arrays of `{path, url, status?}` rows. Input is limited to 32 MiB and
|
|
17
|
+
100,000 routes. This command is for data import, not arbitrary provider config.
|
|
18
|
+
Use explicit provider interchange commands when migrating provider files.
|
|
19
|
+
|
|
20
|
+
The SDK provides
|
|
21
|
+
`importBulkProject(text, format, output, {dryRun, source})`. `format` is `csv`,
|
|
22
|
+
`json` or `yaml`. The report includes `ok`, `routeCount`, source/row diagnostics,
|
|
23
|
+
a SHA-256 fingerprint of the exact input text, and a bounded output-file plan.
|
|
24
|
+
Invalid input returns `ok: false` without writing files. Filesystem/output
|
|
25
|
+
refusals throw. `source` is a caller-supplied provenance label, not a file to read.
|
|
26
|
+
|
|
27
|
+
Routes are sorted by literal path and divided into include files with at most
|
|
28
|
+
1,000 routes each. At 100,000 routes this produces 100 include files. The entry
|
|
29
|
+
`urlcode.yaml` holds the include list and empty `routes`. `provenance.json`
|
|
30
|
+
records the source label, input fingerprint, counts and each shard's first/last
|
|
31
|
+
path. It does not embed the original input or claim exact source line mappings
|
|
32
|
+
for successful output rows; retain the original file in version control if that
|
|
33
|
+
traceability is needed. Invalid row diagnostics preserve their source positions.
|
|
34
|
+
|
|
35
|
+
Dry-run performs conversion and output planning without writing the requested
|
|
36
|
+
destination. Import requires a new output directory under an existing parent;
|
|
37
|
+
there is no implicit merge or overwrite mode. As with recipes, dependencies are
|
|
38
|
+
written first and a rename publishes the completed entry YAML last. Failed
|
|
39
|
+
writes clean up the directory created by that invocation. Source data is never
|
|
40
|
+
interpreted as shell code. The caller must control the output parent while
|
|
41
|
+
publishing.
|
|
42
|
+
|
|
43
|
+
## Local scale evidence
|
|
44
|
+
|
|
45
|
+
Run each dataset in a fresh process, sequentially, without concurrent builds:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
node benchmarks/bulk.ts 1000
|
|
49
|
+
node benchmarks/bulk.ts 10000
|
|
50
|
+
node benchmarks/bulk.ts 100000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Observed on 2026-09-17 against the final integrated next-phase working tree,
|
|
54
|
+
with the merged TypeScript 6.0.3 dependency lock, Node v26.8.2, macOS arm64,
|
|
55
|
+
Apple M4 Pro and 48 GiB system memory. These measurements include the final
|
|
56
|
+
capability, revision-pin and egress activation paths. Synthetic routes redirect `/rN` to `https://example.com/items/N`.
|
|
57
|
+
Conversion includes input validation, semantic compilation and sharded output
|
|
58
|
+
publication; activation uses the normal `createRuntime` loader and compiler.
|
|
59
|
+
Lookup measures 5,000 `Runtime.handle` requests at concurrency one after 100
|
|
60
|
+
warm-up requests, validating each status and Location. It excludes socket/TLS
|
|
61
|
+
transport. Memory is sampled process RSS/heap after phases, not peak memory or
|
|
62
|
+
an isolated worker measurement.
|
|
63
|
+
|
|
64
|
+
| Routes | Includes | Conversion ms | Activation ms | RSS after activation MiB | Heap MiB | Lookup p95 ms |
|
|
65
|
+
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
66
|
+
| 1,000 | 1 | 56 | 139 | 180 | 27 | 0.002292 |
|
|
67
|
+
| 10,000 | 10 | 272 | 373 | 288 | 46 | 0.002916 |
|
|
68
|
+
| 100,000 | 100 | 5,530 | 8,838 | 537 | 214 | 0.003791 |
|
|
69
|
+
|
|
70
|
+
All three sequential fresh-process datasets activated and returned the expected
|
|
71
|
+
redirects. These are single observations, not a repeated-run statistical study;
|
|
72
|
+
background host load can change startup and memory measurements. Unlike the
|
|
73
|
+
previous single-document 100,000-route routing benchmark, the sharded project
|
|
74
|
+
fits the existing configuration worker's 256 MiB heap and 10-second deadline.
|
|
75
|
+
No loader, route-count, compiler deadline or runtime safety limit was increased.
|
|
76
|
+
This local result does not establish cross-platform capacity, provider
|
|
77
|
+
performance, peak-memory bounds, concurrency/soak behavior or production SLOs.
|
|
78
|
+
The benchmark reports failure phase and error if a limit is reached on another
|
|
79
|
+
machine; it does not retry with relaxed limits.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Capabilities and normalized route representation
|
|
2
|
+
|
|
3
|
+
URLCode is a portable runtime for programmable URL behavior: **URL behavior as
|
|
4
|
+
code**. YAML describes behavior; operators supply infrastructure and authority.
|
|
5
|
+
|
|
6
|
+
## Inspect target support
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
urlcode capabilities
|
|
10
|
+
urlcode capabilities --target self-hosted
|
|
11
|
+
urlcode capabilities --target cloudflare --json
|
|
12
|
+
urlcode capabilities --target aws
|
|
13
|
+
urlcode capabilities --target vercel
|
|
14
|
+
urlcode capabilities --target static
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command needs no project or credentials. `node` is an alias for
|
|
18
|
+
`self-hosted`, matching the existing embedding API. Unknown target names fail.
|
|
19
|
+
JSON has `format: 1`, target deployment evidence and capability rows.
|
|
20
|
+
`doctor` also reports `capabilityTargets`; its `providers` list remains empty
|
|
21
|
+
because no provider deployment has been verified. Canonical
|
|
22
|
+
names follow the schema (`respond`, `extension`, `policies.security`), not
|
|
23
|
+
marketing synonyms. `proxy` and `signals` are self-hosted capabilities requiring
|
|
24
|
+
external revision-pinned origin grants. `conditions` (`match`) and `conditional`
|
|
25
|
+
(disjoint cases) are supported by self-hosted/AWS/Vercel and refused by
|
|
26
|
+
Cloudflare (no artifact lowering yet) and by `static` (no server to match a
|
|
27
|
+
request against). `extension`/`policies.extensions` report per-extension
|
|
28
|
+
support from the registered extension's own declared `targets` when a
|
|
29
|
+
`--host-file` is supplied; without one they report `conditional`/`unknown`
|
|
30
|
+
rather than a blanket answer. See [egress](EGRESS.md) and
|
|
31
|
+
[conditions](CONDITIONS.md).
|
|
32
|
+
|
|
33
|
+
| Support | Meaning |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| native | Implemented by the local runtime or Node adapter |
|
|
36
|
+
| compiled | Implemented by the Cloudflare or static-hosting compiler and its runtime/build output |
|
|
37
|
+
| conditional | Depends on configuration; inspect the actual project |
|
|
38
|
+
| delegated | Existing policy contract relies on provider behavior |
|
|
39
|
+
| refused | No implementation that this target can activate |
|
|
40
|
+
| unknown | No support evidence; fail closed during project analysis |
|
|
41
|
+
|
|
42
|
+
`native` and `compiled` describe local implementation tests. AWS, Vercel,
|
|
43
|
+
Cloudflare and static deployment evidence remains **unverified**. This is not a
|
|
44
|
+
blanket exact-portability promise. Cloudflare coalesces duplicate headers and
|
|
45
|
+
receives a normalized Request target; AWS accepts payload v2 only; `static` has
|
|
46
|
+
no server at all, so it refuses every capability that needs one (parameters,
|
|
47
|
+
request bodies, response headers, bindings, every `policies.*`) in addition to
|
|
48
|
+
`function`/`middleware`. See [Cloudflare](CLOUDFLARE.md), [AWS](AWS.md),
|
|
49
|
+
[Vercel](VERCEL.md) and [static hosting](STATIC.md) for transport and fidelity
|
|
50
|
+
limits. Compression is explicitly delegated, not verified equivalent to
|
|
51
|
+
operator-selected settings. Route throttle counters and caches remain per
|
|
52
|
+
instance. No supported entry bypasses semantic validation, required operator
|
|
53
|
+
grants or deployment prerequisites.
|
|
54
|
+
|
|
55
|
+
## One capability or one schema fragment
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
urlcode capabilities redirect
|
|
59
|
+
urlcode capabilities policies.throttle --json
|
|
60
|
+
urlcode schema route
|
|
61
|
+
urlcode schema policies.cache --json
|
|
62
|
+
urlcode schema site.sitemap --yaml
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`urlcode capabilities <name>` prints one catalog entry: its kind (handler,
|
|
66
|
+
policy, routing, request, binding, egress, middleware or project), a summary,
|
|
67
|
+
the resolved schema fragment(s), constraints, the operator grants the capability
|
|
68
|
+
needs at activation, support per target, the targets that refuse it, and the
|
|
69
|
+
bundled recipes and cookbook routes that use it. Names are the catalog names
|
|
70
|
+
(`redirect`, `bindings`, `policies.cache`); an unknown name fails with exit 1
|
|
71
|
+
and lists the valid names. `--target` applies to the full catalog only.
|
|
72
|
+
|
|
73
|
+
`urlcode schema <path>` prints only that fragment of
|
|
74
|
+
`schemas/urlcode.schema.json` with local `$ref`s resolved inline. Paths are
|
|
75
|
+
top-level document keys (`routes`, `policies`, `site`, `extensions`), `route`,
|
|
76
|
+
or a route property (`redirect`, `middleware`, `match`, `env`), optionally
|
|
77
|
+
followed by nested property names (`policies.cache`, `request.body`,
|
|
78
|
+
`site.sitemap`). Resolution is bounded and cycle-safe; where a nested object is
|
|
79
|
+
its own path (`route` inside `routes`, `policies` inside `route`) it is
|
|
80
|
+
summarized with a `$comment` naming that path so every fragment stays under
|
|
81
|
+
16 KiB. Fragments describe shape only: they carry no defaults, validation
|
|
82
|
+
result or operator authority, and the full schema remains the contract.
|
|
83
|
+
|
|
84
|
+
Both commands read bundled package data and need no project, credentials or
|
|
85
|
+
network. The SDK exposes them as `getCapability(name)` and
|
|
86
|
+
`getSchemaFragment(path)`; the MCP server as `get_capability` and `get_schema`
|
|
87
|
+
(see [tooling](TOOLING.md)). Grant descriptions name the operator flag or policy
|
|
88
|
+
involved, never binding values.
|
|
89
|
+
|
|
90
|
+
## Programmatic analysis
|
|
91
|
+
|
|
92
|
+
The main package exports `getCapabilities`, `routeCapabilities`,
|
|
93
|
+
`analyzeProjectCapabilities`, `analyzeCompiledCapabilities`,
|
|
94
|
+
`assertTargetCompatibility`, `normalizeCapabilityTarget` and their report types.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { loadDocument, analyzeProjectCapabilities,
|
|
98
|
+
assertTargetCompatibility } from '@jimhoyd/urlcode';
|
|
99
|
+
|
|
100
|
+
const loaded = await loadDocument('./project');
|
|
101
|
+
const report = analyzeProjectCapabilities(loaded, 'cloudflare');
|
|
102
|
+
console.log(report.issues); // path, capability, support, reason; never binding values
|
|
103
|
+
assertTargetCompatibility(report);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This low-level example examines declared routes. Runtime activation and builds
|
|
107
|
+
first expand `site` conventions using the operator origin, then analyze all
|
|
108
|
+
routes including generated ones. A report is a compatibility preflight, **not**
|
|
109
|
+
a substitute for compilation/validation. Disabled and expired routes are still
|
|
110
|
+
checked. `analyzeProjectCapabilities`/`analyzeCompiledCapabilities` take an
|
|
111
|
+
optional resolved extension registration set (the same shape `--host-file`
|
|
112
|
+
loads); pass it to get per-extension `refused`/`native` from that extension's
|
|
113
|
+
own `targets` instead of the generic `conditional`/`unknown` answer.
|
|
114
|
+
`compatible` means there are no refused, unknown or unresolved conditional
|
|
115
|
+
requirements. Explicit delegation and transport limitations still apply.
|
|
116
|
+
|
|
117
|
+
Requirements include effective inherited/profile policies after route overrides
|
|
118
|
+
and `false` removals. Policy modules' existing `targets(config)` functions remain
|
|
119
|
+
the source of policy decisions. The catalog says serverless throttle is
|
|
120
|
+
conditional; a project report resolves `partition: route` to native and the
|
|
121
|
+
client partitions to refused. Reports contain paths and capability facts, not
|
|
122
|
+
sources, destinations, binding names/values, code, validator closures or assets.
|
|
123
|
+
|
|
124
|
+
Build/activation refusals aggregate all incompatible requirements and name each
|
|
125
|
+
route, capability, target and reason before any artifact files are written.
|
|
126
|
+
Unsupported bindings on Cloudflare fail before credentials are resolved.
|
|
127
|
+
|
|
128
|
+
## Existing IR, formalized
|
|
129
|
+
|
|
130
|
+
The implementation already has a useful internal representation:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
strict YAML + schema validation + includes
|
|
134
|
+
→ site expansion
|
|
135
|
+
→ shared capability preflight (declarations, no credentials)
|
|
136
|
+
→ semantic route compilation
|
|
137
|
+
→ CompiledRouteTable / CompiledRoute
|
|
138
|
+
→ capability analysis / target lowering
|
|
139
|
+
→ host assets + policy chains + trusted or isolated function dispatch, or Worker artifact
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`CompiledRoute` in `src/types.ts` extends shared `MatchableRoute` with validated
|
|
143
|
+
parameters, normalized HTTP replies/headers, resolved bindings and source
|
|
144
|
+
references. `CompiledRouteTable` indexes literal paths, parameter buckets and
|
|
145
|
+
static mounts. `router.ts` owns precedence, collision checks, default methods,
|
|
146
|
+
input/reference validation and normalization. `match.ts` supplies portable
|
|
147
|
+
request-time matching to both runtime and Worker. No second parser or competing
|
|
148
|
+
route IR is introduced.
|
|
149
|
+
|
|
150
|
+
`routeCapabilities` is a value-free projection used for both declaration
|
|
151
|
+
preflight and compiled analysis. Preflight deliberately precedes full semantic
|
|
152
|
+
validation to report unsupported features before reading missing assets or
|
|
153
|
+
resolving secrets; it cannot authorize or validate a project. Normal compilation
|
|
154
|
+
and policy validation still run. AWS/Vercel consume the shared runtime IR;
|
|
155
|
+
Cloudflare additionally analyzes the compiled table before serializing its
|
|
156
|
+
allowlisted artifact fields and standalone validators.
|
|
157
|
+
|
|
158
|
+
The compiled table is internal, mutable during activation and **not serializable
|
|
159
|
+
as an interchange contract**: bindings contain resolved secrets, validators are
|
|
160
|
+
functions, assets contain bytes and policy chains own host state. The existing
|
|
161
|
+
Cloudflare artifact is a separate versioned lowering, not a replacement IR.
|
|
162
|
+
[Interchange](INTERCHANGE.md) projects a validated literal-redirect subset and
|
|
163
|
+
explicitly reports unsupported semantics; it never dumps compiled routes.
|
|
164
|
+
The next-phase schema extends this same IR with normalized condition cases,
|
|
165
|
+
proxy headers and signal definitions. Resolved egress headers are private runtime
|
|
166
|
+
state and must never be serialized. Capability analysis itself adds no authority
|
|
167
|
+
and does not run in the request path.
|
|
168
|
+
|
|
169
|
+
See the [repository review and incremental plan](NEXT-PHASE-PLAN.md) for the
|
|
170
|
+
implementation status. Provider deployment tests, independent security review and
|
|
171
|
+
operational soak/recovery proof are separate work.
|
|
172
|
+
|
|
173
|
+
## Local performance check
|
|
174
|
+
|
|
175
|
+
2026-09-17, Node 26.8.2, macOS arm64, Apple M4 Pro, 48 GiB RAM.
|
|
176
|
+
`npm run benchmark -- <count>` runs 5,000 loopback requests at concurrency 16.
|
|
177
|
+
Single runs against baseline `1a00294` and this change, not a statistical study:
|
|
178
|
+
|
|
179
|
+
| Routes | Startup ms before / after | RSS MiB before / after | Heap MiB before / after | Requests/s before / after | p95 ms before / after |
|
|
180
|
+
| --- | --- | --- | --- | --- | --- |
|
|
181
|
+
| 1,000 | 135 / 138 | 183 / 182 | 29 / 34 | 24,645 / 24,701 | 1.18 / 1.23 |
|
|
182
|
+
| 10,000 | 312 / 303 | 211 / 207 | 55 / 63 | 6,762 / 6,829 | 4.08 / 4.09 |
|
|
183
|
+
|
|
184
|
+
Both baseline and updated 100,000-route runs failed with `Configuration worker
|
|
185
|
+
resource limit or failure` before route compilation. The worker's existing
|
|
186
|
+
resource bounds are unchanged; the configured 100k route ceiling is not evidence
|
|
187
|
+
that every 100k YAML document fits those bounds. Bulk-scale remediation and
|
|
188
|
+
repeatable memory profiling were subsequent work at Phase A. The new
|
|
189
|
+
[bulk sharding benchmark](BULK.md) successfully activates 100,000 routes within
|
|
190
|
+
the unchanged worker limits. Capability analysis adds
|
|
191
|
+
linear activation work and temporary report allocations; no request-time checks
|
|
192
|
+
were added. These measurements are not provider, soak or capacity certification.
|
package/docs/CAPACITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Capacity, concurrency and system limits
|
|
2
2
|
|
|
3
|
-
These are 0.
|
|
3
|
+
These are 0.3.0 implementation limits and planning models, not a throughput
|
|
4
4
|
SLA. Route count, connections, in-flight requests and sandbox concurrency are
|
|
5
5
|
four different quantities. Always measure the actual application on deployment
|
|
6
6
|
hardware with the intended proxy, TLS, logging and limits enabled.
|
|
@@ -13,16 +13,56 @@ lookup after path parsing). Parameter candidates are grouped by segment count
|
|
|
13
13
|
and scanned in specificity order; matching is O(P × L) in the worst case for P
|
|
14
14
|
candidates and L segments. Static mount prefixes are scanned longest first.
|
|
15
15
|
|
|
16
|
-
Plain redirects, declared responses
|
|
17
|
-
the sandbox
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
Plain redirects, declared responses and assets do not enter
|
|
17
|
+
the sandbox or the trusted executor.
|
|
18
|
+
|
|
19
|
+
`function`/`middleware` routes have **two distinct capacity models**, chosen
|
|
20
|
+
per route by `sandbox` (docs/SPIKE-DEFAULT-TRUST-MODEL.md):
|
|
21
|
+
|
|
22
|
+
- **`sandbox: true` (the isolated worker pool, unchanged from every earlier
|
|
23
|
+
release):** a function or any attached middleware occupies one shared
|
|
24
|
+
worker slot for its whole chain. Workers are shared by all `sandbox: true`
|
|
25
|
+
routes in that snapshot; there is no per-route fairness or reserved
|
|
26
|
+
capacity. Awaiting guest timers still occupies the slot. A fresh guest and
|
|
27
|
+
module initialization are part of each call. See "Enforced limits and
|
|
28
|
+
defaults" below for the numbers (2 workers, 5 s deadline, 32 MiB heap).
|
|
29
|
+
- **`sandbox` false/absent (the trusted default):** the call runs in-process,
|
|
30
|
+
on the same event loop as everything else the server does — ordinary Node
|
|
31
|
+
concurrency, not a fixed worker-slot ceiling. There is no separate pool to
|
|
32
|
+
exhaust and no per-invocation heap/module reset: it is bounded by the same
|
|
33
|
+
`--max-in-flight` HTTP admission cap (default 64) that bounds every other
|
|
34
|
+
request, not by a `workers` count. A trusted call's declared `timeoutMs`
|
|
35
|
+
races the call's own promise rather than forcibly terminating a worker
|
|
36
|
+
thread — see "Trusted-path deadlines" below for what that does and does not
|
|
37
|
+
protect against.
|
|
38
|
+
|
|
39
|
+
This was architectural reasoning, not a measurement, until the run below:
|
|
40
|
+
see "Measured: sandboxed vs trusted dispatch" for the actual comparison.
|
|
22
41
|
|
|
23
42
|
Node's main event loop remains a shared bottleneck for HTTP parsing, logging and
|
|
24
|
-
native responses.
|
|
25
|
-
|
|
43
|
+
native responses. The sandbox contains a `sandbox: true` route's application
|
|
44
|
+
code authority and bounds its individual execution; the trusted default does
|
|
45
|
+
not attempt to, by design. Neither mode makes all host resources immune to
|
|
46
|
+
exhaustion.
|
|
47
|
+
|
|
48
|
+
### Trusted-path deadlines
|
|
49
|
+
|
|
50
|
+
A sandboxed worker's deadline is enforced by an interrupt handler the WASM
|
|
51
|
+
engine checks between guest operations, backed by an independent outer
|
|
52
|
+
termination that kills the worker thread if the guest never yields — the
|
|
53
|
+
worker (and its slot) can be forcibly reclaimed even from a stuck call. A
|
|
54
|
+
trusted, in-process call has no such mechanism available: `timeoutMs` starts
|
|
55
|
+
a race between the call's promise and a timer, so a call that never resolves
|
|
56
|
+
(an unresolved promise, an awaited operation that never completes) is
|
|
57
|
+
answered with a 504 on schedule, but a call that blocks the event loop
|
|
58
|
+
*synchronously* (an infinite `while` loop, a huge synchronous computation)
|
|
59
|
+
is not preempted — it keeps running, delays that timer's own firing, and
|
|
60
|
+
holds up every other request on the same process until it returns control to
|
|
61
|
+
the event loop or the process is restarted. This is a real, documented
|
|
62
|
+
difference from the sandboxed path's guarantee, not an oversight: Node has no
|
|
63
|
+
supported way to interrupt another turn of the same thread's event loop from
|
|
64
|
+
inside it. A route whose trusted code cannot be trusted to yield promptly is
|
|
65
|
+
exactly the kind of route `sandbox: true` exists for.
|
|
26
66
|
|
|
27
67
|
## Enforced limits and defaults
|
|
28
68
|
|
|
@@ -39,15 +79,17 @@ individual execution; it does not make all host resources immune to exhaustion.
|
|
|
39
79
|
| Requests per socket | 1,000 | Connection recycling; not a requests-per-second limit |
|
|
40
80
|
| Header / request receipt / keep-alive timeouts | 10 s / 15 s / 5 s | These are not an overall end-to-end response deadline |
|
|
41
81
|
| Request body | 1 MiB default | Buffered; route maxBytes can tighten to 0–1 MiB |
|
|
42
|
-
| Sandbox concurrency | 2 workers, no queue | Shared per snapshot; full pool returns 503 |
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
| Function
|
|
49
|
-
|
|
|
50
|
-
|
|
|
82
|
+
| Sandbox concurrency (`sandbox: true` only) | 2 workers, no queue | Shared per `sandbox: true` snapshot; full pool returns 503 |
|
|
83
|
+
| Sandbox execution deadline (`sandbox: true` only) | 5 s default | Entire middleware + handler invocation; forcibly terminates the worker; timeout returns 504 |
|
|
84
|
+
| Trusted concurrency (`sandbox` false/absent, the default) | Ordinary Node concurrency | Bounded by `--max-in-flight` (default 64), not a worker count; no separate pool to exhaust |
|
|
85
|
+
| Trusted execution deadline (`sandbox` false/absent) | 5 s default (same `timeoutMs` knob) | Races the call's promise; cannot preempt synchronous event-loop-blocking code (see "Trusted-path deadlines" above); timeout returns 504 |
|
|
86
|
+
| Guest heap / stack (`sandbox: true` only) | 32 MiB / 512 KiB | Fresh per invocation; not a bound on total process RSS |
|
|
87
|
+
| Outer worker old-generation V8 budget (`sandbox: true` only) | 128 MiB | Separate from WASM/host/native allocations |
|
|
88
|
+
| Function response | 1 MiB default, 16 KiB / 256 header pairs | Buffered text/JSON; YAML headers also bounded; applies to both execution modes |
|
|
89
|
+
| Middleware | 16 entries per route | One shared slot/deadline (`sandbox: true`) or one in-process call (trusted), not 16 independent workers either way |
|
|
90
|
+
| Function sources (`sandbox: true` only) | 128 modules, 1 MiB/module, 4 MiB total | Sandboxed snapshot, including middleware dependencies; a trusted route's own source is hashed for grant pinning but not bundled or budget-limited this way (see docs/FUNCTION-SECURITY.md) |
|
|
91
|
+
| Worker startup (`sandbox: true` only) | 5 s deadline | Failure rejects activation; no untrusted host fallback |
|
|
92
|
+
| Worker replacement (`sandbox: true` only) | Up to 3 exits/minute per slot trigger replacement | Further churn leaves the slot unavailable until reload/restart |
|
|
51
93
|
| Assets | 16 MiB/file, 64 MiB unique contents | Buffered immutable snapshots; 10,000 static entries, depth 20 |
|
|
52
94
|
| Logger buffering | Drop at 1 MiB stdout buffering | Reports logs_dropped when output recovers |
|
|
53
95
|
|
|
@@ -71,8 +113,70 @@ increasing a timeout also increases how long an attacker can occupy capacity.
|
|
|
71
113
|
The CLI uses defaults. Keep settings identical across replicas unless testing a
|
|
72
114
|
controlled rollout. See [operations](OPERATIONS.md).
|
|
73
115
|
|
|
116
|
+
## Measured: sandboxed vs trusted dispatch
|
|
117
|
+
|
|
118
|
+
The claim above — that the trusted path has "no fixed worker-slot ceiling"
|
|
119
|
+
and scales as ordinary Node concurrency instead — was written as
|
|
120
|
+
architectural reasoning when the trusted-by-default execution model shipped
|
|
121
|
+
(docs/SPIKE-DEFAULT-TRUST-MODEL.md), not backed by a benchmark run. This is
|
|
122
|
+
that run: `benchmarks/sandbox-vs-trusted.ts`
|
|
123
|
+
(`npm run benchmark:sandbox-vs-trusted`), same 20 ms handler source on both a
|
|
124
|
+
`sandbox: true` route and a trusted route in the same server, default
|
|
125
|
+
settings (`workers: 2`, `maxInFlightRequests: 64`, `timeoutMs: 5000`), 2,000
|
|
126
|
+
requests per concurrency level after a 20-request warmup, keep-alive
|
|
127
|
+
connections. Raw output:
|
|
128
|
+
[benchmarks/results/2026-09-19-sandbox-vs-trusted.json](../benchmarks/results/2026-09-19-sandbox-vs-trusted.json).
|
|
129
|
+
|
|
130
|
+
One development machine: Intel Xeon @ 2.10 GHz, 4 vCPUs, 16 GiB RAM, Linux
|
|
131
|
+
x64, Node v22.22.2, 2026-09-19. This is one machine's numbers, not a
|
|
132
|
+
universal claim — re-run on deployment hardware before sizing anything.
|
|
133
|
+
|
|
134
|
+
| Concurrency | Sandboxed req/s (successful) | Sandboxed shed | Sandboxed p95 | Trusted req/s (successful) | Trusted shed | Trusted p95 |
|
|
135
|
+
|---:|---:|---:|---:|---:|---:|---:|
|
|
136
|
+
| 1 | 39 | 0 / 2000 | 26.9 ms | 47 | 0 / 2000 | 21.5 ms |
|
|
137
|
+
| 2 | 80 | 0 / 2000 | 26.5 ms | 95 | 0 / 2000 | 21.7 ms |
|
|
138
|
+
| 8 | 80 | 1,912 / 2000 | 30.2 ms | 379 | 0 / 2000 | 22.1 ms |
|
|
139
|
+
| 32 | 80 | 1,936 / 2000 | 43.5 ms | 1,465 | 0 / 2000 | 23.5 ms |
|
|
140
|
+
| 128 | 80 | 1,950 / 2000 | 70.0 ms | 2,615 | 521 / 2000 | 80.6 ms |
|
|
141
|
+
|
|
142
|
+
"Sandboxed req/s" and "shed" are separate columns for a reason: throughput
|
|
143
|
+
computed over successful responses only is flat at ~80 req/s from
|
|
144
|
+
concurrency 2 upward because the 2-worker ceiling admits at most two
|
|
145
|
+
in-flight calls at a time and returns 503 immediately for the rest, exactly
|
|
146
|
+
as "Enforced limits and defaults" describes (no queue). The ceiling is
|
|
147
|
+
visible starting at concurrency 8 in this run (already >95% shed) and only
|
|
148
|
+
gets worse as concurrency rises — 2 concurrent in-flight sandboxed calls is
|
|
149
|
+
the practical limit regardless of how much load is offered.
|
|
150
|
+
|
|
151
|
+
The trusted path has no such wall: it scales with offered concurrency —
|
|
152
|
+
379 req/s at 8, 1,465 req/s at 32, both with zero shedding — until offered
|
|
153
|
+
concurrency (128) exceeds the `--max-in-flight` admission cap (64), at which
|
|
154
|
+
point it starts shedding too (521/2000, p95 rises to 80.6 ms) but keeps
|
|
155
|
+
substantially higher successful throughput (2,615 req/s) than the sandboxed
|
|
156
|
+
path ever reaches at any concurrency tested. This confirms the architectural
|
|
157
|
+
prediction rather than contradicting it: the trusted path's ceiling is the
|
|
158
|
+
admission cap, not a fixed worker count, and it sits far above the sandboxed
|
|
159
|
+
path's 2-worker ceiling in this run. It is still a real ceiling — raising
|
|
160
|
+
`--max-in-flight` moves it, it does not remove it — and it is shared with
|
|
161
|
+
every other route on the process, per "Native routes and mixed traffic"
|
|
162
|
+
below.
|
|
163
|
+
|
|
164
|
+
Caveats specific to this run: one handler shape (a 20 ms guest/host timer,
|
|
165
|
+
chosen to make both ceilings visible rather than to represent any particular
|
|
166
|
+
application), one machine, one process with both route types present
|
|
167
|
+
(so CPU contention between them is part of the trusted numbers, not
|
|
168
|
+
excluded), and no TLS, proxy or production logging in the path. Re-run with
|
|
169
|
+
your own handler's actual latency profile before sizing a deployment; see
|
|
170
|
+
"Establish a deployment budget" below.
|
|
171
|
+
|
|
74
172
|
## A useful theoretical model
|
|
75
173
|
|
|
174
|
+
This worker-slot model describes the `sandbox: true` path only. A trusted
|
|
175
|
+
route has no fixed worker count to plug in as W; its ceiling is ordinary Node
|
|
176
|
+
request concurrency bounded by `--max-in-flight`, not this model — see
|
|
177
|
+
"Measured: sandboxed vs trusted dispatch" above for what that ceiling looks
|
|
178
|
+
like in practice.
|
|
179
|
+
|
|
76
180
|
Let W be worker slots, S the measured mean slot occupancy in seconds (including
|
|
77
181
|
sandbox startup and cleanup effects), and lambda the offered programmable
|
|
78
182
|
requests per second. An idealized worker ceiling is:
|
|
@@ -159,6 +263,13 @@ loop even though the HTTP listener is not restarted. Do not equate atomic swap
|
|
|
159
263
|
with zero latency impact or incremental route updates. Prefer candidate replicas
|
|
160
264
|
and traffic switching for production. `serve` does not watch configuration.
|
|
161
265
|
|
|
266
|
+
A trusted route's own entry file is re-imported fresh on every reload (see
|
|
267
|
+
[docs/FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)), matching the sandboxed
|
|
268
|
+
pool rebuilding its whole snapshot; a file that entry only imports is not,
|
|
269
|
+
since ordinary Node module resolution — not a per-reload snapshot — governs
|
|
270
|
+
it. Restart the process rather than reload after editing a trusted route's
|
|
271
|
+
dependency, not just its declared `source`.
|
|
272
|
+
|
|
162
273
|
Before parser-worker limits were introduced, recorded 100k-route startup RSS was about 621 MiB on one development machine,
|
|
163
274
|
above the illustrative 512 MiB container example. Route limits are acceptance
|
|
164
275
|
caps, not a promise that the maximum fits your deployment. See [measurements](PERFORMANCE.md).
|
|
@@ -186,20 +297,6 @@ production exercise. The readiness endpoint can stay 200 while all worker slots
|
|
|
186
297
|
are busy. Use error/latency signals too. No universal safe RPS can be derived
|
|
187
298
|
from the route count or these defaults alone. See [resilience](RESILIENCE.md).
|
|
188
299
|
|
|
189
|
-
## Optional stored-link capacity
|
|
190
|
-
|
|
191
|
-
Each SQLite store defaults to two read-only worker connections; writable stores
|
|
192
|
-
add one writer. Independent read/write admission caps default to 32 each across
|
|
193
|
-
their pool. Readers are configurable from 1–8, and caps from 1–32. Operations have
|
|
194
|
-
a 5-second deadline and one-second SQLite lock wait. Public serving has no writer. The
|
|
195
|
-
initial cap is 100,000 stored records across collections; this is separate from
|
|
196
|
-
the YAML route count. No lookup cache is used, so visibility does not depend on
|
|
197
|
-
cache invalidation. Store failures/overload return 503 and failed workers need
|
|
198
|
-
reload/restart; healthy readers can continue while readiness is degraded. Management has a separate listener with 64 connections, 8 KiB
|
|
199
|
-
headers and 16 KiB JSON bodies. Rate limiting remains an ingress responsibility.
|
|
200
|
-
Do not extrapolate in-memory redirect benchmark numbers to database lookups;
|
|
201
|
-
measure disk, writes, contention and restoration on the target host.
|
|
202
|
-
|
|
203
300
|
Configuration parsing/schema validation now run in a terminated-on-deadline worker;
|
|
204
301
|
route compilation still runs cooperatively on the host (10 seconds, yields every
|
|
205
302
|
64 routes). Source, AST, structured-clone output, compiled routes, assets, module
|
package/docs/CI.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Checking a project on GitHub
|
|
2
|
+
|
|
3
|
+
`jimhoyd-com/urlcode/action` is a composite GitHub Action for a URLCode
|
|
4
|
+
*project*: a repository with a `urlcode.yaml`. It runs the same local checks
|
|
5
|
+
you run by hand and, on pull requests, keeps one comment up to date with the
|
|
6
|
+
route-inventory diff against the base branch. It needs no cloud credentials;
|
|
7
|
+
the only token it touches is the workflow's own `GITHUB_TOKEN`.
|
|
8
|
+
|
|
9
|
+
The starter ships it as `.github/workflows/urlcode.yml` (`urlcode init` copies
|
|
10
|
+
it; the [template repository](https://github.com/jimhoyd-com/urlcode-template)
|
|
11
|
+
carries the same file):
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
name: urlcode
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
pull_request:
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
pull-requests: write # the sticky route-diff comment; drop it to only log the diff
|
|
21
|
+
jobs:
|
|
22
|
+
check:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
26
|
+
with:
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
- uses: jimhoyd-com/urlcode/action@main # pin a release tag or commit
|
|
29
|
+
with:
|
|
30
|
+
expect-routes: 2
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The action lives at `action/action.yml` in the runtime repository, so the
|
|
34
|
+
reference is `jimhoyd-com/urlcode/action@<ref>`. Pin `<ref>` the way you pin
|
|
35
|
+
the runtime: a release tag or a commit SHA, not `main`, once the project is
|
|
36
|
+
past its first commit.
|
|
37
|
+
|
|
38
|
+
## What it runs
|
|
39
|
+
|
|
40
|
+
| Step | Command | Fails the job when |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Install | `npm ci` in the project (see below) | Dependencies do not install |
|
|
43
|
+
| Validate | `urlcode validate --project <project>` | The YAML, includes, functions or bindings do not load |
|
|
44
|
+
| Test | `urlcode test --project <project>` | A `tests/requests.json` fixture fails |
|
|
45
|
+
| Audit | `urlcode audit --project <project> --expect-routes N --compliance <profile>` | Count mismatch, failed generated check, uncovered active route/method, or a `high` compliance finding without `compliance-warn` |
|
|
46
|
+
| Route diff | `urlcode routes --compare base.json --format markdown` | Never; it reports |
|
|
47
|
+
|
|
48
|
+
Every command is the CLI documented in [readiness](READINESS.md) and
|
|
49
|
+
[compliance](COMPLIANCE.md); the action adds no check of its own. `--origin`
|
|
50
|
+
is passed to validate, test and audit when set. Steps run with `bash`, so the
|
|
51
|
+
action works on the Linux, macOS and Windows runners.
|
|
52
|
+
|
|
53
|
+
The project's runtime comes from the project. With a `package.json` the action
|
|
54
|
+
runs `npm ci` (or `npm install` without a lockfile) and uses the
|
|
55
|
+
`@jimhoyd/urlcode` that resolves from there, hoisted or not. A project without
|
|
56
|
+
`package.json`, such as a fresh `urlcode init`, gets the `runtime` input
|
|
57
|
+
installed into a private prefix under the runner's temp directory:
|
|
58
|
+
`@jimhoyd/urlcode` (the latest release) by default, or a version
|
|
59
|
+
(`@jimhoyd/urlcode@0.3.0`) or an absolute tarball path.
|
|
60
|
+
|
|
61
|
+
## Inputs
|
|
62
|
+
|
|
63
|
+
| Input | Default | Meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `project` | `.` | Directory containing `urlcode.yaml`, relative to the workspace |
|
|
66
|
+
| `node-version` | `26` | Passed to `actions/setup-node` |
|
|
67
|
+
| `runtime` | `@jimhoyd/urlcode` | npm spec installed when the project has no `package.json` |
|
|
68
|
+
| `expect-routes` | empty | `audit --expect-routes N`; empty skips the count check |
|
|
69
|
+
| `compliance` | `baseline` | `baseline`, `strict`, `privacy` or `none` |
|
|
70
|
+
| `compliance-rules` | empty | Absolute path to an operator rules module outside the project |
|
|
71
|
+
| `compliance-warn` | `false` | Report findings without failing (`--compliance-warn`) |
|
|
72
|
+
| `origin` | empty | Public origin of the deployment under review |
|
|
73
|
+
| `route-diff` | `true` | Post the route diff on pull requests |
|
|
74
|
+
|
|
75
|
+
Output `route-diff` is the path of the Markdown diff on a pull request, empty
|
|
76
|
+
otherwise, for a later step that wants to upload or reuse it.
|
|
77
|
+
|
|
78
|
+
## The sticky comment
|
|
79
|
+
|
|
80
|
+
On `pull_request` events the action fetches the base commit, checks it out into
|
|
81
|
+
a temporary git worktree, runs `urlcode routes` there and on the head with the
|
|
82
|
+
head's runtime, and renders `routes --compare` as Markdown: one table each for
|
|
83
|
+
added, removed and changed routes, or "No route changes". A changed route lists
|
|
84
|
+
each differing field (handler, methods, state, middleware count, policies,
|
|
85
|
+
generated marker or policy description) with its before and after value.
|
|
86
|
+
|
|
87
|
+
`action/comment.mjs` then finds the pull request's comments for the marker
|
|
88
|
+
`<!-- urlcode-route-diff project="<project>" -->` and updates that comment,
|
|
89
|
+
or creates it on the first run. The key is the project directory, so a
|
|
90
|
+
repository with several projects gets one comment per project and never a
|
|
91
|
+
pile of stale ones. Updating comments needs `pull-requests: write`; on a
|
|
92
|
+
fork's pull request or without that permission the API answers 403 or 404 and
|
|
93
|
+
the step logs a notice and exits 0, leaving the diff in the job log. The same
|
|
94
|
+
happens when the base commit is not reachable or its YAML does not load with
|
|
95
|
+
the head runtime. The diff is generic: only the action knows about GitHub.
|
|
96
|
+
|
|
97
|
+
## Custom compliance rules
|
|
98
|
+
|
|
99
|
+
Write a rules module as [compliance](COMPLIANCE.md#writing-custom-rules)
|
|
100
|
+
describes and keep it outside the audited project, because it runs as trusted
|
|
101
|
+
host code. In a repository with the project at the root, put it in a sibling
|
|
102
|
+
directory and pass the absolute path:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- uses: jimhoyd-com/urlcode/action@main
|
|
106
|
+
with:
|
|
107
|
+
expect-routes: 25
|
|
108
|
+
compliance: strict
|
|
109
|
+
compliance-rules: ${{ github.workspace }}/ci/rules.mjs
|
|
110
|
+
compliance-warn: true
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`ci/rules.mjs` is inside the checkout but not inside the project only when
|
|
114
|
+
`project` is a subdirectory; with `project: .` keep the rules in a second
|
|
115
|
+
checkout or under `${{ runner.temp }}`. `compliance-rules` alone implies
|
|
116
|
+
`compliance: baseline`; `compliance: none` without rules skips the compliance
|
|
117
|
+
section entirely.
|
|
118
|
+
|
|
119
|
+
## Exit codes
|
|
120
|
+
|
|
121
|
+
The job fails when any of validate, test or audit exits nonzero; the
|
|
122
|
+
[audit exit codes](COMPLIANCE.md#exit-codes) apply unchanged. The route diff
|
|
123
|
+
and the comment never fail the job. A failing install (missing `@jimhoyd/urlcode`
|
|
124
|
+
dependency, unavailable `runtime` spec) fails the job before any check runs.
|
|
125
|
+
|
|
126
|
+
## The same checks locally
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
urlcode validate --project .
|
|
130
|
+
urlcode test --project .
|
|
131
|
+
urlcode audit --project . --expect-routes 2 --compliance baseline
|
|
132
|
+
git stash && urlcode routes --project . > /tmp/base.json && git stash pop
|
|
133
|
+
urlcode routes --project . --compare /tmp/base.json --format markdown
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or `make validate`, `make test` and `make audit ARGS='--expect-routes 2'`
|
|
137
|
+
from the starter Makefile. The runtime repository exercises the action on
|
|
138
|
+
every pull request against `examples/cookbook` (`.github/workflows/ci.yml`,
|
|
139
|
+
job `action`) with the packed tarball as `runtime`, and `test/action.test.ts`
|
|
140
|
+
checks that `action.yml` is a composite action with the inputs above and that
|
|
141
|
+
every third-party action it or the starter workflow uses is pinned to a
|
|
142
|
+
commit.
|
package/docs/CLOUDFLARE.md
CHANGED
|
@@ -21,8 +21,7 @@ route pattern and the reason named:
|
|
|
21
21
|
|
|
22
22
|
| Handler | Why it is refused |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| `function`, `middleware` | need worker threads and the QuickJS WASM engine |
|
|
25
|
-
| `link` | needs a durable writable store the platform does not provide here |
|
|
24
|
+
| `function`, `middleware` | need the self-hosted Node lifecycle, and worker threads and the QuickJS WASM engine when a route declares `sandbox: true` |
|
|
26
25
|
| `page`, `static`, `download` | need a platform static-asset binding, not an inline copy |
|
|
27
26
|
| `env`, `secrets` | would have to be baked into the artifact |
|
|
28
27
|
|