@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.1
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 +106 -0
- package/.claude/skills/urlcode-operations/SKILL.md +114 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +157 -230
- package/ROADMAP.md +85 -2
- package/SECURITY.md +8 -1
- package/dist/BUILD-MANIFEST.json +62 -32
- 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/bulk.js +37 -0
- package/dist/capabilities.js +208 -0
- package/dist/capability-query.js +72 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +154 -15
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +8 -8
- package/dist/compliance-rules/privacy.js +3 -3
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/conditions.js +88 -0
- package/dist/config.js +60 -4
- package/dist/context.js +156 -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 +132 -0
- package/dist/extensions.js +172 -0
- package/dist/function-sources.js +15 -3
- package/dist/index.js +37 -0
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/link-cli.js +1 -1
- package/dist/management-policy.js +0 -1
- package/dist/manifest.js +111 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +6 -0
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +29 -7
- 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 +5 -5
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +42 -2
- package/dist/runtime.js +95 -14
- package/dist/schema-query.js +62 -0
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -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/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +55 -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/strict.d.ts +0 -5
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +20 -2
- package/dist/types/context.d.ts +65 -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 +122 -0
- package/dist/types/function-sources.d.ts +5 -0
- package/dist/types/index.d.ts +33 -0
- 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/link-cli.d.ts +2 -1
- package/dist/types/management-policy.d.ts +0 -1
- package/dist/types/manifest.d.ts +81 -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 +2 -0
- 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 -1
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +1 -1
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/runtime.d.ts +11 -0
- package/dist/types/schema-query.d.ts +12 -0
- 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/types.d.ts +57 -0
- 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 +21 -2
- package/dist/typescript-authoring.js +104 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +130 -8
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +179 -0
- package/docs/CAPACITY.md +1 -1
- package/docs/CI.md +142 -0
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/DYNAMIC-LINKS.md +18 -518
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +226 -0
- package/docs/FRAMEWORK.md +182 -0
- package/docs/INSTALL.md +45 -7
- package/docs/INTERCHANGE.md +134 -0
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +2 -0
- package/docs/NEXT-PHASE-PLAN.md +90 -0
- package/docs/NEXT-STEPS.md +415 -0
- package/docs/OBSERVABILITY.md +4 -2
- package/docs/OPERATIONAL-PROOF.md +4 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +12 -309
- package/docs/PRERENDER.md +40 -0
- package/docs/PROJECT-DIRECTION.md +42 -0
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +82 -31
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +11 -9
- package/docs/RELEASE-SECURITY.md +27 -4
- package/docs/SECURITY-AUDIT.md +1 -1
- package/docs/SPECIFICATION.md +95 -8
- package/docs/SPIKE-BUSINESS-SUITE.md +1013 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +1 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +199 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/TOOLING.md +291 -0
- package/docs/TYPESCRIPT-AUTHORING.md +67 -0
- package/docs/TYPESCRIPT.md +1 -1
- package/docs/USABILITY-REVIEW.md +123 -0
- package/docs/YAML-GUIDE.md +18 -479
- package/docs/YAML-REFERENCE.md +127 -16
- package/docs/links/cli.md +110 -0
- package/docs/links/limits.md +175 -0
- package/docs/links/management-api.md +80 -0
- package/docs/links/pools.md +75 -0
- package/docs/links/setup.md +135 -0
- package/docs/policies/agents.md +1 -1
- 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/links.md +30 -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 +19 -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 +16 -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/live-links/example.yaml +21 -0
- package/examples/monitoring/example.yaml +8 -0
- 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 +2709 -0
- package/llms.txt +48 -19
- package/package.json +29 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +106 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +114 -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 +19 -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 +16 -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 +7 -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 +16 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +26 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +16 -0
- package/schemas/recipe.schema.json +138 -0
- package/schemas/urlcode.schema.json +656 -80
- package/skills/urlcode/SKILL.md +98 -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/docs/EGRESS.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Bounded proxy and webhook transport
|
|
2
|
+
|
|
3
|
+
Outbound requests use a host-owned HTTPS transport. Guests do not receive a fetch
|
|
4
|
+
API. Project declarations express behavior; an external operator policy, pinned
|
|
5
|
+
to the complete project revision, grants exact HTTPS origins separately to proxy
|
|
6
|
+
and signal operations. A configured destination never grants its own authority.
|
|
7
|
+
Binding injection uses existing explicitly granted secrets, resolved before the
|
|
8
|
+
host transport receives a literal header value. Never log those values.
|
|
9
|
+
|
|
10
|
+
The transport resolves the destination for each request, refuses any DNS response
|
|
11
|
+
containing a nonpublic address, and pins its connection lookup to one validated
|
|
12
|
+
answer. HTTPS certificate and hostname verification remain enabled. There is no
|
|
13
|
+
connection pool, fallback address, redirect following, or automatic retry. Private,
|
|
14
|
+
loopback, link-local, documentation, multicast, IPv4-mapped IPv6 and transition
|
|
15
|
+
addresses are refused. IPv6 filtering is deliberately conservative: 2001::/16,
|
|
16
|
+
2002::/16 and 3fff::/16 are refused along with addresses outside ordinary global
|
|
17
|
+
unicast. Network-level egress controls remain useful defense in depth.
|
|
18
|
+
|
|
19
|
+
Default limits per client are 16 simultaneous requests, five seconds including
|
|
20
|
+
DNS, 1 MiB request and response bodies and 16 KiB headers. Excess concurrency is
|
|
21
|
+
refused immediately. Requests use bounded buffering rather than streaming. Abort
|
|
22
|
+
and shutdown destroy active HTTP requests; unresolved system DNS operations may
|
|
23
|
+
finish in the background but cannot open a socket after cancellation. Those DNS
|
|
24
|
+
operations retain a separate concurrency slot until they settle, so repeatedly
|
|
25
|
+
timing out DNS cannot create an unbounded underlying lookup backlog. A shared
|
|
26
|
+
64-operation DNS cap also spans all client instances and runtime reloads in the
|
|
27
|
+
host Node isolate; closing a client does not release an unresolved DNS slot. Closing a
|
|
28
|
+
client refuses future requests and drains its bounded outstanding promises.
|
|
29
|
+
Errors carry only a fixed category, never a destination, request, header or secret.
|
|
30
|
+
|
|
31
|
+
Proxy destinations have a literal HTTPS authority. Path placeholders are encoded
|
|
32
|
+
as individual components; dot-segment values are refused. Query names and incoming
|
|
33
|
+
and outgoing headers require explicit selection. Host, framing, hop-by-hop and
|
|
34
|
+
proxy-authorization headers cannot be supplied. Ambient authorization, cookies,
|
|
35
|
+
forwarding metadata (the entire `x-forwarded-*` family) and Set-Cookie cannot be selected. Explicit host-resolved
|
|
36
|
+
Authorization injection is supported. Set-Cookie forwarding is deliberately
|
|
37
|
+
unsupported because multiple cookie fields cannot safely be represented by the
|
|
38
|
+
portable scalar header contract. Upstream redirects are returned as responses;
|
|
39
|
+
Location is forwarded only if explicitly selected and is never fetched. Headers
|
|
40
|
+
nominated by an incoming or upstream Connection field are removed even if selected.
|
|
41
|
+
Header names are normalized to lowercase; array-valued upstream fields are omitted.
|
|
42
|
+
An encoded request body requires explicitly selecting its Content-Encoding
|
|
43
|
+
header; literal header injection cannot change or replace that coding. Bodies
|
|
44
|
+
remain raw bytes, including content encoding; select Content-Encoding when
|
|
45
|
+
forwarding compressed responses. No transparent decompression occurs.
|
|
46
|
+
|
|
47
|
+
Webhook signals are best effort. The broker schedules work after the caller's
|
|
48
|
+
synchronous emit operation, with eight concurrent deliveries by default and no
|
|
49
|
+
queue. Saturation and closed brokers drop new events. Each event contains only
|
|
50
|
+
version, declared route pattern, status and method: never the actual request URL,
|
|
51
|
+
parameters, body, client identity, incoming headers or bindings. The webhook
|
|
52
|
+
receives a POST with JSON. Only 2xx responses count as delivered; all other status
|
|
53
|
+
codes and transport failures count as failed. Delivery order is unspecified,
|
|
54
|
+
there are no retries or durability guarantees, and shutdown aborts outstanding
|
|
55
|
+
work and awaits settlement. Counter snapshots report accepted, delivered, failed
|
|
56
|
+
and dropped events without including destination data. Applications must not use
|
|
57
|
+
these signals for guaranteed billing, audit retention or job execution.
|
|
58
|
+
|
|
59
|
+
The transport is a new security-sensitive host bridge. Unit tests and CI are not
|
|
60
|
+
an independent assessment or proof of hostile multi-tenant readiness. Public
|
|
61
|
+
provider network deployments require separate operator-owned fixtures and evidence.
|
|
62
|
+
|
|
63
|
+
## Project declarations and external grants
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
version: "1"
|
|
67
|
+
routes:
|
|
68
|
+
/items/{id}:
|
|
69
|
+
proxy:
|
|
70
|
+
url: https://api.example.com/items/{id}
|
|
71
|
+
query: [page]
|
|
72
|
+
requestHeaders: [accept]
|
|
73
|
+
responseHeaders: [content-type, content-encoding]
|
|
74
|
+
headers:
|
|
75
|
+
authorization: {secret: API_TOKEN}
|
|
76
|
+
parameters:
|
|
77
|
+
- {name: id, in: path, required: true, schema: {type: string}}
|
|
78
|
+
secrets:
|
|
79
|
+
API_TOKEN: {secret: UPSTREAM_AUTHORIZATION}
|
|
80
|
+
signals:
|
|
81
|
+
- url: https://hooks.example.com/events
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
An injected secret is the complete header value (for example an operator-provided
|
|
85
|
+
Bearer value); interpolation and ambient credentials are not supported. Each
|
|
86
|
+
proxy placeholder must name a declared string path parameter. Proxy routes refuse
|
|
87
|
+
middleware in this initial implementation: middleware cannot safely authorize an
|
|
88
|
+
already materialized upstream request. Use host request policies before egress.
|
|
89
|
+
Proxy routes require cache disabled or `no-store`; explicit cache declarations
|
|
90
|
+
cannot override this. Responses always remove CDN cache directives and receive
|
|
91
|
+
`Cache-Control: no-store`. Compressed upstream bytes are refused if their
|
|
92
|
+
Content-Encoding field was not explicitly selected; it cannot be overridden by
|
|
93
|
+
route response headers.
|
|
94
|
+
|
|
95
|
+
The external version-1 policy has the existing `projectSha256` and route grants:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{"version":1,"projectSha256":"<reviewed revision hash>","routes":{"/items/{id}":{"secrets":["UPSTREAM_AUTHORIZATION"],"egress":{"proxy":["https://api.example.com"],"signals":["https://hooks.example.com"]}}}}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Use `permissions` to generate requested grants for review. Exact origin strings
|
|
102
|
+
omit a trailing slash and the default 443 port. Nondefault ports are explicit.
|
|
103
|
+
Proxy and signal purposes are independent even when they share an origin. Every
|
|
104
|
+
route's grant is checked against the current revision before credentials, assets,
|
|
105
|
+
workers or network clients are activated. Each runtime shares one bounded proxy
|
|
106
|
+
client and one bounded signal client, with at most 64 origins per purpose.
|
|
107
|
+
|
|
108
|
+
Signals emit when route execution and response policies produce a result,
|
|
109
|
+
including guest middleware responses and returned error status codes. Thrown
|
|
110
|
+
handler errors and host request-policy/plugin short circuits (including cache
|
|
111
|
+
hits) do not emit. HEAD requests and generated readiness probes do not emit.
|
|
112
|
+
The response never waits for webhook delivery. Counter events contain only
|
|
113
|
+
accepted/delivered/failed/dropped outcomes and counts and are exposed through
|
|
114
|
+
runtime metrics and the Prometheus `signals_total` series. A closed runtime stops
|
|
115
|
+
new requests, aborts egress and drains bounded pending work. Client disconnects
|
|
116
|
+
are not currently propagated into the runtime transport; the five-second deadline
|
|
117
|
+
still applies. All non-self-hosted targets currently refuse proxy and signals.
|
|
118
|
+
See the [executable example](../examples/egress/README.md).
|
|
119
|
+
|
|
120
|
+
Revision hashes also include declared project policies, profiles and site
|
|
121
|
+
configuration. Changing an inherited pre-egress restriction invalidates grants
|
|
122
|
+
just like changing a route or function source. Projects with these declarations
|
|
123
|
+
must regenerate and review their operator policy after upgrading to this hash
|
|
124
|
+
coverage; an old grant is intentionally refused rather than silently retaining
|
|
125
|
+
network authority under changed behavior.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Operator-installed extensions
|
|
2
|
+
|
|
3
|
+
Extensions are trusted operator modules, separate from application WASM. Auth
|
|
4
|
+
and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
|
|
5
|
+
supplies only the generic integration contract. No project file can import a host
|
|
6
|
+
extension or choose its npm package.
|
|
7
|
+
|
|
8
|
+
A project declares versioned configuration and exclusive route mounts:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
version: "1"
|
|
12
|
+
extensions:
|
|
13
|
+
auth:
|
|
14
|
+
version: "1"
|
|
15
|
+
config: {}
|
|
16
|
+
routes:
|
|
17
|
+
/auth/*:
|
|
18
|
+
extension: auth
|
|
19
|
+
methods: [GET, HEAD, POST]
|
|
20
|
+
/private:
|
|
21
|
+
respond: {text: Private}
|
|
22
|
+
policies:
|
|
23
|
+
extensions:
|
|
24
|
+
auth: {signedIn: true}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Protecting a route: the `auth` short form
|
|
28
|
+
|
|
29
|
+
When the project declares `extensions.auth`, a route may say `auth` instead of
|
|
30
|
+
spelling out `policies.extensions.auth`. This is the preferred way to protect a
|
|
31
|
+
route:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
routes:
|
|
35
|
+
/account:
|
|
36
|
+
respond: {text: Account}
|
|
37
|
+
auth: {role: member} # or `auth: true` for any signed-in principal
|
|
38
|
+
/docs:
|
|
39
|
+
respond: {text: Docs}
|
|
40
|
+
auth: {required: false} # documents intent; emits no requirement
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The compiler expands the short form before anything else reads the project:
|
|
44
|
+
`auth: true` becomes `policies.extensions.auth: {}` and an object becomes the
|
|
45
|
+
same object minus `required`. The long form stays the canonical representation,
|
|
46
|
+
so `routes`, `audit` and `explain` show the expansion, the extension revision
|
|
47
|
+
hash covers it, and the installed auth extension validates the expanded
|
|
48
|
+
requirement with its own policy schema. The keys other than `required` are
|
|
49
|
+
exactly that schema's keys (`role`, `permission`, `verified`,
|
|
50
|
+
`freshWithinSeconds`, `onDeny`); the runtime adds nothing of its own. Loading
|
|
51
|
+
fails, naming the route, when `auth` appears without an `extensions.auth`
|
|
52
|
+
declaration, next to `policies.extensions.auth`, or next to
|
|
53
|
+
`policies.extensions: false`.
|
|
54
|
+
|
|
55
|
+
The same shape is reserved for the cache policy: a future `cache: {strategy,
|
|
56
|
+
maxAge}` route key may expand to `policies.cache` in the same pass. It is not
|
|
57
|
+
implemented; declare `policies.cache` today (see [policies](POLICIES.md)).
|
|
58
|
+
|
|
59
|
+
The configuration and requirement objects above are validated by the installed
|
|
60
|
+
extension's schemas. They are examples of extension-owned fields, not built-in
|
|
61
|
+
authentication behavior. See the executable generic fixture in
|
|
62
|
+
[examples/extensions](../examples/extensions). Included files can declare
|
|
63
|
+
extensions; duplicate names fail rather than silently override one another.
|
|
64
|
+
|
|
65
|
+
The operator passes `extensions: RuntimeExtension[]` to `createRuntime`,
|
|
66
|
+
`startServer`, or the AWS/Vercel adapter. Types and
|
|
67
|
+
`inspectExtensionRevision(project)` are exported from
|
|
68
|
+
`@jimhoyd/urlcode/extensions`. Inspection does not grant access: review the
|
|
69
|
+
project and place the exact returned SHA-256 in each registration's
|
|
70
|
+
`projectSha256`. YAML extension configuration, policies and routes participate
|
|
71
|
+
in the revision. Changing them requires an explicit operator reapproval.
|
|
72
|
+
|
|
73
|
+
Registrations provide a name, contract version, target list, JSON configuration
|
|
74
|
+
schema, optional policy schema and activation factory. Activation receives the
|
|
75
|
+
canonical operator origin, target, revision and mount bases. Its instance handles
|
|
76
|
+
bounded requests and, when used in policies, authorizes requests. Missing
|
|
77
|
+
registrations, stale grants, invalid configuration and unsupported targets fail
|
|
78
|
+
activation. Multiple mounts cannot overlap other declared routes.
|
|
79
|
+
|
|
80
|
+
For extension-protected routes, agents/throttle run before authorization and
|
|
81
|
+
cache access happens only after authorization. Extension routes and protected
|
|
82
|
+
routes reject cache strategies other than no-store; every resulting response is
|
|
83
|
+
forced to no-store after host response hooks. Compression is disabled on these
|
|
84
|
+
responses.
|
|
85
|
+
|
|
86
|
+
One exception exists for content-hashed assets. A registration may declare
|
|
87
|
+
`immutableAssets: {prefix: '/static'}`, a normalized literal path under each
|
|
88
|
+
of its mounts (no `.` or `..` segments, no trailing slash). The runtime then
|
|
89
|
+
answers `Cache-Control: public, max-age=31536000, immutable` instead of
|
|
90
|
+
no-store only when every condition holds: the request path lies under
|
|
91
|
+
`<mount><prefix>/`, the method is GET or HEAD, the status is 200 or 304, the
|
|
92
|
+
response carries exactly one strong ETag, sets no Set-Cookie, and does not
|
|
93
|
+
vary on Cookie, Authorization or `*`. A stricter Cache-Control the extension
|
|
94
|
+
set (no-store, no-cache, private or a shorter max-age) is preserved; other CDN
|
|
95
|
+
cache headers are still stripped and compression stays disabled. Anything
|
|
96
|
+
that fails a condition, including a cookie added by a later response hook,
|
|
97
|
+
stays no-store. The extension owns the content-hashed filename: a file under
|
|
98
|
+
the prefix must change its name when its bytes change, because clients never
|
|
99
|
+
revalidate it. The prefix belongs to the operator registration, not to the
|
|
100
|
+
pinned project revision. The runtime withholds Cookie and Authorization plus any declared
|
|
101
|
+
credential headers from all application guest requests and mapped parameters.
|
|
102
|
+
This does not isolate browser JavaScript running on the same origin: application
|
|
103
|
+
HTML/JS on an authentication origin must be trusted by that site's operator.
|
|
104
|
+
|
|
105
|
+
Cloudflare refuses extensions until its artifact format supports their execution.
|
|
106
|
+
Node adapter conformance is not a live-provider deployment claim.
|
|
107
|
+
|
|
108
|
+
## Discovering schemas
|
|
109
|
+
|
|
110
|
+
Each registration carries the JSON Schemas that validate its `config` block and
|
|
111
|
+
its per-route policy requirements. `urlcode extensions` prints them together with
|
|
112
|
+
the project's own declarations so an author can see what a mount accepts:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--json]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For every registration in the host file it reports the name, contract version,
|
|
119
|
+
targets, credential headers, configuration schema, policy schema (if any),
|
|
120
|
+
whether the project declares it, whether its `projectSha256` matches the current
|
|
121
|
+
revision, the routes that mount it and the routes whose policies require it.
|
|
122
|
+
Declared names the host does not register are listed as unregistered. The command
|
|
123
|
+
executes the trusted host module exactly as `validate` does, including its
|
|
124
|
+
absolute-path and outside-project rules, and calls `close` afterwards; it never
|
|
125
|
+
activates an extension and grants nothing. Without `--host-file` it lists only
|
|
126
|
+
the names the project declares and notes that schemas need the host file.
|
|
127
|
+
|
|
128
|
+
The same report is available as `inspectExtensions({project, hostFile?})` from
|
|
129
|
+
the package root and, for assistants, as the MCP tool `get_extensions`, which the
|
|
130
|
+
server advertises only when the operator started `urlcode mcp` with
|
|
131
|
+
`--host-file`. No tool argument can name a host file. See [TOOLING.md](TOOLING.md).
|
|
132
|
+
|
|
133
|
+
## CLI host binding
|
|
134
|
+
|
|
135
|
+
Use an explicitly named operator ES module outside the application directory:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
urlcode serve --project ./site --origin https://site.example \
|
|
139
|
+
--host-file /absolute/operator/host.mjs
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The module default-exports `{extensions, plugins?, close?}`. It may import installed
|
|
143
|
+
operator packages, open their stores and read operator secrets. `close` releases
|
|
144
|
+
shared services when the CLI command finishes or the server shuts down. A runtime
|
|
145
|
+
reload closes extension instances but does not close caller-owned services. Host
|
|
146
|
+
modules are not watched or automatically rediscovered. Restart to update them.
|
|
147
|
+
|
|
148
|
+
The same explicit option is supported by dev, validate, test, routes, audit,
|
|
149
|
+
benchmark, extensions and mcp. These commands execute trusted host activation and may access its
|
|
150
|
+
store; read-only project inspection commands never implicitly load a host file.
|
|
151
|
+
Host-file paths must be absolute `.mjs`/`.js` files whose real path lies outside
|
|
152
|
+
the project, including after symlink resolution. This is an operator-code trust
|
|
153
|
+
boundary, not a JavaScript sandbox or an independent security review.
|
|
154
|
+
|
|
155
|
+
## Scaffolding with `init --with`
|
|
156
|
+
|
|
157
|
+
`urlcode init <directory> --with auth,admin` produces the layered site the
|
|
158
|
+
[framework page](FRAMEWORK.md#the-composition-contract) describes in one
|
|
159
|
+
command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
|
|
160
|
+
and each extension's own operator files. Core never bundles or imports the
|
|
161
|
+
extension packages at build time; at run time it resolves
|
|
162
|
+
`@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
|
|
163
|
+
the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
|
|
164
|
+
directory, from npm where the packages are published as `0.1.0-alpha.1`
|
|
165
|
+
prereleases, is the normal path and what makes `--with auth` work), imports
|
|
166
|
+
the package and calls its
|
|
167
|
+
`scaffold` export with this request:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
interface ScaffoldRequest {
|
|
171
|
+
directory: string; // absolute site directory; result file paths are relative to it
|
|
172
|
+
project: string; // absolute route project, <directory>/app (holds urlcode.yaml)
|
|
173
|
+
hostFile: string; // absolute combined host module, <directory>/host.mjs
|
|
174
|
+
names: readonly string[]; // every name in --with order, including this one
|
|
175
|
+
}
|
|
176
|
+
interface ScaffoldFile { path: string; content: string | Uint8Array; mode?: number }
|
|
177
|
+
interface ScaffoldResult {
|
|
178
|
+
name: string; // must equal the requested name
|
|
179
|
+
extensions: Record<string, unknown>; // merged into the project's top-level extensions
|
|
180
|
+
routes: Record<string, unknown>; // merged into app/routes/extensions.yaml
|
|
181
|
+
hostImports: string[]; hostSetup: string[]; hostEntries: string[]; hostClose?: string[];
|
|
182
|
+
files: ScaffoldFile[]; // written relative to directory with their modes
|
|
183
|
+
readme: string; nextSteps: string[]; // README section and numbered steps
|
|
184
|
+
env?: Record<string, string>; // environment variables the host reads
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`scaffold` writes nothing; it returns fragments and may generate key material
|
|
189
|
+
in memory (core zeroes `Uint8Array` contents after writing or on failure). The
|
|
190
|
+
types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
|
|
191
|
+
against them.
|
|
192
|
+
|
|
193
|
+
Assembly rules, in `--with` order:
|
|
194
|
+
|
|
195
|
+
- Every package is resolved and every `scaffold` is called before anything is
|
|
196
|
+
written. A name that is not installed refuses with the `npm install` command;
|
|
197
|
+
a package without a `scaffold` export refuses and names the package; an error
|
|
198
|
+
thrown by a `scaffold` (for example admin without auth in the same `--with`)
|
|
199
|
+
is reported as that package's refusal. No directory is left behind.
|
|
200
|
+
- `extensions` fragments are declared in `app/urlcode.yaml`; `routes`
|
|
201
|
+
fragments are written to `app/routes/extensions.yaml`, appended to the
|
|
202
|
+
starter's `includes`, so the starter's own routes load first. A route or
|
|
203
|
+
extension key produced twice, or one the starter already declares, is refused
|
|
204
|
+
naming both sources.
|
|
205
|
+
- `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
|
|
206
|
+
`extensions` array of every `hostEntries` item, then `close()` running the
|
|
207
|
+
`hostClose` statements in reverse `--with` order so later entries release
|
|
208
|
+
before what they built on. Setup lines share one module scope: admin's entry
|
|
209
|
+
references the `service`, `csrfKey` and `projectSha256` identifiers that
|
|
210
|
+
auth's setup defines, which is why `names` carries the full list.
|
|
211
|
+
- `files` are created exclusively (`wx`) with their `mode` (default `0644`),
|
|
212
|
+
must stay inside the site directory and outside `app/`, and never pass
|
|
213
|
+
through a symlink. Nothing generated is ever overwritten; an existing
|
|
214
|
+
destination refuses like plain `init`.
|
|
215
|
+
- `README.md` holds the starter's README as a section, then each result's
|
|
216
|
+
`readme` under `## Extension: <name>`, the merged numbered `nextSteps`, the
|
|
217
|
+
merged `env` table and the project revision. The command prints that
|
|
218
|
+
revision (`inspectExtensionRevision` of `app/`) with the instruction to
|
|
219
|
+
review the project and pin it explicitly; the host is generated to require
|
|
220
|
+
the pin, never to compute it.
|
|
221
|
+
|
|
222
|
+
Serving the result is the usual explicit host binding:
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
|
|
226
|
+
```
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# The URLCode framework
|
|
2
|
+
|
|
3
|
+
One page for people and AI agents. It says what the four packages are, how a
|
|
4
|
+
project grows from a handful of redirects into an application with accounts
|
|
5
|
+
and an administration console, and which facts an agent must not guess. Every
|
|
6
|
+
claim here is implemented in the linked repository; nothing is roadmap.
|
|
7
|
+
|
|
8
|
+
## Four packages, one project shape
|
|
9
|
+
|
|
10
|
+
| Package | Repository | What it adds | How a project declares it |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, isolated functions and middleware, pages and assets, live short links, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
|
|
13
|
+
| `@jimhoyd/urlcode-ui` | [urlcode-ui](https://github.com/jimhoyd-com/urlcode-ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
|
|
14
|
+
| `@jimhoyd/urlcode-auth` | [urlcode-auth](https://github.com/jimhoyd-com/urlcode-auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
|
|
15
|
+
| `@jimhoyd/urlcode-admin` | [urlcode-admin](https://github.com/jimhoyd-com/urlcode-admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
|
|
16
|
+
|
|
17
|
+
The core is Apache-2.0 and released. The three extension packages are
|
|
18
|
+
Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.1`,
|
|
19
|
+
`@jimhoyd/urlcode-auth@0.1.0-alpha.1`, `@jimhoyd/urlcode-admin@0.1.0-alpha.1`,
|
|
20
|
+
on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
|
|
21
|
+
endorsement: the source is complete, but independent review, deployment
|
|
22
|
+
evidence and an accessibility assessment are still pending
|
|
23
|
+
([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
|
|
24
|
+
files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
|
|
25
|
+
[admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
|
|
26
|
+
[ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
|
|
27
|
+
|
|
28
|
+
## The ladder
|
|
29
|
+
|
|
30
|
+
A project climbs these rungs by adding YAML, never by rewriting what it has.
|
|
31
|
+
Each rung's YAML is valid on every rung above it.
|
|
32
|
+
|
|
33
|
+
1. **Redirects.** A `urlcode.yaml` with `redirect` routes. No code, no database,
|
|
34
|
+
runs anywhere, including Vercel, AWS Lambda and Cloudflare Workers.
|
|
35
|
+
Thousands of rows import from CSV or provider files with `bulk-import`.
|
|
36
|
+
2. **Responses, pages and files.** `respond`, `page`, `static` and `download`
|
|
37
|
+
handlers, `site` conventions (robots, sitemap, favicon, security.txt,
|
|
38
|
+
llms.txt) and `policies` (throttle, agents, security headers, compression,
|
|
39
|
+
cache). Still no code.
|
|
40
|
+
3. **Functions and middleware.** `function` routes and ordered `middleware`
|
|
41
|
+
in isolated JavaScript (QuickJS inside WebAssembly, fresh heap per call, no
|
|
42
|
+
Node, filesystem or network). Secrets reach a function only through an
|
|
43
|
+
operator grant pinned to the project revision.
|
|
44
|
+
4. **Live short links.** A `link` route backed by an optional SQLite store,
|
|
45
|
+
with CLI and a private management API. Records change without reloads.
|
|
46
|
+
5. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
|
|
47
|
+
page and protected routes. The operator installs it in a host file outside
|
|
48
|
+
the project; YAML only declares the mount and configuration.
|
|
49
|
+
6. **Administration.** The `admin` extension on the same service: manage the
|
|
50
|
+
people who signed up, their sessions and roles, review the audit trail.
|
|
51
|
+
7. **Your own look.** A shared `presentation` (catalogue and theme variables)
|
|
52
|
+
restyles auth and admin together; the `ui` extension adds the template kit,
|
|
53
|
+
project copy, template and stylesheet overrides for kit-rendered pages.
|
|
54
|
+
|
|
55
|
+
Rungs 1 to 4 need only the core package. Rungs 5 to 7 need the extension
|
|
56
|
+
packages, installed from npm as `0.1.0-alpha.1` prereleases, and a Node host
|
|
57
|
+
with a patched SQLite build; see each repository's README for the exact
|
|
58
|
+
requirement.
|
|
59
|
+
|
|
60
|
+
## The composition contract
|
|
61
|
+
|
|
62
|
+
An extended project starts with the packages and one command:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
|
|
66
|
+
urlcode init my-site --with auth,admin
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Installing from npm is the normal path; the extension repositories'
|
|
70
|
+
`pack-sources.mjs` still builds local tarballs from a reviewed checkout for
|
|
71
|
+
operators who install only source they have read. Three files make an extended
|
|
72
|
+
project. Nothing else is discovered by convention.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
site/
|
|
76
|
+
urlcode.yaml the project: routes, extensions, policies (Git-owned, untrusted content)
|
|
77
|
+
functions/, public/ guest code and assets referenced from the YAML
|
|
78
|
+
operator/
|
|
79
|
+
host.mjs trusted operator code: default-exports { extensions, plugins?, close? }
|
|
80
|
+
operator-service.mjs opens the auth store, keys and senders; imported by host.mjs
|
|
81
|
+
data/ private: auth.sqlite, encryption key, CSRF key
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The project declares logical extensions and exclusive mounts:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
version: "1"
|
|
88
|
+
extensions:
|
|
89
|
+
ui: { version: "1", config: { theme: { name: Acme, colors: { primary: "24 95% 53%" } } } }
|
|
90
|
+
auth: { version: "1", config: { registration: "off" } }
|
|
91
|
+
admin: { version: "1", config: {} }
|
|
92
|
+
routes:
|
|
93
|
+
/assets/ui/*: { extension: ui, methods: [GET, HEAD] }
|
|
94
|
+
/account/*: { extension: auth, methods: [GET, HEAD, POST] }
|
|
95
|
+
/admin/*: { extension: admin, methods: [GET, HEAD, POST] }
|
|
96
|
+
/private:
|
|
97
|
+
respond: { text: Signed in }
|
|
98
|
+
policies:
|
|
99
|
+
extensions:
|
|
100
|
+
auth: {}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The operator host wires the packages, once, in code the project cannot reach:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
|
|
107
|
+
import { authExtension, createPresentation, englishCatalogue } from '@jimhoyd/urlcode-auth';
|
|
108
|
+
import { adminExtension } from '@jimhoyd/urlcode-admin';
|
|
109
|
+
import { service, csrfKey, projectSha256 } from './operator-service.mjs';
|
|
110
|
+
|
|
111
|
+
const ui = createUiExtension({ projectSha256, projectRoot: '/absolute/site', sources: [englishCatalogue] });
|
|
112
|
+
const presentation = createPresentation({ theme: { '--ui-accent': '#0645ad' } });
|
|
113
|
+
export default {
|
|
114
|
+
extensions: [
|
|
115
|
+
ui.registration,
|
|
116
|
+
authExtension({ service, csrfKey, projectSha256, presentation }),
|
|
117
|
+
adminExtension({ service, csrfKey, projectSha256, authMount: '/account', presentation }),
|
|
118
|
+
],
|
|
119
|
+
async close() { await service.close(); },
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Auth and admin pages currently render through the shared primitives and a
|
|
124
|
+
`presentation` (catalogue and `--ui-*` theme variables). The `ui` extension
|
|
125
|
+
serves the template kit and its assets for pages an extension renders through
|
|
126
|
+
`ui.kit`; moving the auth and admin screens onto the kit is a listed remaining
|
|
127
|
+
item in the ui repository, so today one `presentation` restyles them and the
|
|
128
|
+
`ui` block is optional.
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`urlcode init <dir> --with auth,admin` writes this layout in one step: it
|
|
135
|
+
resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
|
|
136
|
+
calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
|
|
137
|
+
one `host.mjs` and one `README.md`, refusing before writing anything when a
|
|
138
|
+
package is missing or two fragments collide (the contract is documented under
|
|
139
|
+
[scaffolding](EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
|
|
140
|
+
and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
|
|
141
|
+
administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
|
|
142
|
+
the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
|
|
143
|
+
or mounts changes the revision and needs an explicit operator reapproval.
|
|
144
|
+
|
|
145
|
+
## Rules an agent must follow
|
|
146
|
+
|
|
147
|
+
These are the facts that keep generated projects valid. The full matrix is in
|
|
148
|
+
[AI authoring](AI-AUTHORING.md); this is the short list.
|
|
149
|
+
|
|
150
|
+
- **YAML never names code, packages, databases or credentials.** Extensions are
|
|
151
|
+
logical names; the host file chooses the implementation. There is no
|
|
152
|
+
`--extension` flag, no `import` in YAML, no interpolation.
|
|
153
|
+
- **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
|
|
154
|
+
`function`, `link`, `proxy`, `conditional` or `extension`, plus optional
|
|
155
|
+
`middleware`. Paths are exact or single-segment `{param}`; `/*` only on
|
|
156
|
+
`static` and `extension` mounts. No regex.
|
|
157
|
+
- **Guest code is sandboxed.** Functions see a text/JSON `Request`/`Response`
|
|
158
|
+
subset, validated `args` and granted `env`. No `fetch`, Node, filesystem or
|
|
159
|
+
timers. Do not write code that needs them; declare a `proxy` or a binding.
|
|
160
|
+
- **Authentication is host processing.** Do not build login forms, session
|
|
161
|
+
cookies or password checks in functions. Declare `policies.extensions.auth`
|
|
162
|
+
on the route; the runtime withholds `Cookie` and `Authorization` from guests.
|
|
163
|
+
- **Everything is validated before it runs.** `urlcode validate --local`,
|
|
164
|
+
`urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
|
|
165
|
+
with the route named; nothing degrades silently.
|
|
166
|
+
- **Provider targets refuse what they cannot enforce.** Cloudflare runs
|
|
167
|
+
redirects and declared responses only. Serverless adapters refuse functions,
|
|
168
|
+
links, proxy, signals and extensions. Check
|
|
169
|
+
`urlcode capabilities --target NAME` before promising a deployment.
|
|
170
|
+
- **Report evidence, not hope.** The commands above are the evidence. Local
|
|
171
|
+
tests are not deployment, soak or independent security review.
|
|
172
|
+
|
|
173
|
+
## Where to read next
|
|
174
|
+
|
|
175
|
+
| Need | Read |
|
|
176
|
+
|---|---|
|
|
177
|
+
| Write or change routes | [YAML guide](YAML-GUIDE.md), [field reference](YAML-REFERENCE.md), [cookbook](../examples/cookbook/README.md) |
|
|
178
|
+
| Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode-auth#readme), [auth security](https://github.com/jimhoyd-com/urlcode-auth/blob/main/SECURITY.md) |
|
|
179
|
+
| Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
|
|
180
|
+
| Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode-ui#readme), [ui contract](https://github.com/jimhoyd-com/urlcode-ui/blob/main/CONTRACT.md) |
|
|
181
|
+
| Write an extension | [extensions](EXTENSIONS.md), [extension model review](SPIKE-EXTENSION-MODEL.md) |
|
|
182
|
+
| Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |
|
package/docs/INSTALL.md
CHANGED
|
@@ -14,26 +14,51 @@ everything except live links works on any supported build. See
|
|
|
14
14
|
## npm
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
npm install --global urlcode
|
|
17
|
+
npm install --global @jimhoyd/urlcode
|
|
18
18
|
urlcode --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Project-local, which is what an application repository should normally pin
|
|
21
|
+
Project-local, which is what an application repository should normally pin.
|
|
22
|
+
Which dependency list it belongs in depends on how the project uses URLCode:
|
|
22
23
|
|
|
23
24
|
```sh
|
|
24
|
-
|
|
25
|
+
# Using URLCode as a tool: validate, test and build in CI, never imported by
|
|
26
|
+
# the code that serves requests.
|
|
27
|
+
npm install --save-dev @jimhoyd/urlcode
|
|
25
28
|
npx urlcode validate
|
|
29
|
+
|
|
30
|
+
# Embedding the runtime (see TYPESCRIPT.md): the application imports
|
|
31
|
+
# @jimhoyd/urlcode at startup, so it must survive `npm ci --omit=dev`.
|
|
32
|
+
npm install --save @jimhoyd/urlcode
|
|
26
33
|
```
|
|
27
34
|
|
|
35
|
+
A devDependency is absent from a production install, so an application that
|
|
36
|
+
imports `createRuntime`, `startServer`, `prerenderPages` or any other
|
|
37
|
+
[embedding entry point](TYPESCRIPT.md) fails at startup on a missing module if it
|
|
38
|
+
is installed with `--save-dev`. An application should also pin an **exact**
|
|
39
|
+
version rather than a range: the compiled Cloudflare artifact format is tied to
|
|
40
|
+
the runtime version that reads it.
|
|
41
|
+
|
|
28
42
|
## Homebrew
|
|
29
43
|
|
|
30
44
|
```sh
|
|
31
45
|
brew tap jimhoyd-com/urlcode
|
|
46
|
+
brew trust jimhoyd-com/urlcode
|
|
32
47
|
brew install urlcode
|
|
33
48
|
```
|
|
34
49
|
|
|
50
|
+
Homebrew refuses to load a formula from a third-party tap until you trust it,
|
|
51
|
+
so without the middle line the install stops with `Refusing to load formula …
|
|
52
|
+
from untrusted tap`. Trusting a tap means agreeing to run code from this
|
|
53
|
+
repository, the same as with the install script; `brew trust --formula
|
|
54
|
+
jimhoyd-com/urlcode/urlcode` limits it to this one formula.
|
|
55
|
+
|
|
35
56
|
The tap's formula is generated from the published tarball for each release and
|
|
36
|
-
attached to the GitHub release as `urlcode.rb`.
|
|
57
|
+
attached to the GitHub release as `urlcode.rb`. Homebrew verifies the tarball's
|
|
58
|
+
SHA-256 against the formula before installing.
|
|
59
|
+
|
|
60
|
+
Verified on macOS (arm64) against the 0.3.0 tap: fetch verified, installed into
|
|
61
|
+
`/opt/homebrew/Cellar/urlcode/0.3.0`.
|
|
37
62
|
|
|
38
63
|
## Install script
|
|
39
64
|
|
|
@@ -45,7 +70,7 @@ It downloads the release tarball, verifies its SHA-256 against the release's
|
|
|
45
70
|
`SHA256SUMS`, and installs with npm. Options:
|
|
46
71
|
|
|
47
72
|
```sh
|
|
48
|
-
curl -fsSL .../install.sh | sh -s -- --version 0.
|
|
73
|
+
curl -fsSL .../install.sh | sh -s -- --version 0.3.0 --prefix "$HOME/.local"
|
|
49
74
|
```
|
|
50
75
|
|
|
51
76
|
`--prefix` avoids needing privileges for a global npm directory; add
|
|
@@ -54,8 +79,14 @@ for that moment: to inspect first, download it, read it, then run it.
|
|
|
54
79
|
|
|
55
80
|
## Container
|
|
56
81
|
|
|
82
|
+
No image is published yet: the release job's GHCR step is gated behind the
|
|
83
|
+
`PUBLISH_CONTAINER` repository variable and has not run, so there is nothing at
|
|
84
|
+
`ghcr.io/jimhoyd-com/urlcode` to pull. Build it from a release checkout:
|
|
85
|
+
|
|
57
86
|
```sh
|
|
58
|
-
|
|
87
|
+
git clone --branch v0.3.0 https://github.com/jimhoyd-com/urlcode.git
|
|
88
|
+
docker build -t urlcode:0.3.0 urlcode
|
|
89
|
+
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" urlcode:0.3.0 \
|
|
59
90
|
serve --project /project --host 0.0.0.0
|
|
60
91
|
```
|
|
61
92
|
|
|
@@ -74,13 +105,20 @@ make dev
|
|
|
74
105
|
A clone runs the TypeScript source directly (`node src/cli.ts`, Node 22.18+),
|
|
75
106
|
with no build step; see [local development](LOCAL-DEVELOPMENT.md).
|
|
76
107
|
|
|
108
|
+
Three Node versions appear around the project, and they are not a contradiction:
|
|
109
|
+
the installed package runs on Node 22.13 or newer (`engines`), running the
|
|
110
|
+
TypeScript source from a clone needs 22.18 or newer because it relies on Node's
|
|
111
|
+
built-in type stripping, the release workflow's npm trusted publishing needs
|
|
112
|
+
22.14 or newer, and the container image pins Node 26. Only the first number
|
|
113
|
+
constrains a deployment of the published tarball.
|
|
114
|
+
|
|
77
115
|
## Verify what you installed
|
|
78
116
|
|
|
79
117
|
Releases carry Sigstore provenance signed by the release workflow. Before
|
|
80
118
|
trusting a downloaded artifact:
|
|
81
119
|
|
|
82
120
|
```sh
|
|
83
|
-
gh attestation verify urlcode-0.
|
|
121
|
+
gh attestation verify jimhoyd-urlcode-0.3.0.tgz --repo jimhoyd-com/urlcode \
|
|
84
122
|
--signer-workflow jimhoyd-com/urlcode/.github/workflows/release.yml
|
|
85
123
|
```
|
|
86
124
|
|