@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,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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Language redirect from Accept-Language, like Next.js i18n middleware. Only
|
|
2
|
+
// languages listed in LOCALES are chosen; the native redirect is the default.
|
|
3
|
+
// `Response.redirect()`'s headers are immutable (a trusted route's real
|
|
4
|
+
// `Response` enforces the Fetch standard here, unlike the sandbox's guest
|
|
5
|
+
// API), so that branch builds its own `Response` with headers up front.
|
|
6
|
+
export default async function locale(request, context, next) {
|
|
7
|
+
const supported = (context.env.LOCALES || '').split(/\s+/).filter(Boolean);
|
|
8
|
+
const ranked = (request.headers.get('accept-language') || '').split(',').map((part, index) => {
|
|
9
|
+
const [tag, ...params] = part.trim().split(';');
|
|
10
|
+
const q = params.map(p => p.trim()).find(p => p.startsWith('q='));
|
|
11
|
+
return {lang: tag.trim().toLowerCase().split('-')[0], q: q ? Number(q.slice(2)) || 0 : 1, index};
|
|
12
|
+
}).filter(p => p.q > 0).sort((a, b) => b.q - a.q || a.index - b.index);
|
|
13
|
+
const chosen = ranked.find(p => supported.includes(p.lang))?.lang;
|
|
14
|
+
if (chosen && chosen !== supported[0]) {
|
|
15
|
+
return new Response(null, { status: 302, headers: { location: context.env.SITE + '/' + chosen + '/welcome', vary: 'accept-language' } });
|
|
16
|
+
}
|
|
17
|
+
const response = await next();
|
|
18
|
+
response.headers.set('vary', 'accept-language');
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// A kill switch: return 503 while MAINTENANCE is on, unless the caller presents
|
|
2
|
+
// the bypass token. Flip the binding and restart; no code change is needed.
|
|
3
|
+
export default async function maintenance(request, context, next) {
|
|
4
|
+
const active = context.env.MAINTENANCE === 'true';
|
|
5
|
+
const bypass = context.env.MAINTENANCE_BYPASS;
|
|
6
|
+
if (active && !(bypass && request.headers.get('x-maintenance-bypass') === bypass)) {
|
|
7
|
+
return new Response('Down for maintenance\n', {status: 503, headers: {'retry-after': '120', 'cache-control': 'no-store'}});
|
|
8
|
+
}
|
|
9
|
+
return next();
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Method override for clients that can only send POST (Express method-override).
|
|
2
|
+
// The route still declares every real method; only a bounded set may be tunneled.
|
|
3
|
+
const tunnelable = ['PUT', 'PATCH', 'DELETE'];
|
|
4
|
+
export async function override(request, context, next) {
|
|
5
|
+
const requested = (request.headers.get('x-http-method-override') || '').toUpperCase();
|
|
6
|
+
if (request.method === 'POST' && requested) {
|
|
7
|
+
if (!tunnelable.includes(requested)) {
|
|
8
|
+
return new Response('Method override not allowed\n', {status: 405, headers: {allow: tunnelable.join(', ')}});
|
|
9
|
+
}
|
|
10
|
+
context.state.method = requested;
|
|
11
|
+
} else {
|
|
12
|
+
context.state.method = request.method;
|
|
13
|
+
}
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Content negotiation in the spirit of Express res.format. The handler always
|
|
2
|
+
// produces JSON; this converts it to plain text when the client prefers that.
|
|
3
|
+
function preferences(accept) {
|
|
4
|
+
return (accept || '*/*').split(',').map((part, index) => {
|
|
5
|
+
const [type, ...params] = part.trim().split(';');
|
|
6
|
+
const q = params.map(p => p.trim()).find(p => p.startsWith('q='));
|
|
7
|
+
return {type: type.trim().toLowerCase(), q: q ? Number(q.slice(2)) || 0 : 1, index};
|
|
8
|
+
}).filter(p => p.q > 0).sort((a, b) => b.q - a.q || a.index - b.index);
|
|
9
|
+
}
|
|
10
|
+
export default async function negotiate(request, context, next) {
|
|
11
|
+
const ranked = preferences(request.headers.get('accept'));
|
|
12
|
+
const choice = ranked.find(p => ['application/json', 'text/plain', 'application/*', 'text/*', '*/*'].includes(p.type));
|
|
13
|
+
if (!choice) return new Response('Not acceptable: application/json or text/plain\n', {status: 406});
|
|
14
|
+
const response = await next();
|
|
15
|
+
response.headers.set('vary', 'accept');
|
|
16
|
+
if (!response.ok || !['text/plain', 'text/*'].includes(choice.type)) return response;
|
|
17
|
+
const value = await response.json();
|
|
18
|
+
const lines = Object.entries(value).map(([key, item]) => key + ': ' + String(item)).join('\n') + '\n';
|
|
19
|
+
return new Response(lines, {status: response.status, headers: {...Object.fromEntries(response.headers), 'content-type': 'text/plain; charset=utf-8'}});
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Hotlink protection for a download: the Referer host must be on the allowlist.
|
|
2
|
+
// Referers are advisory, so this deters casual embedding rather than securing anything.
|
|
3
|
+
function host(referer) {
|
|
4
|
+
const match = /^https?:\/\/([^/:?#]+)/i.exec(referer || '');
|
|
5
|
+
return match ? match[1].toLowerCase() : null;
|
|
6
|
+
}
|
|
7
|
+
export default async function referer(request, context, next) {
|
|
8
|
+
const allowed = (context.env.ALLOWED_REFERERS || '').split(/\s+/).filter(Boolean);
|
|
9
|
+
const from = host(request.headers.get('referer'));
|
|
10
|
+
if (from === null || !allowed.includes(from)) return new Response('Direct download links are not permitted\n', {status: 403});
|
|
11
|
+
return next();
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Correlation id and timing headers (Hono requestId/timing, Express response-time).
|
|
2
|
+
// The runtime already stamps its own x-request-id on every reply; this carries the
|
|
3
|
+
// caller's id instead so a client can match its logs to yours. The guest has no
|
|
4
|
+
// crypto, so a generated id is a trace label, never a secret.
|
|
5
|
+
const validId = /^[A-Za-z0-9._-]{1,64}$/;
|
|
6
|
+
export default async function correlation(request, context, next) {
|
|
7
|
+
const started = Date.now();
|
|
8
|
+
const incoming = request.headers.get('x-correlation-id');
|
|
9
|
+
context.state.correlationId = incoming !== null && validId.test(incoming)
|
|
10
|
+
? incoming
|
|
11
|
+
: started.toString(36) + '-' + Math.floor(Math.random() * 0xffffffff).toString(36);
|
|
12
|
+
const response = await next();
|
|
13
|
+
response.headers.set('x-correlation-id', context.state.correlationId);
|
|
14
|
+
response.headers.set('server-timing', 'app;dur=' + (Date.now() - started));
|
|
15
|
+
return response;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
URLCode guide
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
id: middleware
|
|
2
|
+
description: Fourteen reusable middleware patterns around fifteen routes, mirroring the cookbook's middleware modules.
|
|
3
|
+
tags: [middleware, auth, bearer, basic, cors, tracing, request-id, errors, etag, caching, maintenance, negotiation, locale, referer, method-override, envelope, validation, debug, bucket, experiment]
|
|
4
|
+
complexity: advanced
|
|
5
|
+
capabilities: [bindings, download, enabled, function, methods, middleware, parameters, redirect, request.body, respond]
|
|
6
|
+
targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
|
|
7
|
+
routes: 15
|
|
8
|
+
inputs:
|
|
9
|
+
- name: credentials
|
|
10
|
+
file: urlcode.yaml
|
|
11
|
+
description: The literal demo token and password bound as env values; bind real secrets through operator-granted secrets instead.
|
|
12
|
+
- name: allowlists
|
|
13
|
+
file: middleware/cors.mjs
|
|
14
|
+
description: Allowed browser origins here and the referers, locales and buckets in the sibling modules.
|
|
15
|
+
files:
|
|
16
|
+
- urlcode.yaml
|
|
17
|
+
- middleware/auth.mjs
|
|
18
|
+
- middleware/body.mjs
|
|
19
|
+
- middleware/bucket.mjs
|
|
20
|
+
- middleware/cors.mjs
|
|
21
|
+
- middleware/debug.mjs
|
|
22
|
+
- middleware/envelope.mjs
|
|
23
|
+
- middleware/errors.mjs
|
|
24
|
+
- middleware/etag.mjs
|
|
25
|
+
- middleware/locale.mjs
|
|
26
|
+
- middleware/maintenance.mjs
|
|
27
|
+
- middleware/methods.mjs
|
|
28
|
+
- middleware/negotiate.mjs
|
|
29
|
+
- middleware/referer.mjs
|
|
30
|
+
- middleware/request-id.mjs
|
|
31
|
+
- functions/catalog.mjs
|
|
32
|
+
- functions/fail.mjs
|
|
33
|
+
- functions/items.mjs
|
|
34
|
+
- functions/profile.mjs
|
|
35
|
+
- functions/resource.mjs
|
|
36
|
+
- functions/status.mjs
|
|
37
|
+
- public/guide.txt
|
|
38
|
+
- tests/requests.json
|
|
39
|
+
- README.md
|
|
40
|
+
tests:
|
|
41
|
+
fixtures: tests/requests.json
|
|
42
|
+
commands:
|
|
43
|
+
- urlcode validate --local --project .
|
|
44
|
+
- urlcode test --project .
|
|
45
|
+
- urlcode audit --project . --expect-routes 15
|
|
46
|
+
behavior:
|
|
47
|
+
- /api/private answers 401 without the bearer token and JSON with it
|
|
48
|
+
- OPTIONS /cors/data from an allowed origin answers 204 with CORS headers
|
|
49
|
+
- /maintenance answers 503; /fragile?fail=true answers a JSON error carrying the correlation id
|
|
50
|
+
- /versioned answers a weak ETag and 304 on a matching If-None-Match
|