@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
- package/.claude/skills/urlcode-operations/SKILL.md +108 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +195 -255
- package/ROADMAP.md +143 -15
- package/SECURITY.md +31 -9
- package/dist/BUILD-MANIFEST.json +72 -47
- package/dist/adapters.js +4 -23
- package/dist/agent-lists.js +1 -1
- package/dist/agents-guide.js +113 -0
- package/dist/authoring-files.js +60 -0
- package/dist/authoring.js +11 -1
- package/dist/aws.js +4 -3
- package/dist/build-cloudflare.js +11 -24
- package/dist/build-static.js +134 -0
- package/dist/bulk.js +37 -0
- package/dist/capabilities.js +262 -0
- package/dist/capability-query.js +71 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +165 -34
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +9 -17
- package/dist/compliance-rules/privacy.js +7 -18
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/compliance.js +6 -8
- package/dist/conditions.js +88 -0
- package/dist/config.js +69 -6
- package/dist/context.js +155 -0
- package/dist/ecosystem-cli.js +88 -0
- package/dist/egress.js +98 -0
- package/dist/examples.js +92 -0
- package/dist/explain-cli.js +64 -0
- package/dist/explain.js +131 -0
- package/dist/extensions.js +231 -0
- package/dist/function-sources.js +49 -5
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +40 -6
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/manifest.js +109 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +7 -21
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/cache.js +8 -3
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +56 -15
- package/dist/prerender.js +100 -41
- package/dist/project-tests.js +3 -3
- package/dist/provider-verification.js +92 -0
- package/dist/proxy.js +44 -0
- package/dist/readiness.js +34 -11
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +45 -7
- package/dist/runtime.js +164 -64
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/schema-query.js +62 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -0
- package/dist/trusted-functions.js +210 -0
- package/dist/types/adapters.d.ts +7 -4
- package/dist/types/agent-lists.d.ts +0 -1
- package/dist/types/agents-guide.d.ts +17 -0
- package/dist/types/authoring-files.d.ts +10 -0
- package/dist/types/aws.d.ts +3 -1
- package/dist/types/build-cloudflare.d.ts +1 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +64 -0
- package/dist/types/capability-query.d.ts +24 -0
- package/dist/types/catalog.d.ts +65 -0
- package/dist/types/client-address.d.ts +0 -1
- package/dist/types/compliance-rules/baseline.d.ts +1 -9
- package/dist/types/compliance-rules/privacy.d.ts +1 -4
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance-rules/strict.d.ts +0 -5
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +21 -2
- package/dist/types/context.d.ts +66 -0
- package/dist/types/ecosystem-cli.d.ts +17 -0
- package/dist/types/egress.d.ts +46 -0
- package/dist/types/examples.d.ts +50 -0
- package/dist/types/explain-cli.d.ts +11 -0
- package/dist/types/explain.d.ts +95 -0
- package/dist/types/extensions.d.ts +177 -0
- package/dist/types/function-sources.d.ts +9 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +36 -6
- package/dist/types/init-with.d.ts +30 -0
- package/dist/types/interchange-cli.d.ts +16 -0
- package/dist/types/interchange.d.ts +42 -0
- package/dist/types/manifest.d.ts +79 -0
- package/dist/types/match.d.ts +1 -0
- package/dist/types/mcp-authoring.d.ts +92 -0
- package/dist/types/mcp.d.ts +12 -0
- package/dist/types/observability.d.ts +3 -14
- package/dist/types/operator-host.d.ts +8 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/policies/agents.d.ts +0 -2
- package/dist/types/policies/compression.d.ts +2 -0
- package/dist/types/policies/security.d.ts +0 -1
- package/dist/types/policy.d.ts +15 -4
- package/dist/types/project-tests.d.ts +3 -2
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +10 -3
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +11 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/schema-query.d.ts +12 -0
- package/dist/types/server.d.ts +1 -4
- package/dist/types/signals.d.ts +25 -0
- package/dist/types/site.d.ts +0 -1
- package/dist/types/tooling.d.ts +115 -0
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +71 -7
- package/dist/types/typescript-authoring.d.ts +12 -0
- package/dist/types/vercel.d.ts +3 -1
- package/dist/types/verify-deployment.d.ts +47 -0
- package/dist/types.js +37 -5
- package/dist/typescript-authoring.js +142 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +232 -15
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +3 -2
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +192 -0
- package/docs/CAPACITY.md +129 -32
- package/docs/CI.md +142 -0
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/COMPLIANCE.md +6 -9
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +398 -0
- package/docs/FRAMEWORK.md +198 -0
- package/docs/FUNCTION-SECURITY.md +129 -32
- package/docs/INSTALL.md +45 -12
- package/docs/INTERCHANGE.md +134 -0
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +29 -16
- package/docs/MONITORING.md +2 -19
- package/docs/NEXT-PHASE-PLAN.md +98 -0
- package/docs/NEXT-STEPS.md +634 -0
- package/docs/OBSERVABILITY.md +11 -18
- package/docs/OPEN-DECISIONS.md +212 -0
- package/docs/OPERATIONAL-PROOF.md +30 -31
- package/docs/OPERATIONS.md +29 -35
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +23 -309
- package/docs/PRERENDER.md +41 -1
- package/docs/PROJECT-DIRECTION.md +75 -8
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +87 -34
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +57 -35
- package/docs/RELEASE-SECURITY.md +116 -7
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +8 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +41 -1
- package/docs/SPECIFICATION.md +150 -29
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
- package/docs/SPIKE-CORE-LAYERING.md +337 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +6 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
- package/docs/SPIKE-MONOREPO.md +322 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/STATIC.md +94 -0
- package/docs/TOOLING.md +295 -0
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +82 -0
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/USABILITY-REVIEW.md +129 -0
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +205 -0
- package/docs/YAML-GUIDE.md +15 -479
- package/docs/YAML-REFERENCE.md +143 -22
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/contract.md +52 -0
- package/docs/policies/hardened.md +56 -0
- package/docs/policies/interoperability.md +169 -0
- package/docs/policies/operations.md +45 -0
- package/docs/yaml/assets.md +36 -0
- package/docs/yaml/conditions.md +20 -0
- package/docs/yaml/functions.md +160 -0
- package/docs/yaml/middleware.md +29 -0
- package/docs/yaml/organization.md +74 -0
- package/docs/yaml/policies.md +37 -0
- package/docs/yaml/redirects.md +64 -0
- package/docs/yaml/responses.md +57 -0
- package/docs/yaml/site.md +24 -0
- package/examples/assets/example.yaml +17 -0
- package/examples/aws/example.yaml +20 -0
- package/examples/cloudflare/example.yaml +19 -0
- package/examples/compliance/example.yaml +11 -0
- package/examples/conditions/README.md +12 -0
- package/examples/conditions/example.yaml +19 -0
- package/examples/conditions/tests/requests.json +13 -0
- package/examples/conditions/urlcode.yaml +24 -0
- package/examples/cookbook/README.md +8 -4
- package/examples/cookbook/example.yaml +17 -0
- package/examples/cookbook/functions/catalog.mjs +3 -0
- package/examples/cookbook/functions/fail.mjs +4 -0
- package/examples/cookbook/functions/items.mjs +3 -0
- package/examples/cookbook/functions/profile.mjs +3 -0
- package/examples/cookbook/functions/resource.mjs +3 -0
- package/examples/cookbook/functions/status.mjs +3 -0
- package/examples/cookbook/middleware/auth.mjs +48 -0
- package/examples/cookbook/middleware/body.mjs +15 -0
- package/examples/cookbook/middleware/bucket.mjs +29 -0
- package/examples/cookbook/middleware/cors.mjs +21 -0
- package/examples/cookbook/middleware/debug.mjs +13 -0
- package/examples/cookbook/middleware/envelope.mjs +11 -0
- package/examples/cookbook/middleware/errors.mjs +11 -0
- package/examples/cookbook/middleware/etag.mjs +18 -0
- package/examples/cookbook/middleware/locale.mjs +20 -0
- package/examples/cookbook/middleware/maintenance.mjs +10 -0
- package/examples/cookbook/middleware/methods.mjs +15 -0
- package/examples/cookbook/middleware/negotiate.mjs +20 -0
- package/examples/cookbook/middleware/referer.mjs +12 -0
- package/examples/cookbook/middleware/request-id.mjs +16 -0
- package/examples/cookbook/route-index.json +676 -0
- package/examples/cookbook/routes/middleware.yaml +126 -0
- package/examples/cookbook/tests/requests.json +526 -0
- package/examples/cookbook/urlcode.yaml +1 -0
- package/examples/egress/README.md +22 -0
- package/examples/egress/example.yaml +19 -0
- package/examples/egress/urlcode.yaml +19 -0
- package/examples/extensions/README.md +7 -0
- package/examples/extensions/example.yaml +21 -0
- package/examples/extensions/urlcode.yaml +25 -0
- package/examples/monitoring/example.yaml +8 -0
- package/examples/prerender/README.md +2 -2
- package/examples/prerender/example.yaml +16 -0
- package/examples/provider-conformance/README.md +12 -0
- package/examples/provider-conformance/example.yaml +14 -0
- package/examples/provider-conformance/urlcode.yaml +34 -0
- package/examples/tunnel/example.yaml +8 -0
- package/examples/vercel/example.yaml +19 -0
- package/llms-full.txt +3084 -0
- package/llms.txt +61 -21
- package/package.json +36 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
- package/recipes/authenticated-json-api/README.md +51 -0
- package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
- package/recipes/authenticated-json-api/recipe.yaml +34 -0
- package/recipes/authenticated-json-api/tests/requests.json +39 -0
- package/recipes/authenticated-json-api/urlcode.yaml +12 -0
- package/recipes/contact-form/README.md +25 -0
- package/recipes/contact-form/functions/contact.mjs +17 -0
- package/recipes/contact-form/recipe.yaml +33 -0
- package/recipes/contact-form/tests/requests.json +47 -0
- package/recipes/contact-form/urlcode.yaml +18 -0
- package/recipes/cors-api/README.md +16 -0
- package/recipes/cors-api/functions/items.mjs +3 -0
- package/recipes/cors-api/middleware/cors.mjs +21 -0
- package/recipes/cors-api/recipe.yaml +26 -0
- package/recipes/cors-api/tests/requests.json +65 -0
- package/recipes/cors-api/urlcode.yaml +12 -0
- package/recipes/health-page/README.md +13 -0
- package/recipes/health-page/recipe.yaml +23 -0
- package/recipes/health-page/tests/requests.json +36 -0
- package/recipes/health-page/urlcode.yaml +19 -0
- package/recipes/json-api/README.md +6 -0
- package/recipes/json-api/functions/echo.mjs +3 -0
- package/recipes/json-api/recipe.yaml +25 -0
- package/recipes/json-api/tests/requests.json +34 -0
- package/recipes/json-api/urlcode.yaml +12 -0
- package/recipes/middleware/README.md +34 -0
- package/recipes/middleware/functions/catalog.mjs +3 -0
- package/recipes/middleware/functions/fail.mjs +4 -0
- package/recipes/middleware/functions/items.mjs +3 -0
- package/recipes/middleware/functions/profile.mjs +3 -0
- package/recipes/middleware/functions/resource.mjs +3 -0
- package/recipes/middleware/functions/status.mjs +3 -0
- package/recipes/middleware/middleware/auth.mjs +48 -0
- package/recipes/middleware/middleware/body.mjs +15 -0
- package/recipes/middleware/middleware/bucket.mjs +29 -0
- package/recipes/middleware/middleware/cors.mjs +21 -0
- package/recipes/middleware/middleware/debug.mjs +13 -0
- package/recipes/middleware/middleware/envelope.mjs +11 -0
- package/recipes/middleware/middleware/errors.mjs +11 -0
- package/recipes/middleware/middleware/etag.mjs +18 -0
- package/recipes/middleware/middleware/locale.mjs +20 -0
- package/recipes/middleware/middleware/maintenance.mjs +10 -0
- package/recipes/middleware/middleware/methods.mjs +15 -0
- package/recipes/middleware/middleware/negotiate.mjs +20 -0
- package/recipes/middleware/middleware/referer.mjs +12 -0
- package/recipes/middleware/middleware/request-id.mjs +16 -0
- package/recipes/middleware/public/guide.txt +1 -0
- package/recipes/middleware/recipe.yaml +50 -0
- package/recipes/middleware/tests/requests.json +528 -0
- package/recipes/middleware/urlcode.yaml +127 -0
- package/recipes/protected-download/README.md +22 -0
- package/recipes/protected-download/files/report.txt +1 -0
- package/recipes/protected-download/recipe.yaml +31 -0
- package/recipes/protected-download/tests/requests.json +32 -0
- package/recipes/protected-download/urlcode.yaml +15 -0
- package/recipes/redirect/README.md +7 -0
- package/recipes/redirect/recipe.yaml +25 -0
- package/recipes/redirect/tests/requests.json +19 -0
- package/recipes/redirect/urlcode.yaml +9 -0
- package/recipes/static-plus-api/README.md +15 -0
- package/recipes/static-plus-api/functions/info.mjs +3 -0
- package/recipes/static-plus-api/public/assets/index.html +3 -0
- package/recipes/static-plus-api/public/assets/site.css +1 -0
- package/recipes/static-plus-api/public/index.html +8 -0
- package/recipes/static-plus-api/recipe.yaml +29 -0
- package/recipes/static-plus-api/tests/requests.json +56 -0
- package/recipes/static-plus-api/urlcode.yaml +23 -0
- package/recipes/typescript/README.md +8 -0
- package/recipes/typescript/functions/hello.ts +5 -0
- package/recipes/typescript/recipe.yaml +23 -0
- package/recipes/typescript/tests/requests.json +18 -0
- package/recipes/typescript/urlcode.yaml +5 -0
- package/recipes/webhook-receiver/README.md +20 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +27 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +23 -0
- package/schemas/recipe.schema.json +139 -0
- package/schemas/urlcode.schema.json +659 -110
- package/skills/urlcode/SKILL.md +119 -0
- package/starters/default/.github/workflows/urlcode.yml +23 -0
- package/starters/default/.mcp.json +12 -0
- package/starters/default/AGENTS.md +79 -0
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -41
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -36
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -9
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -561
- package/docs/MANAGEMENT-SECURITY.md +0 -82
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Field checks the runtime does not do: the body limit and JSON syntax are
|
|
2
|
+
// enforced before this runs. The declared signal fires after a response with
|
|
3
|
+
// a fixed payload (route, method, status); the message itself never leaves
|
|
4
|
+
// the sandbox, so a form store or mailer must sit behind the granted hook.
|
|
5
|
+
const emailPattern = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
|
|
6
|
+
export default async function contact(request) {
|
|
7
|
+
const body = await request.json();
|
|
8
|
+
const errors = [];
|
|
9
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
10
|
+
return Response.json({accepted: false, errors: ['body must be a JSON object']}, {status: 422});
|
|
11
|
+
}
|
|
12
|
+
if (typeof body.name !== 'string' || body.name.trim().length === 0 || body.name.length > 100) errors.push('name: 1-100 characters');
|
|
13
|
+
if (typeof body.email !== 'string' || !emailPattern.test(body.email)) errors.push('email: a valid address');
|
|
14
|
+
if (typeof body.message !== 'string' || body.message.trim().length < 10 || body.message.length > 4000) errors.push('message: 10-4000 characters');
|
|
15
|
+
if (errors.length) return Response.json({accepted: false, errors}, {status: 422});
|
|
16
|
+
return Response.json({accepted: true}, {status: 202});
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
id: contact-form
|
|
2
|
+
description: Validate a JSON contact message in the sandbox and notify an operator-granted hook with a fixed signal after the response.
|
|
3
|
+
tags: [contact, form, message, email, post, json, validation, signal, notify, hook, egress, "422"]
|
|
4
|
+
complexity: intermediate
|
|
5
|
+
capabilities: [enabled, function, methods, request.body, response.headers, signals]
|
|
6
|
+
targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
|
|
7
|
+
routes: 1
|
|
8
|
+
services:
|
|
9
|
+
- name: hook endpoint
|
|
10
|
+
description: An HTTPS endpoint you own that receives the fixed signal payload (route, method, status); replace https://hooks.example.com/contact.
|
|
11
|
+
grants:
|
|
12
|
+
- kind: signals
|
|
13
|
+
description: A revision-pinned operator policy granting the hook origin, generated with `urlcode permissions` and passed as --policy outside the project.
|
|
14
|
+
inputs:
|
|
15
|
+
- name: signal url
|
|
16
|
+
file: urlcode.yaml
|
|
17
|
+
description: The hook that is notified after each response.
|
|
18
|
+
- name: field rules
|
|
19
|
+
file: functions/contact.mjs
|
|
20
|
+
description: The name, email and message limits.
|
|
21
|
+
files: [urlcode.yaml, functions/contact.mjs, tests/requests.json, README.md]
|
|
22
|
+
tests:
|
|
23
|
+
fixtures: tests/requests.json
|
|
24
|
+
commands:
|
|
25
|
+
- urlcode permissions --project . > /operator/contact-policy.json
|
|
26
|
+
- urlcode validate --local --project . --policy /operator/contact-policy.json
|
|
27
|
+
- urlcode test --project . --policy /operator/contact-policy.json
|
|
28
|
+
- urlcode audit --project . --expect-routes 1 --policy /operator/contact-policy.json
|
|
29
|
+
behavior:
|
|
30
|
+
- POST /contact with a valid name, email and message answers 202 {"accepted":true}
|
|
31
|
+
- invalid fields answer 422 with one error line per field; a non-object body answers 422
|
|
32
|
+
- form-encoded bodies answer 415 and GET answers 405
|
|
33
|
+
- the signal carries route, method and status only, best effort; the message itself never leaves the sandbox
|
|
@@ -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, static: 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, static: refused}
|
|
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, static: 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,29 @@
|
|
|
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
|
+
// `Response.redirect()`'s headers are immutable (per the Fetch standard a
|
|
4
|
+
// trusted route's real `Response` enforces this, unlike the sandbox's guest
|
|
5
|
+
// API), so build that branch's headers up front instead of mutating the
|
|
6
|
+
// result afterward.
|
|
7
|
+
function cookie(request, name) {
|
|
8
|
+
for (const part of (request.headers.get('cookie') || '').split(';')) {
|
|
9
|
+
const [key, ...rest] = part.trim().split('=');
|
|
10
|
+
if (key === name) return rest.join('=');
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export default async function bucket(request, context, next) {
|
|
15
|
+
let assigned = cookie(request, 'bucket');
|
|
16
|
+
const fresh = assigned !== 'a' && assigned !== 'b';
|
|
17
|
+
if (fresh) assigned = Math.random() < 0.5 ? 'a' : 'b';
|
|
18
|
+
context.state.bucket = assigned;
|
|
19
|
+
const setCookie = 'bucket=' + assigned + '; Path=/; Max-Age=604800; SameSite=Lax';
|
|
20
|
+
if (assigned === 'b') {
|
|
21
|
+
const headers = new Headers({ location: context.env.VARIANT_URL, vary: 'cookie' });
|
|
22
|
+
if (fresh) headers.append('set-cookie', setCookie);
|
|
23
|
+
return new Response(null, { status: 302, headers });
|
|
24
|
+
}
|
|
25
|
+
const response = await next();
|
|
26
|
+
response.headers.set('vary', 'cookie');
|
|
27
|
+
if (fresh) response.headers.append('set-cookie', setCookie);
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
@@ -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
|
+
}
|