@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
- package/.claude/skills/urlcode-operations/SKILL.md +108 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +195 -255
- package/ROADMAP.md +143 -15
- package/SECURITY.md +31 -9
- package/dist/BUILD-MANIFEST.json +72 -47
- package/dist/adapters.js +4 -23
- package/dist/agent-lists.js +1 -1
- package/dist/agents-guide.js +113 -0
- package/dist/authoring-files.js +60 -0
- package/dist/authoring.js +11 -1
- package/dist/aws.js +4 -3
- package/dist/build-cloudflare.js +11 -24
- package/dist/build-static.js +134 -0
- package/dist/bulk.js +37 -0
- package/dist/capabilities.js +262 -0
- package/dist/capability-query.js +71 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +165 -34
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +9 -17
- package/dist/compliance-rules/privacy.js +7 -18
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/compliance.js +6 -8
- package/dist/conditions.js +88 -0
- package/dist/config.js +69 -6
- package/dist/context.js +155 -0
- package/dist/ecosystem-cli.js +88 -0
- package/dist/egress.js +98 -0
- package/dist/examples.js +92 -0
- package/dist/explain-cli.js +64 -0
- package/dist/explain.js +131 -0
- package/dist/extensions.js +231 -0
- package/dist/function-sources.js +49 -5
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +40 -6
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/manifest.js +109 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +7 -21
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/cache.js +8 -3
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +56 -15
- package/dist/prerender.js +100 -41
- package/dist/project-tests.js +3 -3
- package/dist/provider-verification.js +92 -0
- package/dist/proxy.js +44 -0
- package/dist/readiness.js +34 -11
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +45 -7
- package/dist/runtime.js +164 -64
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/schema-query.js +62 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -0
- package/dist/trusted-functions.js +210 -0
- package/dist/types/adapters.d.ts +7 -4
- package/dist/types/agent-lists.d.ts +0 -1
- package/dist/types/agents-guide.d.ts +17 -0
- package/dist/types/authoring-files.d.ts +10 -0
- package/dist/types/aws.d.ts +3 -1
- package/dist/types/build-cloudflare.d.ts +1 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +64 -0
- package/dist/types/capability-query.d.ts +24 -0
- package/dist/types/catalog.d.ts +65 -0
- package/dist/types/client-address.d.ts +0 -1
- package/dist/types/compliance-rules/baseline.d.ts +1 -9
- package/dist/types/compliance-rules/privacy.d.ts +1 -4
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance-rules/strict.d.ts +0 -5
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +21 -2
- package/dist/types/context.d.ts +66 -0
- package/dist/types/ecosystem-cli.d.ts +17 -0
- package/dist/types/egress.d.ts +46 -0
- package/dist/types/examples.d.ts +50 -0
- package/dist/types/explain-cli.d.ts +11 -0
- package/dist/types/explain.d.ts +95 -0
- package/dist/types/extensions.d.ts +177 -0
- package/dist/types/function-sources.d.ts +9 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +36 -6
- package/dist/types/init-with.d.ts +30 -0
- package/dist/types/interchange-cli.d.ts +16 -0
- package/dist/types/interchange.d.ts +42 -0
- package/dist/types/manifest.d.ts +79 -0
- package/dist/types/match.d.ts +1 -0
- package/dist/types/mcp-authoring.d.ts +92 -0
- package/dist/types/mcp.d.ts +12 -0
- package/dist/types/observability.d.ts +3 -14
- package/dist/types/operator-host.d.ts +8 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/policies/agents.d.ts +0 -2
- package/dist/types/policies/compression.d.ts +2 -0
- package/dist/types/policies/security.d.ts +0 -1
- package/dist/types/policy.d.ts +15 -4
- package/dist/types/project-tests.d.ts +3 -2
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +10 -3
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +11 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/schema-query.d.ts +12 -0
- package/dist/types/server.d.ts +1 -4
- package/dist/types/signals.d.ts +25 -0
- package/dist/types/site.d.ts +0 -1
- package/dist/types/tooling.d.ts +115 -0
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +71 -7
- package/dist/types/typescript-authoring.d.ts +12 -0
- package/dist/types/vercel.d.ts +3 -1
- package/dist/types/verify-deployment.d.ts +47 -0
- package/dist/types.js +37 -5
- package/dist/typescript-authoring.js +142 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +232 -15
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +3 -2
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +192 -0
- package/docs/CAPACITY.md +129 -32
- package/docs/CI.md +142 -0
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/COMPLIANCE.md +6 -9
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +398 -0
- package/docs/FRAMEWORK.md +198 -0
- package/docs/FUNCTION-SECURITY.md +129 -32
- package/docs/INSTALL.md +45 -12
- package/docs/INTERCHANGE.md +134 -0
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +29 -16
- package/docs/MONITORING.md +2 -19
- package/docs/NEXT-PHASE-PLAN.md +98 -0
- package/docs/NEXT-STEPS.md +634 -0
- package/docs/OBSERVABILITY.md +11 -18
- package/docs/OPEN-DECISIONS.md +212 -0
- package/docs/OPERATIONAL-PROOF.md +30 -31
- package/docs/OPERATIONS.md +29 -35
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +23 -309
- package/docs/PRERENDER.md +41 -1
- package/docs/PROJECT-DIRECTION.md +75 -8
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +87 -34
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +57 -35
- package/docs/RELEASE-SECURITY.md +116 -7
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +8 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +41 -1
- package/docs/SPECIFICATION.md +150 -29
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
- package/docs/SPIKE-CORE-LAYERING.md +337 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +6 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
- package/docs/SPIKE-MONOREPO.md +322 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/STATIC.md +94 -0
- package/docs/TOOLING.md +295 -0
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +82 -0
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/USABILITY-REVIEW.md +129 -0
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +205 -0
- package/docs/YAML-GUIDE.md +15 -479
- package/docs/YAML-REFERENCE.md +143 -22
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/contract.md +52 -0
- package/docs/policies/hardened.md +56 -0
- package/docs/policies/interoperability.md +169 -0
- package/docs/policies/operations.md +45 -0
- package/docs/yaml/assets.md +36 -0
- package/docs/yaml/conditions.md +20 -0
- package/docs/yaml/functions.md +160 -0
- package/docs/yaml/middleware.md +29 -0
- package/docs/yaml/organization.md +74 -0
- package/docs/yaml/policies.md +37 -0
- package/docs/yaml/redirects.md +64 -0
- package/docs/yaml/responses.md +57 -0
- package/docs/yaml/site.md +24 -0
- package/examples/assets/example.yaml +17 -0
- package/examples/aws/example.yaml +20 -0
- package/examples/cloudflare/example.yaml +19 -0
- package/examples/compliance/example.yaml +11 -0
- package/examples/conditions/README.md +12 -0
- package/examples/conditions/example.yaml +19 -0
- package/examples/conditions/tests/requests.json +13 -0
- package/examples/conditions/urlcode.yaml +24 -0
- package/examples/cookbook/README.md +8 -4
- package/examples/cookbook/example.yaml +17 -0
- package/examples/cookbook/functions/catalog.mjs +3 -0
- package/examples/cookbook/functions/fail.mjs +4 -0
- package/examples/cookbook/functions/items.mjs +3 -0
- package/examples/cookbook/functions/profile.mjs +3 -0
- package/examples/cookbook/functions/resource.mjs +3 -0
- package/examples/cookbook/functions/status.mjs +3 -0
- package/examples/cookbook/middleware/auth.mjs +48 -0
- package/examples/cookbook/middleware/body.mjs +15 -0
- package/examples/cookbook/middleware/bucket.mjs +29 -0
- package/examples/cookbook/middleware/cors.mjs +21 -0
- package/examples/cookbook/middleware/debug.mjs +13 -0
- package/examples/cookbook/middleware/envelope.mjs +11 -0
- package/examples/cookbook/middleware/errors.mjs +11 -0
- package/examples/cookbook/middleware/etag.mjs +18 -0
- package/examples/cookbook/middleware/locale.mjs +20 -0
- package/examples/cookbook/middleware/maintenance.mjs +10 -0
- package/examples/cookbook/middleware/methods.mjs +15 -0
- package/examples/cookbook/middleware/negotiate.mjs +20 -0
- package/examples/cookbook/middleware/referer.mjs +12 -0
- package/examples/cookbook/middleware/request-id.mjs +16 -0
- package/examples/cookbook/route-index.json +676 -0
- package/examples/cookbook/routes/middleware.yaml +126 -0
- package/examples/cookbook/tests/requests.json +526 -0
- package/examples/cookbook/urlcode.yaml +1 -0
- package/examples/egress/README.md +22 -0
- package/examples/egress/example.yaml +19 -0
- package/examples/egress/urlcode.yaml +19 -0
- package/examples/extensions/README.md +7 -0
- package/examples/extensions/example.yaml +21 -0
- package/examples/extensions/urlcode.yaml +25 -0
- package/examples/monitoring/example.yaml +8 -0
- package/examples/prerender/README.md +2 -2
- package/examples/prerender/example.yaml +16 -0
- package/examples/provider-conformance/README.md +12 -0
- package/examples/provider-conformance/example.yaml +14 -0
- package/examples/provider-conformance/urlcode.yaml +34 -0
- package/examples/tunnel/example.yaml +8 -0
- package/examples/vercel/example.yaml +19 -0
- package/llms-full.txt +3084 -0
- package/llms.txt +61 -21
- package/package.json +36 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
- package/recipes/authenticated-json-api/README.md +51 -0
- package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
- package/recipes/authenticated-json-api/recipe.yaml +34 -0
- package/recipes/authenticated-json-api/tests/requests.json +39 -0
- package/recipes/authenticated-json-api/urlcode.yaml +12 -0
- package/recipes/contact-form/README.md +25 -0
- package/recipes/contact-form/functions/contact.mjs +17 -0
- package/recipes/contact-form/recipe.yaml +33 -0
- package/recipes/contact-form/tests/requests.json +47 -0
- package/recipes/contact-form/urlcode.yaml +18 -0
- package/recipes/cors-api/README.md +16 -0
- package/recipes/cors-api/functions/items.mjs +3 -0
- package/recipes/cors-api/middleware/cors.mjs +21 -0
- package/recipes/cors-api/recipe.yaml +26 -0
- package/recipes/cors-api/tests/requests.json +65 -0
- package/recipes/cors-api/urlcode.yaml +12 -0
- package/recipes/health-page/README.md +13 -0
- package/recipes/health-page/recipe.yaml +23 -0
- package/recipes/health-page/tests/requests.json +36 -0
- package/recipes/health-page/urlcode.yaml +19 -0
- package/recipes/json-api/README.md +6 -0
- package/recipes/json-api/functions/echo.mjs +3 -0
- package/recipes/json-api/recipe.yaml +25 -0
- package/recipes/json-api/tests/requests.json +34 -0
- package/recipes/json-api/urlcode.yaml +12 -0
- package/recipes/middleware/README.md +34 -0
- package/recipes/middleware/functions/catalog.mjs +3 -0
- package/recipes/middleware/functions/fail.mjs +4 -0
- package/recipes/middleware/functions/items.mjs +3 -0
- package/recipes/middleware/functions/profile.mjs +3 -0
- package/recipes/middleware/functions/resource.mjs +3 -0
- package/recipes/middleware/functions/status.mjs +3 -0
- package/recipes/middleware/middleware/auth.mjs +48 -0
- package/recipes/middleware/middleware/body.mjs +15 -0
- package/recipes/middleware/middleware/bucket.mjs +29 -0
- package/recipes/middleware/middleware/cors.mjs +21 -0
- package/recipes/middleware/middleware/debug.mjs +13 -0
- package/recipes/middleware/middleware/envelope.mjs +11 -0
- package/recipes/middleware/middleware/errors.mjs +11 -0
- package/recipes/middleware/middleware/etag.mjs +18 -0
- package/recipes/middleware/middleware/locale.mjs +20 -0
- package/recipes/middleware/middleware/maintenance.mjs +10 -0
- package/recipes/middleware/middleware/methods.mjs +15 -0
- package/recipes/middleware/middleware/negotiate.mjs +20 -0
- package/recipes/middleware/middleware/referer.mjs +12 -0
- package/recipes/middleware/middleware/request-id.mjs +16 -0
- package/recipes/middleware/public/guide.txt +1 -0
- package/recipes/middleware/recipe.yaml +50 -0
- package/recipes/middleware/tests/requests.json +528 -0
- package/recipes/middleware/urlcode.yaml +127 -0
- package/recipes/protected-download/README.md +22 -0
- package/recipes/protected-download/files/report.txt +1 -0
- package/recipes/protected-download/recipe.yaml +31 -0
- package/recipes/protected-download/tests/requests.json +32 -0
- package/recipes/protected-download/urlcode.yaml +15 -0
- package/recipes/redirect/README.md +7 -0
- package/recipes/redirect/recipe.yaml +25 -0
- package/recipes/redirect/tests/requests.json +19 -0
- package/recipes/redirect/urlcode.yaml +9 -0
- package/recipes/static-plus-api/README.md +15 -0
- package/recipes/static-plus-api/functions/info.mjs +3 -0
- package/recipes/static-plus-api/public/assets/index.html +3 -0
- package/recipes/static-plus-api/public/assets/site.css +1 -0
- package/recipes/static-plus-api/public/index.html +8 -0
- package/recipes/static-plus-api/recipe.yaml +29 -0
- package/recipes/static-plus-api/tests/requests.json +56 -0
- package/recipes/static-plus-api/urlcode.yaml +23 -0
- package/recipes/typescript/README.md +8 -0
- package/recipes/typescript/functions/hello.ts +5 -0
- package/recipes/typescript/recipe.yaml +23 -0
- package/recipes/typescript/tests/requests.json +18 -0
- package/recipes/typescript/urlcode.yaml +5 -0
- package/recipes/webhook-receiver/README.md +20 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +27 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +23 -0
- package/schemas/recipe.schema.json +139 -0
- package/schemas/urlcode.schema.json +659 -110
- package/skills/urlcode/SKILL.md +119 -0
- package/starters/default/.github/workflows/urlcode.yml +23 -0
- package/starters/default/.mcp.json +12 -0
- package/starters/default/AGENTS.md +79 -0
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -41
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -36
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -9
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -561
- package/docs/MANAGEMENT-SECURITY.md +0 -82
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EgressError, egressUrl, safeEgressHeaders } from './egress.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const sensitive=new Set(['authorization','cookie','set-cookie','proxy-authorization','forwarded','x-forwarded-for','x-forwarded-host','x-forwarded-proto']);
|
|
7
|
+
export function validateProxy(definition ) {
|
|
8
|
+
if(!definition||typeof definition!=='object'||Object.keys(definition).some(key=>!['url','query','requestHeaders','responseHeaders','headers'].includes(key))) throw new EgressError('denied');
|
|
9
|
+
const parsed=egressUrl(definition.url);
|
|
10
|
+
if(/[{}]/.test(parsed.host)||(/[{}]/.test(parsed.search)||/%7[bd]/i.test(parsed.search))||/%7[bd]/i.test(parsed.host)) throw new EgressError('denied');
|
|
11
|
+
if(/%7[bd]/i.test(parsed.pathname.replace(/%7B[A-Za-z_][A-Za-z0-9_]*%7D/gi,''))) throw new EgressError('denied');
|
|
12
|
+
if(definition.url.length>8192) throw new EgressError('limit');
|
|
13
|
+
for(const list of [definition.query,definition.requestHeaders,definition.responseHeaders]) if(list&&(!Array.isArray(list)||list.length>32||list.some(value=>typeof value!=='string'||!value||value.length>128))) throw new EgressError('denied');
|
|
14
|
+
for(const name of [...definition.requestHeaders||[],...definition.responseHeaders||[]]) {if(sensitive.has(name.toLowerCase())||/^x-forwarded-/i.test(name)) throw new EgressError('denied');safeEgressHeaders({[name]:'x'});}
|
|
15
|
+
safeEgressHeaders(definition.headers||{});
|
|
16
|
+
}
|
|
17
|
+
export async function executeProxy(client ,definition ,input ) {
|
|
18
|
+
validateProxy(definition);
|
|
19
|
+
const url=egressUrl(definition.url); const origin=url.origin;
|
|
20
|
+
// URL serializes braces in path unchanged. Arguments can never affect origin/query.
|
|
21
|
+
url.pathname=url.pathname.replace(/%7B([A-Za-z_][A-Za-z0-9_]*)%7D/gi,(_all,name )=> {
|
|
22
|
+
const value=input.params[name]; if(typeof value!=='string'||value==='.'||value==='..') throw new EgressError('denied'); return encodeURIComponent(value);
|
|
23
|
+
});
|
|
24
|
+
if((/[{}]/.test(url.pathname))||url.origin!==origin) throw new EgressError('denied');
|
|
25
|
+
const incoming=new URL(input.url); for(const name of definition.query||[]) for(const value of incoming.searchParams.getAll(name)) url.searchParams.append(name,value);
|
|
26
|
+
if(url.href.length>8192) throw new EgressError('limit');
|
|
27
|
+
const headers =Object.create(null);
|
|
28
|
+
const requestHop=new Set((input.headers.connection||'').toLowerCase().split(',').map(name=>name.trim()));
|
|
29
|
+
for(const name of definition.requestHeaders||[]) {const value=input.headers[name.toLowerCase()]; if(value!==undefined&&!requestHop.has(name.toLowerCase())) headers[name.toLowerCase()]=value;}
|
|
30
|
+
Object.assign(headers,definition.headers||{});
|
|
31
|
+
const safeHeaders=safeEgressHeaders(headers);
|
|
32
|
+
if(input.body?.byteLength){
|
|
33
|
+
const coding=(value )=>value?.split(',').map(part=>part.trim().toLowerCase()).join(',')||'identity';
|
|
34
|
+
const incomingCoding=coding(input.headers['content-encoding']);
|
|
35
|
+
if(incomingCoding!==coding(safeHeaders['content-encoding'])||(incomingCoding!=='identity'&&!(definition.requestHeaders||[]).some(name=>name.toLowerCase()==='content-encoding')))throw new EgressError('denied');
|
|
36
|
+
}
|
|
37
|
+
const result=await client.request({url:url.href,method:input.method,headers:safeHeaders,...(input.body?{body:input.body}:{}),...(input.signal?{signal:input.signal}:{})});
|
|
38
|
+
if(result.headers['content-encoding']&&result.headers['content-encoding']!=='identity'&&!(definition.responseHeaders||[]).some(name=>name.toLowerCase()==='content-encoding'))throw new EgressError('denied');
|
|
39
|
+
const selected =Object.create(null);
|
|
40
|
+
const responseHop=new Set((result.headers.connection||'').toLowerCase().split(',').map(name=>name.trim()));
|
|
41
|
+
for(const name of definition.responseHeaders||[]) {const value=result.headers[name.toLowerCase()]; if(value!==undefined&&!responseHop.has(name.toLowerCase())) selected[name.toLowerCase()]=value;}
|
|
42
|
+
if(result.headers['content-encoding']&&result.headers['content-encoding']!=='identity'&&!selected['content-encoding'])throw new EgressError('denied');
|
|
43
|
+
return {...result,headers:safeEgressHeaders(selected)};
|
|
44
|
+
}
|
package/dist/readiness.js
CHANGED
|
@@ -13,9 +13,14 @@ import { runCompliance } from './compliance.js';
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
/** One configured route as the inventory reports it: a PlanInventoryEntry with the handler kind named. */
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
/** One request case: a generated probe or a `tests/requests.json` fixture. */
|
|
20
25
|
|
|
21
26
|
|
|
@@ -25,14 +30,17 @@ import { runCompliance } from './compliance.js';
|
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
/** A started server as the audit and benchmark see it. structural: the real type is startServer's result in src/server.ts. */
|
|
28
|
-
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
36
44
|
|
|
37
45
|
|
|
38
46
|
|
|
@@ -41,29 +49,43 @@ import { runCompliance } from './compliance.js';
|
|
|
41
49
|
|
|
42
50
|
|
|
43
51
|
|
|
44
|
-
const handlers = ['redirect','function','page','static','download','respond'
|
|
52
|
+
const handlers = ['extension','proxy','conditional','redirect','function','page','static','download','respond'] ;
|
|
45
53
|
/** Narrows a compiled route to one that redirects, so redirectLocation can read its spec. */
|
|
46
54
|
export const hasRedirect = (route ) => Boolean(route.redirect);
|
|
47
55
|
const isRecord = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
48
56
|
// Probes identify themselves so an agents policy that denies an empty
|
|
49
57
|
// User-Agent does not fail every generated case; fixtures may override it.
|
|
50
58
|
export const probeAgent = 'Mozilla/5.0 (compatible; RouteProbe/0.1)';
|
|
59
|
+
// Advisory only (docs/AI-AUTHORING.md, "Deciding when a route needs sandbox: true"):
|
|
60
|
+
// a route that runs project code, accepts POST with a declared request.body
|
|
61
|
+
// policy, and declares neither `sandbox: true` nor `sandboxReason` looks
|
|
62
|
+
// plausibly webhook/callback/third-party-input-shaped. This is a nudge to
|
|
63
|
+
// look, never an inferred verdict — it never fails `audit` or changes `ready`.
|
|
64
|
+
function routeAdvisories(route ) {
|
|
65
|
+
const advisories = [];
|
|
66
|
+
const runsCode = Boolean(route.function) || Boolean(route.middleware?.length);
|
|
67
|
+
if (runsCode && route.methods.includes('POST') && route.request?.body && !route.sandbox && !route.sandboxReason) {
|
|
68
|
+
advisories.push("This route accepts POST with a declared request.body policy but declares neither sandbox: true nor sandboxReason; consider whether this route needs sandbox: true.");
|
|
69
|
+
}
|
|
70
|
+
return advisories;
|
|
71
|
+
}
|
|
51
72
|
export function projectPlan(compiled ) {
|
|
52
73
|
const routes = [...compiled.exact.values(), ...[...compiled.byLength.values()].flat(), ...compiled.mounts];
|
|
53
74
|
const now = Date.now();
|
|
54
|
-
const inventory = routes.map(route => ({ path:route.pattern, handler:handlers.find(key => route[key]), methods:route.methods, middleware:route.middleware?.length || 0,
|
|
55
|
-
policies:route.policy ? Object.keys(route.policy.describe) : [],
|
|
75
|
+
const inventory = routes.map(route => { const advisories = routeAdvisories(route); return { path:route.pattern, handler:handlers.find(key => route[key]), methods:route.methods, middleware:route.middleware?.length || 0,
|
|
76
|
+
policies:[...(route.policy ? Object.keys(route.policy.describe) : []),...(route.extensionPolicyNames??[]).map(name=>`extensions.${name}`)],
|
|
56
77
|
...(route.generated ? { generated:route.generated } : {}),
|
|
57
|
-
|
|
78
|
+
...(advisories.length ? { advisories } : {}),
|
|
79
|
+
state:route.enabled === false ? 'disabled' : route.expiresAt && now >= route.expiresAt ? 'expired' : 'active' }; });
|
|
58
80
|
const cases = [];
|
|
59
81
|
for (const [i,route] of routes.entries()) {
|
|
60
82
|
const entry = inventory[i];
|
|
61
83
|
if (!entry) continue;
|
|
62
84
|
if (entry.state !== 'active') {
|
|
63
|
-
if(!route.names.length && !route.static) cases.push({path:route.pattern,method:'GET',status:entry.state==='disabled'?404:410});
|
|
85
|
+
if(!route.names.length && !route.static && !route.extension && !route.extensionPolicyNames?.length) cases.push({path:route.pattern,method:'GET',status:entry.state==='disabled'?404:410});
|
|
64
86
|
continue;
|
|
65
87
|
}
|
|
66
|
-
if (route.
|
|
88
|
+
if (route.extension || route.extensionPolicyNames?.length || route.proxy || route.signals?.length || route.match || route.conditional || route.function || route.middleware?.length || route.names.length) continue;
|
|
67
89
|
// Required inputs need intentional fixtures; never invent business data.
|
|
68
90
|
let context ;
|
|
69
91
|
try { context = contextFor(route,{},new URLSearchParams(),new Headers()); } catch { continue; }
|
|
@@ -160,9 +182,10 @@ export async function auditProject(app , {expectRoutes,log=()=>{},c
|
|
|
160
182
|
const counts ={configured:plan.inventory.length,active:0,disabled:0,expired:0,byHandler:{}};
|
|
161
183
|
for(const route of plan.inventory){counts[route.state]++;const handler=String(route.handler);counts.byHandler[handler]=(counts.byHandler[handler]||0)+1;}
|
|
162
184
|
const countMatches=expectRoutes===undefined || counts.configured===expectRoutes;
|
|
185
|
+
const advisories=plan.inventory.flatMap(route=>(route.advisories??[]).map(message=>({route:route.path,message})));
|
|
163
186
|
// The per-route capability table: which policies apply and whether this
|
|
164
187
|
// host enforces, compiles or delegates each one. Refusals never get here.
|
|
165
|
-
return {
|
|
188
|
+
return {elapsedMs:performance.now()-began,ready:countMatches && !failed && !uncovered.length && counts.active>0,counts,expectedRoutes:expectRoutes ?? null,countMatches,checks:cases.length,passed,failed,coveredRouteMethods:covered.size,unassertedCases,uncovered,policies:plan.policies ?? {},compliance:compliance?await runCompliance(app,compliance):null,advisories};
|
|
166
189
|
}
|
|
167
190
|
export async function benchmarkProject(app ,{requests=1000,concurrency=2,maxP95Ms,seconds=30,warmup=0,target} ={}) {
|
|
168
191
|
assert(Number.isInteger(requests)&&requests>=1&&requests<=100000,'Requests must be 1–100000');
|
package/dist/recipes.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {fileURLToPath} from 'node:url';
|
|
2
|
+
import {readAuthoringFile,publishAuthoringProject} from './authoring-files.js';
|
|
3
|
+
import {validateDocument,parseYaml} from './config.js';
|
|
4
|
+
import {readMetadata,searchMetadata} from './catalog.js';
|
|
5
|
+
|
|
6
|
+
import {assert} from './errors.js';
|
|
7
|
+
|
|
8
|
+
/** recipe.yaml, validated against schemas/recipe.schema.json; `name` repeats `id` and `files` is the copy list. */
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// This fixed local catalog is trusted package data, never an executable registry:
|
|
14
|
+
// names come from here, metadata from each recipe's schema-checked recipe.yaml.
|
|
15
|
+
export const recipeNames=['redirect','json-api','typescript','middleware','health-page','static-plus-api','cors-api','webhook-receiver','contact-form','authenticated-json-api','protected-download'] ;
|
|
16
|
+
const recipesRoot=fileURLToPath(new URL('../recipes/',import.meta.url));
|
|
17
|
+
const root=(name )=>recipesRoot+name+'/';
|
|
18
|
+
async function metadata(name ) {const value=await readMetadata(root(name),name,'recipe.yaml');return {name,...value};}
|
|
19
|
+
export async function listRecipes() {
|
|
20
|
+
const result =[];
|
|
21
|
+
for(const name of recipeNames)result.push(await metadata(name));
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function flatten(hits ) {return hits.map(hit=>({...hit.entry,score:hit.score,matched:hit.matched}));}
|
|
25
|
+
/** Matches id, description, tags and capabilities locally; no service is consulted. */
|
|
26
|
+
export async function searchRecipes(text ) {
|
|
27
|
+
const hits=searchMetadata(await listRecipes(),text);
|
|
28
|
+
return {query:text,count:hits.length,results:flatten(hits)};
|
|
29
|
+
}
|
|
30
|
+
export async function showRecipe(name ) {
|
|
31
|
+
assert((recipeNames ).includes(name),'Unknown local recipe');
|
|
32
|
+
const summary=await metadata(name),content =Object.create(null) ;
|
|
33
|
+
for(const path of summary.files)content[path]=(await readAuthoringFile(root(name),path,1048576)).toString('utf8');
|
|
34
|
+
assert(content['urlcode.yaml'],'Recipe must copy urlcode.yaml');
|
|
35
|
+
validateDocument(parseYaml(content['urlcode.yaml']));
|
|
36
|
+
return {...summary,content};
|
|
37
|
+
}
|
|
38
|
+
export async function addRecipe(name ,output ,{dryRun=false} ={}) {
|
|
39
|
+
const recipe=await showRecipe(name);
|
|
40
|
+
return {name,output:await publishAuthoringProject(output,new Map(Object.entries(recipe.content)),dryRun),dryRun,files:[...recipe.files]};
|
|
41
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Route-inventory diff: compares two `urlcode routes` reports and reports what
|
|
2
|
+
// was added, removed or changed. Pure and generic: it knows nothing about
|
|
3
|
+
// pull requests; the GitHub action renders its Markdown into a PR comment.
|
|
4
|
+
import { ConfigError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** One route as the diff sees it: the inventory entry plus its policy description from the `policies` map. */
|
|
8
|
+
|
|
9
|
+
/** The parts of a `urlcode routes` report the diff reads. */
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const states = ['active','disabled','expired'];
|
|
15
|
+
const isRecord = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
const isStrings = (value ) => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
17
|
+
function entry(value , index ) {
|
|
18
|
+
const fail = (what ) => { throw new ConfigError(`Route report inventory[${index}] ${what}`); };
|
|
19
|
+
if (!isRecord(value)) return fail('is not an object');
|
|
20
|
+
const { path, handler, methods, middleware, policies, generated, state } = value;
|
|
21
|
+
if (typeof path !== 'string' || !path) return fail('needs a path');
|
|
22
|
+
if (handler !== undefined && typeof handler !== 'string') return fail('has an invalid handler');
|
|
23
|
+
if (!isStrings(methods)) return fail('needs methods');
|
|
24
|
+
if (typeof middleware !== 'number' || !Number.isInteger(middleware) || middleware < 0) return fail('needs a middleware count');
|
|
25
|
+
if (!isStrings(policies)) return fail('needs policies');
|
|
26
|
+
if (generated !== undefined && typeof generated !== 'string') return fail('has an invalid generated marker');
|
|
27
|
+
if (typeof state !== 'string' || !states.includes(state )) return fail('needs a state');
|
|
28
|
+
return { path, handler, methods, middleware, policies, ...(generated !== undefined ? { generated } : {}), state:state };
|
|
29
|
+
}
|
|
30
|
+
/** Validates a parsed `urlcode routes` JSON report (a child-process or file boundary) into a snapshot. */
|
|
31
|
+
export function parseRouteSnapshot(value ) {
|
|
32
|
+
if (!isRecord(value) || !Array.isArray(value.inventory)) throw new ConfigError('Route report needs an inventory array');
|
|
33
|
+
const inventory = value.inventory.map(entry);
|
|
34
|
+
const policies = {};
|
|
35
|
+
if (value.policies !== undefined) {
|
|
36
|
+
if (!isRecord(value.policies)) throw new ConfigError('Route report policies must be an object');
|
|
37
|
+
for (const [path, description] of Object.entries(value.policies)) {
|
|
38
|
+
if (!isRecord(description)) throw new ConfigError(`Route report policies[${path}] must be an object`);
|
|
39
|
+
policies[path] = description;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { inventory, policies };
|
|
43
|
+
}
|
|
44
|
+
// Key order in a report is not a change: compare canonical JSON.
|
|
45
|
+
function canonical(value ) {
|
|
46
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
47
|
+
if (isRecord(value)) return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
48
|
+
return JSON.stringify(value) ?? 'null';
|
|
49
|
+
}
|
|
50
|
+
function records(snapshot ) {
|
|
51
|
+
const map = new Map ();
|
|
52
|
+
for (const item of snapshot.inventory) {
|
|
53
|
+
if (map.has(item.path)) throw new ConfigError(`Route report lists ${item.path} twice`);
|
|
54
|
+
const policy = snapshot.policies?.[item.path];
|
|
55
|
+
map.set(item.path, policy ? { ...item, policy } : { ...item });
|
|
56
|
+
}
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
59
|
+
/** Compares two route reports. Paths are sorted; an entry changes when any inventory field or its policy description differs. */
|
|
60
|
+
export function diffRoutes(before , after ) {
|
|
61
|
+
const previous = records(before), current = records(after);
|
|
62
|
+
const paths = [...new Set([...previous.keys(), ...current.keys()])].sort();
|
|
63
|
+
const diff = { added:[], removed:[], changed:[] };
|
|
64
|
+
for (const path of paths) {
|
|
65
|
+
const old = previous.get(path), now = current.get(path);
|
|
66
|
+
if (old && !now) diff.removed.push(old);
|
|
67
|
+
else if (now && !old) diff.added.push(now);
|
|
68
|
+
else if (old && now && canonical(old) !== canonical(now)) diff.changed.push({ path, before:old, after:now });
|
|
69
|
+
}
|
|
70
|
+
return diff;
|
|
71
|
+
}
|
|
72
|
+
export const hasRouteChanges = (diff ) => diff.added.length + diff.removed.length + diff.changed.length > 0;
|
|
73
|
+
|
|
74
|
+
const fields = ['handler','methods','state','middleware','policies','generated','policy'] ;
|
|
75
|
+
|
|
76
|
+
const cell = (value ) => {
|
|
77
|
+
const text = value === undefined ? '' : Array.isArray(value) && value.every(item => typeof item === 'string') ? value.join(', ') : typeof value === 'string' ? value : canonical(value);
|
|
78
|
+
// Backslashes first, so an escape this adds is never itself re-escaped.
|
|
79
|
+
return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ') || '-';
|
|
80
|
+
};
|
|
81
|
+
const code = (value ) => `\`${value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\|/g, '\\|')}\``;
|
|
82
|
+
function table(headers , rows ) {
|
|
83
|
+
return [`| ${headers.join(' | ')} |`, `|${headers.map(() => '---').join('|')}|`, ...rows.map(row => `| ${row.join(' | ')} |`)];
|
|
84
|
+
}
|
|
85
|
+
const routeRow = (record ) => [code(record.path), cell(record.handler), cell(record.methods), cell(record.state), String(record.middleware), cell(record.policies), cell(record.generated)];
|
|
86
|
+
/** Renders a diff as Markdown: one table per nonempty section, or "No route changes". */
|
|
87
|
+
export function renderRouteDiff(diff ) {
|
|
88
|
+
if (!hasRouteChanges(diff)) return 'No route changes\n';
|
|
89
|
+
const lines = [];
|
|
90
|
+
const routeHeaders = ['Route','Handler','Methods','State','Middleware','Policies','Generated'];
|
|
91
|
+
const section = (title , list ) => {
|
|
92
|
+
if (!list.length) return;
|
|
93
|
+
lines.push(`### ${title} (${list.length})`, '', ...table(routeHeaders, list.map(routeRow)), '');
|
|
94
|
+
};
|
|
95
|
+
section('Added routes', diff.added);
|
|
96
|
+
section('Removed routes', diff.removed);
|
|
97
|
+
if (diff.changed.length) {
|
|
98
|
+
const rows = [];
|
|
99
|
+
for (const change of diff.changed) {
|
|
100
|
+
const differing = fields.filter((field ) => canonical(change.before[field]) !== canonical(change.after[field]));
|
|
101
|
+
for (const field of differing) rows.push([code(change.path), field, cell(change.before[field]), cell(change.after[field])]);
|
|
102
|
+
}
|
|
103
|
+
lines.push(`### Changed routes (${diff.changed.length})`, '', ...table(['Route','Field','Before','After'], rows), '');
|
|
104
|
+
}
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
package/dist/router.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { effectiveExtensionPolicies, isSensitiveExtensionPolicy } from './extensions.js';
|
|
2
|
+
|
|
3
|
+
import { validateProxy } from './proxy.js';
|
|
4
|
+
import { validateSignal } from './signals.js';
|
|
5
|
+
|
|
6
|
+
import { normalizeMatch, assertDisjointMatches } from './conditions.js';
|
|
7
|
+
import { effectivePolicies } from './policies.js';
|
|
1
8
|
import { setImmediate as yieldTurn } from 'node:timers/promises';
|
|
2
9
|
import { compileHttp } from './http-policy.js';
|
|
3
10
|
import Ajv from 'ajv/dist/2020.js';
|
|
@@ -51,7 +58,7 @@ function compiledRedirect(redirect ) {
|
|
|
51
58
|
const { pass: _pass, ...query } = redirect.query;
|
|
52
59
|
return { ...redirect, query };
|
|
53
60
|
}
|
|
54
|
-
export async function compileRoutes(loaded , bindings , permissions = {}, projectSha256 ) {
|
|
61
|
+
export async function compileRoutes(loaded , bindings , permissions = {}, projectSha256 , extensions ) {
|
|
55
62
|
const deadline=performance.now()+10000;
|
|
56
63
|
let processed=0;
|
|
57
64
|
const exact = new Map (), dynamic = [], mounts = [], modules = new Map ();
|
|
@@ -63,7 +70,7 @@ export async function compileRoutes(loaded , bindings
|
|
|
63
70
|
const parts = segments(pattern);
|
|
64
71
|
assert(!pattern.startsWith('/_urlcode'), 'The /_urlcode prefix is reserved for runtime operations');
|
|
65
72
|
const names = parts.map(parameterName).filter((name) => Boolean(name));
|
|
66
|
-
assert(!pattern.includes('*') || (config.static && pattern.endsWith('/*') && parts.filter(p => p.includes('*')).length === 1 && parts.at(-1) === '*' && !names.length), 'Only static routes support a terminal /* wildcard');
|
|
73
|
+
assert(!pattern.includes('*') || ((config.static || config.extension) && pattern.endsWith('/*') && parts.filter(p => p.includes('*')).length === 1 && parts.at(-1) === '*' && !names.length), 'Only static or extension routes support a terminal /* wildcard');
|
|
67
74
|
assert(!config.static || pattern.endsWith('/*'), 'Static routes require a terminal /* wildcard');
|
|
68
75
|
if (config.page || config.download || config.static) assert((config.methods || methodsDefault).every(m => methodsDefault.includes(m)), 'Asset routes support only GET and HEAD');
|
|
69
76
|
assert(new Set(names).size === names.length, 'Duplicate path parameter');
|
|
@@ -72,6 +79,29 @@ export async function compileRoutes(loaded , bindings
|
|
|
72
79
|
const route = { ...declared, pattern, parts, names, specificity: parts.length - names.length,
|
|
73
80
|
methods: config.methods || methodsDefault, parameters: [], env: dict(), secrets: dict(), responseHeaders: [], middleware: [] };
|
|
74
81
|
compileHttp(route);
|
|
82
|
+
if (config.match) route.match = normalizeMatch(config.match);
|
|
83
|
+
if(config.extension){assert(!config.middleware?.length&&!config.parameters?.length&&!config.env&&!config.secrets,'Extension handlers cannot declare guest middleware, parameters or bindings');assert(pattern.endsWith('/*')&&!names.length&&pattern!=='/*','Extension handler requires a non-root literal /* mount');}
|
|
84
|
+
const extensionPolicyNames = Object.keys(effectiveExtensionPolicies(loaded.document,config));
|
|
85
|
+
if (config.match || config.conditional || config.extension || isSensitiveExtensionPolicy(extensionPolicyNames,extensions)) {
|
|
86
|
+
const cache = effectivePolicies(loaded.document,config).cache;
|
|
87
|
+
assert(!cache || cache.strategy === 'no-store', `${pattern}: conditional routing requires cache disabled or no-store`);
|
|
88
|
+
assert(!route.responseHeaders.some(([name,value]) => ['cache-control','cdn-cache-control','vercel-cdn-cache-control','surrogate-control'].includes(name.toLowerCase()) && value !== 'no-store'), 'Conditional responses require no-store');
|
|
89
|
+
}
|
|
90
|
+
if (config.conditional) {
|
|
91
|
+
const matches = config.conditional.cases.map(item => normalizeMatch(item.match));
|
|
92
|
+
assertDisjointMatches(matches);
|
|
93
|
+
const compileBranch = async (branch ) => {
|
|
94
|
+
const entry = { ...branch, ...(config.parameters ? { parameters: config.parameters } : {}), ...(config.methods ? { methods: config.methods } : {}) };
|
|
95
|
+
const table = await compileRoutes({ ...loaded, routes: { [pattern]: entry } }, {}, {});
|
|
96
|
+
return table.exact.get(pattern) ?? [...table.byLength.values()].flat()[0] ;
|
|
97
|
+
};
|
|
98
|
+
route.conditionalRoutes = { cases: [] };
|
|
99
|
+
for (const [index,item] of config.conditional.cases.entries()) {
|
|
100
|
+
const { match: _match, ...branch } = item;
|
|
101
|
+
route.conditionalRoutes.cases.push({ match: matches[index] , route: await compileBranch(branch) });
|
|
102
|
+
}
|
|
103
|
+
if (config.conditional.fallback) route.conditionalRoutes.fallback = await compileBranch(config.conditional.fallback);
|
|
104
|
+
}
|
|
75
105
|
const seen = new Set ();
|
|
76
106
|
for (const param of config.parameters || []) {
|
|
77
107
|
const schema = param.schema, name = param.in === 'header' ? param.name.toLowerCase() : param.name;
|
|
@@ -101,6 +131,17 @@ export async function compileRoutes(loaded , bindings
|
|
|
101
131
|
assert(typeof value === 'string' && value.length, 'Missing required secret binding');
|
|
102
132
|
route.secrets[alias] = value;
|
|
103
133
|
}
|
|
134
|
+
const resolveHeaders=(headers ) => Object.fromEntries(Object.entries(headers||{}).map(([key,value])=>{if(typeof value==='string')return [key,value];assert(own(route.secrets,value.secret),'Unknown egress secret alias');return [key,route.secrets[value.secret] ];}));
|
|
135
|
+
if(config.proxy){
|
|
136
|
+
assert(!config.middleware?.length,'Proxy middleware is not supported; authorize requests with host policy before egress');
|
|
137
|
+
const cache=effectivePolicies(loaded.document,config).cache;
|
|
138
|
+
assert(!cache||cache.strategy==='no-store','Proxy routes require cache disabled or no-store');
|
|
139
|
+
assert(!route.responseHeaders.some(([name,value])=>['cache-control','cdn-cache-control','vercel-cdn-cache-control','surrogate-control'].includes(name.toLowerCase())&&value!=='no-store'),'Proxy response caching must be no-store');
|
|
140
|
+
route.compiledProxy={...config.proxy,headers:resolveHeaders(config.proxy.headers)};validateProxy(route.compiledProxy);
|
|
141
|
+
for(const match of config.proxy.url.matchAll(/(?:\{|%7B)([A-Za-z_][A-Za-z0-9_]*)(?:\}|%7D)/gi))assert(names.includes(match[1] ), 'Proxy placeholder requires a declared path parameter');
|
|
142
|
+
assert(!Object.keys(config.response?.headers||{}).some(name=>name.toLowerCase()==='content-encoding'),'Proxy content encoding cannot be overridden');
|
|
143
|
+
}
|
|
144
|
+
if(config.signals){route.compiledSignals=config.signals.map(signal=>({...signal,headers:resolveHeaders(signal.headers)}));for(const signal of route.compiledSignals)validateSignal(signal);}
|
|
104
145
|
if (config.expires) {
|
|
105
146
|
assert(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(config.expires) && Number.isFinite(Date.parse(config.expires)), 'Expiry must be a UTC ISO timestamp');
|
|
106
147
|
route.expiresAt = Date.parse(config.expires);
|
|
@@ -126,10 +167,6 @@ export async function compileRoutes(loaded , bindings
|
|
|
126
167
|
for (const key of query.pass || []) assert(!reserved.has(key), 'Query passthrough conflicts with destination or mapping');
|
|
127
168
|
route.redirect = compiledRedirect(declaredRedirect);
|
|
128
169
|
}
|
|
129
|
-
if (config.link) {
|
|
130
|
-
assert(route.methods.every(method => methodsDefault.includes(method)), 'Stored links support only GET and HEAD');
|
|
131
|
-
referenceCheck(config.link.code,route);
|
|
132
|
-
}
|
|
133
170
|
for (const item of config.middleware || []) {
|
|
134
171
|
const source = await functionFile(loaded.root,item.source);
|
|
135
172
|
modules.set(source,true);
|
|
@@ -141,7 +178,7 @@ export async function compileRoutes(loaded , bindings
|
|
|
141
178
|
route.function = { ...declaredFunction, source, export: declaredFunction.export || 'default' };
|
|
142
179
|
for (const ref of Object.values(declaredFunction.args || {})) referenceCheck(ref, route, true);
|
|
143
180
|
}
|
|
144
|
-
if (config.static) { route.prefix = pattern.slice(0, -1); mounts.push(route); }
|
|
181
|
+
if (config.static || config.extension) { route.prefix = pattern.slice(0, -1); mounts.push(route); }
|
|
145
182
|
else if (!names.length) exact.set(pattern, route);
|
|
146
183
|
else {
|
|
147
184
|
assert(dynamic.length < 1000, 'Maximum 1000 parameterized routes per snapshot');
|
|
@@ -160,6 +197,7 @@ export async function compileRoutes(loaded , bindings
|
|
|
160
197
|
if (!byLength.has(route.parts.length)) byLength.set(route.parts.length, []);
|
|
161
198
|
byLength.get(route.parts.length) .push(route);
|
|
162
199
|
}
|
|
200
|
+
for(const mount of mounts.filter(route=>route.extension)){const base=mount.parts.slice(0,-1);for(const candidate of [...exact.values(),...dynamic,...mounts]){if(candidate===mount)continue;const parts=candidate.parts;const shared=Math.min(base.length,parts.length-(candidate.prefix?1:0));const compatible=base.slice(0,shared).every((part,index)=>part===parts[index]||parameterName(parts[index] ));assert(!compatible||(!candidate.prefix&&parts.length<base.length),'Extension mount overlaps another route');}}
|
|
163
201
|
mounts.sort((a,b) => b.prefix .length - a.prefix .length);
|
|
164
202
|
assert(performance.now()<deadline, 'Route compilation deadline exceeded');
|
|
165
203
|
return { exact, byLength, mounts, modules: [...modules.keys()], count: exact.size + dynamic.length + mounts.length };
|