@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/contact",
|
|
4
|
+
"method": "POST",
|
|
5
|
+
"headers": {
|
|
6
|
+
"content-type": "application/json"
|
|
7
|
+
},
|
|
8
|
+
"body": "{\"name\":\"Ada\",\"email\":\"ada@example.com\",\"message\":\"Please send the brochure.\"}",
|
|
9
|
+
"status": 202,
|
|
10
|
+
"expectBody": "{\"accepted\":true}",
|
|
11
|
+
"expectHeaders": {
|
|
12
|
+
"cache-control": "no-store"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "/contact",
|
|
17
|
+
"method": "POST",
|
|
18
|
+
"headers": {
|
|
19
|
+
"content-type": "application/json"
|
|
20
|
+
},
|
|
21
|
+
"body": "{\"name\":\"\",\"email\":\"not-an-address\",\"message\":\"short\"}",
|
|
22
|
+
"status": 422,
|
|
23
|
+
"expectBody": "{\"accepted\":false,\"errors\":[\"name: 1-100 characters\",\"email: a valid address\",\"message: 10-4000 characters\"]}"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "/contact",
|
|
27
|
+
"method": "POST",
|
|
28
|
+
"headers": {
|
|
29
|
+
"content-type": "application/json"
|
|
30
|
+
},
|
|
31
|
+
"body": "\"just a string\"",
|
|
32
|
+
"status": 422
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "/contact",
|
|
36
|
+
"method": "POST",
|
|
37
|
+
"headers": {
|
|
38
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
39
|
+
},
|
|
40
|
+
"body": "name=Ada",
|
|
41
|
+
"status": 415
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"path": "/contact",
|
|
45
|
+
"status": 405
|
|
46
|
+
}
|
|
47
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: "1"
|
|
2
|
+
routes:
|
|
3
|
+
/contact:
|
|
4
|
+
description: Validate a JSON contact message and notify an operator-granted hook after the response.
|
|
5
|
+
methods: [POST]
|
|
6
|
+
request:
|
|
7
|
+
body:
|
|
8
|
+
required: true
|
|
9
|
+
maxBytes: 16384
|
|
10
|
+
contentTypes: [application/json]
|
|
11
|
+
format: json
|
|
12
|
+
function:
|
|
13
|
+
source: functions/contact.mjs
|
|
14
|
+
signals:
|
|
15
|
+
- url: https://hooks.example.com/contact
|
|
16
|
+
response:
|
|
17
|
+
headers:
|
|
18
|
+
Cache-Control: no-store
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CORS JSON API
|
|
2
|
+
|
|
3
|
+
Run `urlcode validate --local --project .`, `urlcode test --project .` and
|
|
4
|
+
`urlcode audit --project . --expect-routes 1`.
|
|
5
|
+
|
|
6
|
+
`/api/items` returns JSON from a sandboxed function. `middleware/cors.mjs` runs
|
|
7
|
+
around it: it answers `OPTIONS` preflight itself with 204 and adds
|
|
8
|
+
`Access-Control-Allow-Origin` to responses for origins in its allowlist. Other
|
|
9
|
+
origins get `Vary: Origin` and no allow header, so browsers refuse them.
|
|
10
|
+
|
|
11
|
+
Edit `allowedOrigins` in the middleware and the function body. The route must
|
|
12
|
+
list `OPTIONS` in `methods`, or the runtime answers 405 before the middleware
|
|
13
|
+
runs. CORS is not a host policy in this contract; middleware is the supported
|
|
14
|
+
place for it, and it needs the sandbox, so serverless targets refuse this
|
|
15
|
+
project. A browser-facing API that also needs credentials must add
|
|
16
|
+
`Access-Control-Allow-Credentials` deliberately, never with a wildcard origin.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Edit this allowlist. The runtime has no CORS policy; middleware is the
|
|
2
|
+
// supported place for it, and it runs only on routes that declare it.
|
|
3
|
+
const allowedOrigins = ['https://app.example.com'];
|
|
4
|
+
const allowMethods = 'GET, HEAD, OPTIONS';
|
|
5
|
+
const allowHeaders = 'Content-Type';
|
|
6
|
+
|
|
7
|
+
export default async function cors(request, context, next) {
|
|
8
|
+
const origin = request.headers.get('origin');
|
|
9
|
+
const allowed = origin !== null && allowedOrigins.includes(origin);
|
|
10
|
+
if (request.method === 'OPTIONS') {
|
|
11
|
+
// Preflight never reaches the handler. A disallowed origin gets no CORS
|
|
12
|
+
// headers, so the browser refuses the real request.
|
|
13
|
+
const headers = {Vary: 'Origin'};
|
|
14
|
+
if (allowed) Object.assign(headers, {'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': allowMethods, 'Access-Control-Allow-Headers': allowHeaders, 'Access-Control-Max-Age': '600'});
|
|
15
|
+
return new Response(null, {status: 204, headers});
|
|
16
|
+
}
|
|
17
|
+
const response = await next();
|
|
18
|
+
response.headers.set('Vary', 'Origin');
|
|
19
|
+
if (allowed) response.headers.set('Access-Control-Allow-Origin', origin);
|
|
20
|
+
return response;
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: cors-api
|
|
2
|
+
description: JSON endpoint for one allowed browser origin; middleware answers preflight and adds CORS headers.
|
|
3
|
+
tags: [cors, preflight, options, browser, origin, api, json, middleware, access-control, vary]
|
|
4
|
+
complexity: intermediate
|
|
5
|
+
capabilities: [enabled, function, methods, middleware, response.headers]
|
|
6
|
+
targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
|
|
7
|
+
routes: 1
|
|
8
|
+
inputs:
|
|
9
|
+
- name: allowedOrigins
|
|
10
|
+
file: middleware/cors.mjs
|
|
11
|
+
description: The browser origins allowed to read the response.
|
|
12
|
+
- name: handler
|
|
13
|
+
file: functions/items.mjs
|
|
14
|
+
description: Replace the literal list with the real data.
|
|
15
|
+
files: [urlcode.yaml, middleware/cors.mjs, functions/items.mjs, tests/requests.json, README.md]
|
|
16
|
+
tests:
|
|
17
|
+
fixtures: tests/requests.json
|
|
18
|
+
commands:
|
|
19
|
+
- urlcode validate --local --project .
|
|
20
|
+
- urlcode test --project .
|
|
21
|
+
- urlcode audit --project . --expect-routes 1
|
|
22
|
+
behavior:
|
|
23
|
+
- OPTIONS /api/items from https://app.example.com answers 204 with Access-Control-Allow-Origin, -Methods and -Headers
|
|
24
|
+
- GET from the allowed origin carries Access-Control-Allow-Origin; other origins get Vary Origin and no allow header
|
|
25
|
+
- the route lists OPTIONS in methods, otherwise the runtime answers 405 before the middleware runs
|
|
26
|
+
- CORS is middleware, not a host policy, so the route needs the sandbox
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/api/items",
|
|
4
|
+
"headers": {
|
|
5
|
+
"origin": "https://app.example.com"
|
|
6
|
+
},
|
|
7
|
+
"status": 200,
|
|
8
|
+
"expectBody": "{\"items\":[{\"id\":1,\"name\":\"first\"},{\"id\":2,\"name\":\"second\"}]}",
|
|
9
|
+
"expectHeaders": {
|
|
10
|
+
"access-control-allow-origin": "https://app.example.com",
|
|
11
|
+
"vary": "Origin",
|
|
12
|
+
"cache-control": "no-store"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "/api/items",
|
|
17
|
+
"status": 200,
|
|
18
|
+
"expectBody": "{\"items\":[{\"id\":1,\"name\":\"first\"},{\"id\":2,\"name\":\"second\"}]}",
|
|
19
|
+
"expectHeaders": {
|
|
20
|
+
"vary": "Origin"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "/api/items",
|
|
25
|
+
"method": "HEAD",
|
|
26
|
+
"headers": {
|
|
27
|
+
"origin": "https://app.example.com"
|
|
28
|
+
},
|
|
29
|
+
"status": 200,
|
|
30
|
+
"expectBody": "",
|
|
31
|
+
"expectHeaders": {
|
|
32
|
+
"access-control-allow-origin": "https://app.example.com"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "/api/items",
|
|
37
|
+
"method": "OPTIONS",
|
|
38
|
+
"headers": {
|
|
39
|
+
"origin": "https://app.example.com",
|
|
40
|
+
"access-control-request-method": "GET"
|
|
41
|
+
},
|
|
42
|
+
"status": 204,
|
|
43
|
+
"expectBody": "",
|
|
44
|
+
"expectHeaders": {
|
|
45
|
+
"access-control-allow-origin": "https://app.example.com",
|
|
46
|
+
"access-control-allow-methods": "GET, HEAD, OPTIONS",
|
|
47
|
+
"access-control-allow-headers": "Content-Type"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "/api/items",
|
|
52
|
+
"method": "OPTIONS",
|
|
53
|
+
"headers": {
|
|
54
|
+
"origin": "https://evil.example.net",
|
|
55
|
+
"access-control-request-method": "GET"
|
|
56
|
+
},
|
|
57
|
+
"status": 204,
|
|
58
|
+
"expectBody": ""
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"path": "/api/items",
|
|
62
|
+
"method": "POST",
|
|
63
|
+
"status": 405
|
|
64
|
+
}
|
|
65
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
version: "1"
|
|
2
|
+
routes:
|
|
3
|
+
/api/items:
|
|
4
|
+
description: JSON list callable from one allowed browser origin; middleware answers preflight and adds CORS headers.
|
|
5
|
+
methods: [GET, HEAD, OPTIONS]
|
|
6
|
+
middleware:
|
|
7
|
+
- source: middleware/cors.mjs
|
|
8
|
+
function:
|
|
9
|
+
source: functions/items.mjs
|
|
10
|
+
response:
|
|
11
|
+
headers:
|
|
12
|
+
Cache-Control: no-store
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Health and status
|
|
2
|
+
|
|
3
|
+
Run `urlcode validate --local --project .`, `urlcode test --project .` and
|
|
4
|
+
`urlcode audit --project . --expect-routes 2`.
|
|
5
|
+
|
|
6
|
+
`/health` answers `ok` as text and `/status` answers a small JSON document. Both
|
|
7
|
+
are native `respond` routes with `Cache-Control: no-store`, so no sandbox runs
|
|
8
|
+
and every target supports them. Change `service` in `urlcode.yaml` to your
|
|
9
|
+
service name.
|
|
10
|
+
|
|
11
|
+
These routes report that the runtime is serving this revision; they do not
|
|
12
|
+
check databases or upstreams, because native routes run no code. Operators who
|
|
13
|
+
want process-level detail use the runtime's own metrics endpoint instead.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: health-page
|
|
2
|
+
description: Plain-text liveness and JSON readiness answers served natively with no caching.
|
|
3
|
+
tags: [health, status, readiness, liveness, uptime, probe, monitoring, respond, native, json]
|
|
4
|
+
complexity: starter
|
|
5
|
+
capabilities: [enabled, methods, respond, response.headers]
|
|
6
|
+
targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible}
|
|
7
|
+
routes: 2
|
|
8
|
+
inputs:
|
|
9
|
+
- name: service
|
|
10
|
+
file: urlcode.yaml
|
|
11
|
+
description: The service name in the /status document.
|
|
12
|
+
files: [urlcode.yaml, tests/requests.json, README.md]
|
|
13
|
+
tests:
|
|
14
|
+
fixtures: tests/requests.json
|
|
15
|
+
commands:
|
|
16
|
+
- urlcode validate --local --project .
|
|
17
|
+
- urlcode test --project .
|
|
18
|
+
- urlcode audit --project . --expect-routes 2
|
|
19
|
+
behavior:
|
|
20
|
+
- GET /health answers 200 "ok" with Cache-Control no-store
|
|
21
|
+
- GET /status answers 200 {"ok":true,"service":"example","schema":1}
|
|
22
|
+
- HEAD answers empty bodies; POST answers 405
|
|
23
|
+
- the routes report that this revision is serving; native routes check no upstream
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/health",
|
|
4
|
+
"status": 200,
|
|
5
|
+
"expectBody": "ok",
|
|
6
|
+
"expectHeaders": {
|
|
7
|
+
"cache-control": "no-store"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "/health",
|
|
12
|
+
"method": "HEAD",
|
|
13
|
+
"status": 200,
|
|
14
|
+
"expectBody": ""
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "/status",
|
|
18
|
+
"status": 200,
|
|
19
|
+
"expectBody": "{\"ok\":true,\"service\":\"example\",\"schema\":1}",
|
|
20
|
+
"expectHeaders": {
|
|
21
|
+
"content-type": "application/json; charset=utf-8",
|
|
22
|
+
"cache-control": "no-store"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "/status",
|
|
27
|
+
"method": "HEAD",
|
|
28
|
+
"status": 200,
|
|
29
|
+
"expectBody": ""
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "/status",
|
|
33
|
+
"method": "POST",
|
|
34
|
+
"status": 405
|
|
35
|
+
}
|
|
36
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: "1"
|
|
2
|
+
routes:
|
|
3
|
+
/health:
|
|
4
|
+
description: Plain-text liveness answer for load balancers and uptime probes.
|
|
5
|
+
respond:
|
|
6
|
+
text: ok
|
|
7
|
+
response:
|
|
8
|
+
headers:
|
|
9
|
+
Cache-Control: no-store
|
|
10
|
+
/status:
|
|
11
|
+
description: JSON status document with the service name and a fixed schema version.
|
|
12
|
+
respond:
|
|
13
|
+
json:
|
|
14
|
+
ok: true
|
|
15
|
+
service: example
|
|
16
|
+
schema: 1
|
|
17
|
+
response:
|
|
18
|
+
headers:
|
|
19
|
+
Cache-Control: no-store
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# JSON echo API
|
|
2
|
+
|
|
3
|
+
Run `urlcode validate --local --project .` and `urlcode serve --project .`.
|
|
4
|
+
POST JSON with `Content-Type: application/json` to `/echo`. The sandbox returns
|
|
5
|
+
`{"received": ...}`. Other methods are refused. Do not submit credentials to an
|
|
6
|
+
echo endpoint. This recipe grants no network or filesystem access.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
id: json-api
|
|
2
|
+
description: Bounded JSON request body echoed back by a sandboxed function.
|
|
3
|
+
tags: [json, api, post, echo, function, body, validation, sandbox]
|
|
4
|
+
complexity: starter
|
|
5
|
+
capabilities: [enabled, function, methods, request.body]
|
|
6
|
+
targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
|
|
7
|
+
routes: 1
|
|
8
|
+
inputs:
|
|
9
|
+
- name: handler
|
|
10
|
+
file: functions/echo.mjs
|
|
11
|
+
description: Replace the echo with the real response; the parsed body arrives through request.json().
|
|
12
|
+
- name: maxBytes
|
|
13
|
+
file: urlcode.yaml
|
|
14
|
+
description: The request body limit (16 KiB by default).
|
|
15
|
+
files: [urlcode.yaml, functions/echo.mjs, tests/requests.json, README.md]
|
|
16
|
+
tests:
|
|
17
|
+
fixtures: tests/requests.json
|
|
18
|
+
commands:
|
|
19
|
+
- urlcode validate --local --project .
|
|
20
|
+
- urlcode test --project .
|
|
21
|
+
- urlcode audit --project . --expect-routes 1
|
|
22
|
+
behavior:
|
|
23
|
+
- POST /echo with application/json answers 200 {"received":<body>}
|
|
24
|
+
- malformed JSON answers 400 and another content type 415 before the sandbox runs
|
|
25
|
+
- GET answers 405
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/echo",
|
|
4
|
+
"method": "POST",
|
|
5
|
+
"headers": {
|
|
6
|
+
"content-type": "application/json"
|
|
7
|
+
},
|
|
8
|
+
"body": "{\"hello\":\"world\"}",
|
|
9
|
+
"status": 200,
|
|
10
|
+
"expectBody": "{\"received\":{\"hello\":\"world\"}}"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "/echo",
|
|
14
|
+
"method": "POST",
|
|
15
|
+
"headers": {
|
|
16
|
+
"content-type": "application/json"
|
|
17
|
+
},
|
|
18
|
+
"body": "{",
|
|
19
|
+
"status": 400
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "/echo",
|
|
23
|
+
"method": "POST",
|
|
24
|
+
"headers": {
|
|
25
|
+
"content-type": "text/plain"
|
|
26
|
+
},
|
|
27
|
+
"body": "hello",
|
|
28
|
+
"status": 415
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "/echo",
|
|
32
|
+
"status": 405
|
|
33
|
+
}
|
|
34
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Middleware patterns
|
|
2
|
+
|
|
3
|
+
Fourteen reusable middleware modules on fifteen routes (`auth.mjs` serves two),
|
|
4
|
+
covering what Express, Koa,
|
|
5
|
+
Hono, Next.js and edge runtimes usually ship as middleware, rewritten for
|
|
6
|
+
URLCode's small sandboxed guest API. Run `urlcode validate --local --project .`
|
|
7
|
+
and `urlcode serve --project .`, then keep the routes you need and delete the
|
|
8
|
+
rest; every module is independent.
|
|
9
|
+
|
|
10
|
+
| Route | Module | Pattern |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `/api/private` | `middleware/auth.mjs` (`bearer`) | Bearer token with constant-time comparison |
|
|
13
|
+
| `/admin/panel` | `middleware/auth.mjs` (`basic`) | HTTP Basic decoded in the guest |
|
|
14
|
+
| `/cors/data` | `middleware/cors.mjs` | Origin allowlist and preflight |
|
|
15
|
+
| `/traced` | `middleware/request-id.mjs` | Caller correlation id and Server-Timing |
|
|
16
|
+
| `/maintenance` | `middleware/maintenance.mjs` | 503 kill switch with bypass header |
|
|
17
|
+
| `/fragile` | `middleware/errors.mjs` | Error boundary returning JSON 500 |
|
|
18
|
+
| `/api/items` | `middleware/envelope.mjs` | JSON response envelope |
|
|
19
|
+
| `/negotiated` | `middleware/negotiate.mjs` | Accept negotiation, 406 otherwise |
|
|
20
|
+
| `/resource` | `middleware/methods.mjs` | POST method override |
|
|
21
|
+
| `/versioned` | `middleware/etag.mjs` | Weak ETag and 304 for function output |
|
|
22
|
+
| `/experiment` | `middleware/bucket.mjs` | Sticky A/B cookie bucket |
|
|
23
|
+
| `/welcome` | `middleware/locale.mjs` | Accept-Language redirect |
|
|
24
|
+
| `/downloads/report` | `middleware/referer.mjs` | Referer allowlist on a download |
|
|
25
|
+
| `/profile` | `middleware/body.mjs` | Body validation handed through `context.state` |
|
|
26
|
+
| `/inspect` | `middleware/debug.mjs` | Redacted request echo for authoring |
|
|
27
|
+
|
|
28
|
+
Tokens, passwords and switches are literal `env` values so the recipe runs
|
|
29
|
+
without grants. Before deploying, move credentials to `{secret: name}` bindings
|
|
30
|
+
with an operator grant, replace the example.com destinations, and delete the
|
|
31
|
+
`/inspect` route or set `DEBUG` to `false`. The guest exposes no crypto, so none
|
|
32
|
+
of these modules can verify signatures or hash passwords; treat them as request
|
|
33
|
+
shaping, not as a security boundary. See the runtime's middleware documentation
|
|
34
|
+
for the full list of guest limits.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Authentication gates. Like Hono bearerAuth/basicAuth or express-basic-auth,
|
|
2
|
+
// but the expected credential comes from the route's declared bindings.
|
|
3
|
+
// The cookbook binds literal values; real projects use {secret: name} grants.
|
|
4
|
+
|
|
5
|
+
// Compare without leaking where two strings differ through timing.
|
|
6
|
+
function same(actual, expected) {
|
|
7
|
+
if (typeof actual !== 'string' || typeof expected !== 'string') return false;
|
|
8
|
+
const length = Math.max(actual.length, expected.length);
|
|
9
|
+
let mismatch = actual.length ^ expected.length;
|
|
10
|
+
for (let i = 0; i < length; i++) mismatch |= (actual.charCodeAt(i) || 0) ^ (expected.charCodeAt(i) || 0);
|
|
11
|
+
return mismatch === 0 && length > 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The guest exposes no atob; a small decoder handles standard Basic credentials.
|
|
15
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
16
|
+
function decodeBase64(text) {
|
|
17
|
+
const clean = String(text).replace(/=+$/, '');
|
|
18
|
+
if (!/^[A-Za-z0-9+/]*$/.test(clean)) return null;
|
|
19
|
+
let bits = 0, buffer = 0, out = '';
|
|
20
|
+
for (const char of clean) {
|
|
21
|
+
buffer = (buffer << 6) | alphabet.indexOf(char); bits += 6;
|
|
22
|
+
if (bits >= 8) { bits -= 8; out += String.fromCharCode((buffer >> bits) & 0xff); }
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function bearer(request, context, next) {
|
|
28
|
+
const header = request.headers.get('authorization') || '';
|
|
29
|
+
const token = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
|
|
30
|
+
if (!same(token, context.env.API_TOKEN)) {
|
|
31
|
+
return new Response('Unauthorized\n', {status: 401, headers: {'www-authenticate': 'Bearer realm="cookbook"', 'cache-control': 'no-store'}});
|
|
32
|
+
}
|
|
33
|
+
return next();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function basic(request, context, next) {
|
|
37
|
+
const header = request.headers.get('authorization') || '';
|
|
38
|
+
const decoded = header.startsWith('Basic ') ? decodeBase64(header.slice(6).trim()) : null;
|
|
39
|
+
const separator = decoded === null ? -1 : decoded.indexOf(':');
|
|
40
|
+
const user = separator < 0 ? '' : decoded.slice(0, separator), password = separator < 0 ? '' : decoded.slice(separator + 1);
|
|
41
|
+
// Evaluate both comparisons so a wrong user costs the same as a wrong password.
|
|
42
|
+
const userOk = same(user, context.env.ADMIN_USER), passwordOk = same(password, context.env.ADMIN_PASSWORD);
|
|
43
|
+
if (!(userOk && passwordOk)) {
|
|
44
|
+
return new Response('Unauthorized\n', {status: 401, headers: {'www-authenticate': 'Basic realm="cookbook", charset="UTF-8"', 'cache-control': 'no-store'}});
|
|
45
|
+
}
|
|
46
|
+
context.state.user = user;
|
|
47
|
+
return next();
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Validate a JSON body once and hand the parsed object to the handler through
|
|
2
|
+
// context.state. Bodies are single-use, so downstream code must not read again.
|
|
3
|
+
export default async function validateBody(request, context, next) {
|
|
4
|
+
const body = await request.json();
|
|
5
|
+
const errors = [];
|
|
6
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) errors.push('body must be an object');
|
|
7
|
+
else {
|
|
8
|
+
if (typeof body.name !== 'string' || body.name.length < 1 || body.name.length > 80) errors.push('name must be 1-80 characters');
|
|
9
|
+
if (body.age !== undefined && (!Number.isInteger(body.age) || body.age < 0 || body.age > 150)) errors.push('age must be an integer from 0 to 150');
|
|
10
|
+
for (const key of Object.keys(body)) if (!['name', 'age'].includes(key)) errors.push('unknown field ' + key);
|
|
11
|
+
}
|
|
12
|
+
if (errors.length) return Response.json({errors}, {status: 422});
|
|
13
|
+
context.state.body = body;
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Sticky A/B bucketing through a cookie, as on Vercel or Cloudflare edge examples.
|
|
2
|
+
// Bucket b gets a different destination; everyone keeps their bucket for a week.
|
|
3
|
+
function cookie(request, name) {
|
|
4
|
+
for (const part of (request.headers.get('cookie') || '').split(';')) {
|
|
5
|
+
const [key, ...rest] = part.trim().split('=');
|
|
6
|
+
if (key === name) return rest.join('=');
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
export default async function bucket(request, context, next) {
|
|
11
|
+
let assigned = cookie(request, 'bucket');
|
|
12
|
+
const fresh = assigned !== 'a' && assigned !== 'b';
|
|
13
|
+
if (fresh) assigned = Math.random() < 0.5 ? 'a' : 'b';
|
|
14
|
+
context.state.bucket = assigned;
|
|
15
|
+
const response = assigned === 'b' ? Response.redirect(context.env.VARIANT_URL, 302) : await next();
|
|
16
|
+
response.headers.set('vary', 'cookie');
|
|
17
|
+
if (fresh) response.headers.append('set-cookie', 'bucket=' + assigned + '; Path=/; Max-Age=604800; SameSite=Lax');
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Cross-origin resource sharing with an origin allowlist, as in Express cors
|
|
2
|
+
// or Hono cors. The route must declare OPTIONS in its methods for preflight.
|
|
3
|
+
const preflightHeaders = {
|
|
4
|
+
'access-control-allow-methods': 'GET, HEAD, OPTIONS',
|
|
5
|
+
'access-control-allow-headers': 'Content-Type, Authorization',
|
|
6
|
+
'access-control-max-age': '600'
|
|
7
|
+
};
|
|
8
|
+
export default async function cors(request, context, next) {
|
|
9
|
+
const allowed = (context.env.ALLOWED_ORIGINS || '').split(/\s+/).filter(Boolean);
|
|
10
|
+
const origin = request.headers.get('origin');
|
|
11
|
+
const permitted = origin !== null && allowed.includes(origin);
|
|
12
|
+
if (request.method === 'OPTIONS') {
|
|
13
|
+
// Preflight never reaches the handler. Unknown origins get no allow headers.
|
|
14
|
+
const headers = {vary: 'origin', ...(permitted ? {...preflightHeaders, 'access-control-allow-origin': origin} : {})};
|
|
15
|
+
return new Response(null, {status: 204, headers});
|
|
16
|
+
}
|
|
17
|
+
const response = await next();
|
|
18
|
+
response.headers.set('vary', 'origin');
|
|
19
|
+
if (permitted) response.headers.set('access-control-allow-origin', origin);
|
|
20
|
+
return response;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Authoring aid: with DEBUG on and X-Debug: 1, describe what the guest received.
|
|
2
|
+
// Secrets and credential-bearing headers are never echoed.
|
|
3
|
+
const hidden = ['authorization', 'cookie', 'proxy-authorization'];
|
|
4
|
+
export default async function debug(request, context, next) {
|
|
5
|
+
if (context.env.DEBUG !== 'true' || request.headers.get('x-debug') !== '1') return next();
|
|
6
|
+
const headers = {};
|
|
7
|
+
for (const [name, value] of request.headers) headers[name] = hidden.includes(name) ? '[redacted]' : value;
|
|
8
|
+
return Response.json({
|
|
9
|
+
method: request.method, url: request.url, headers,
|
|
10
|
+
inputs: context.inputs, args: context.args ?? null,
|
|
11
|
+
env: Object.keys(context.env), secrets: Object.keys(context.secrets)
|
|
12
|
+
}, {headers: {'cache-control': 'no-store'}});
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Wrap successful JSON function responses in a stable envelope. Native bodies are
|
|
2
|
+
// opaque and pass through untouched; only readable JSON is rewritten.
|
|
3
|
+
export default async function envelope(request, context, next) {
|
|
4
|
+
const response = await next();
|
|
5
|
+
if (!response.ok || !(response.headers.get('content-type') || '').startsWith('application/json')) return response;
|
|
6
|
+
let data;
|
|
7
|
+
try { data = await response.json(); } catch { return response; }
|
|
8
|
+
const path = request.url.replace(/^[a-z]+:\/\/[^/]+/i, '').split('?')[0];
|
|
9
|
+
const meta = {route: path, count: Array.isArray(data) ? data.length : 1};
|
|
10
|
+
return Response.json({ok: true, data, meta}, {status: response.status, headers: response.headers});
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Error boundary. Without it, a throwing function is a bare 502 from the runtime.
|
|
2
|
+
// Nothing about the thrown error reaches the client; the guest console is silent,
|
|
3
|
+
// so the correlation id is what an operator can search for.
|
|
4
|
+
export default async function errorBoundary(request, context, next) {
|
|
5
|
+
try {
|
|
6
|
+
return await next();
|
|
7
|
+
} catch {
|
|
8
|
+
return Response.json({error: 'Temporarily unavailable', correlationId: context.state.correlationId ?? null},
|
|
9
|
+
{status: 500, headers: {'cache-control': 'no-store'}});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Conditional requests for function output: a weak ETag from a cheap FNV-1a
|
|
2
|
+
// hash, plus 304 when the client already holds that version. Native static
|
|
3
|
+
// and download routes already do this in the runtime; this is for JSON.
|
|
4
|
+
function fnv1a(text) {
|
|
5
|
+
let hash = 0x811c9dc5;
|
|
6
|
+
for (let i = 0; i < text.length; i++) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 0x01000193) >>> 0; }
|
|
7
|
+
return hash.toString(16).padStart(8, '0');
|
|
8
|
+
}
|
|
9
|
+
export default async function etag(request, context, next) {
|
|
10
|
+
const response = await next();
|
|
11
|
+
if (!response.ok || request.method !== 'GET' && request.method !== 'HEAD') return response;
|
|
12
|
+
const body = await response.text();
|
|
13
|
+
const tag = 'W/"' + fnv1a(body) + '"';
|
|
14
|
+
const headers = {...Object.fromEntries(response.headers), etag: tag, 'cache-control': 'public, max-age=60'};
|
|
15
|
+
const held = (request.headers.get('if-none-match') || '').split(',').map(s => s.trim());
|
|
16
|
+
if (held.includes(tag) || held.includes('*')) return new Response(null, {status: 304, headers: {etag: tag, 'cache-control': headers['cache-control']}});
|
|
17
|
+
return new Response(body, {status: response.status, headers});
|
|
18
|
+
}
|