@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,189 @@
|
|
|
1
|
+
import { stringify } from 'yaml';
|
|
2
|
+
import { parseYaml, validateDocument, MAX_CONFIG_BYTES } from './config.js';
|
|
3
|
+
import { compileRoutes } from './router.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const formats = new Set(['csv','json','yaml','netlify','cloudflare','vercel','netlify-toml']);
|
|
17
|
+
const providers = new Set(['netlify','cloudflare','vercel','netlify-toml']);
|
|
18
|
+
const statuses = new Set([301,302,303,307,308]);
|
|
19
|
+
function record(value ) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected an object');
|
|
21
|
+
return value ;
|
|
22
|
+
}
|
|
23
|
+
function keys(value , allowed ) {
|
|
24
|
+
if (Object.keys(value).some(key => !allowed.includes(key))) throw new Error('Unsupported field; conversion would discard behavior');
|
|
25
|
+
}
|
|
26
|
+
function row(value , index ) {
|
|
27
|
+
const item = record(value); keys(item, ['path','url','status']);
|
|
28
|
+
if (typeof item.path !== 'string' || typeof item.url !== 'string' || (item.status !== undefined && typeof item.status !== 'number')) throw new Error('Expected path and url strings and optional numeric status');
|
|
29
|
+
const status = item.status === undefined ? 302 : item.status ;
|
|
30
|
+
if (!statuses.has(status)) throw new Error('Unsupported redirect status');
|
|
31
|
+
if (!/^\/[A-Za-z0-9_./~-]*$/.test(item.path)) throw new Error('Only literal ASCII paths are supported; patterns, escapes and conditions require the runtime');
|
|
32
|
+
if (/[{}]/.test(item.url)) throw new Error('Destination placeholders require the runtime');
|
|
33
|
+
return {path:item.path,url:item.url,status,row:index};
|
|
34
|
+
}
|
|
35
|
+
/** RFC 4180-style fields, including quoted commas/newlines and escaped quotes. */
|
|
36
|
+
function csv(text ) {
|
|
37
|
+
const result = []; let cells = [], cell = '', quoted = false, closed = false, line = 1, start = 1;
|
|
38
|
+
for (let i=0;i<text.length;i++) {
|
|
39
|
+
const ch=text[i] ;
|
|
40
|
+
if (quoted) { if(ch==='"') { if(text[i+1]==='"'){cell+='"';i++;}else{quoted=false;closed=true;} }else{cell+=ch;if(ch==='\n')line++;} continue; }
|
|
41
|
+
if(ch==='"'){if(cell || closed)throw new Error(`Invalid CSV quoting at row ${line}`);quoted=true;continue;}
|
|
42
|
+
if(ch===',' || ch==='\n' || ch==='\r') {
|
|
43
|
+
cells.push(cell);cell='';closed=false;
|
|
44
|
+
if(ch!==','){if(ch==='\r'&&text[i+1]==='\n')i++;result.push({cells,row:start});cells=[];line++;start=line;}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if(closed)throw new Error(`Unexpected data after CSV quote at row ${line}`);cell+=ch;
|
|
48
|
+
}
|
|
49
|
+
if(quoted)throw new Error(`Unterminated CSV quote at row ${start}`);
|
|
50
|
+
if(cell || cells.length || closed){cells.push(cell);result.push({cells,row:start});}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function parseRows(format , text , add ) {
|
|
54
|
+
if(format==='csv') {
|
|
55
|
+
const entries=csv(text), header=entries.shift()?.cells;
|
|
56
|
+
if(!header || header.join(',')!=='path,url,status')throw new Error('CSV header must be path,url,status');
|
|
57
|
+
for(const entry of entries){if(entry.cells.length!==3)throw new Error(`CSV row ${entry.row} must have three fields`);add({path:entry.cells[0],url:entry.cells[1],...(entry.cells[2] ? {status:Number(entry.cells[2])}: {})},entry.row);}
|
|
58
|
+
}else if(format==='netlify'||format==='cloudflare') {
|
|
59
|
+
for(const [i,line] of text.split(/\r?\n/).entries()) {
|
|
60
|
+
const trimmed=line.trim();if(!trimmed||trimmed.startsWith('#'))continue;
|
|
61
|
+
const fields=trimmed.split(/\s+/);if(fields.length<2||fields.length>3)throw new Error(`Unsupported redirect fields at row ${i+1}`);
|
|
62
|
+
const status=fields[2] === undefined ? (format==='netlify'?301:302) : Number(fields[2]);
|
|
63
|
+
add({path:fields[0],url:fields[1],status},i+1);
|
|
64
|
+
}
|
|
65
|
+
}else if(format==='netlify-toml') {
|
|
66
|
+
// Deliberately a redirects-only grammar, not a general TOML implementation.
|
|
67
|
+
let current , start=0;
|
|
68
|
+
const finish=()=>{if(current)add({path:current.from,url:current.to,status:current.status??301},start);};
|
|
69
|
+
for(const [i,line]of text.split(/\r?\n/).entries()) {
|
|
70
|
+
const value=line.trim();if(!value||value.startsWith('#'))continue;
|
|
71
|
+
if(value==='[[redirects]]'){finish();current={};start=i+1;continue;}
|
|
72
|
+
const match=/^(from|to|status)\s*=\s*(.+)$/.exec(value);
|
|
73
|
+
if(!current||!match||Object.hasOwn(current,match[1] ))throw new Error(`Unsupported or duplicate TOML declaration at row ${i+1}`);
|
|
74
|
+
const key=match[1] , literal=match[2] ;
|
|
75
|
+
if(key==='status'){if(!/^\d{3}$/.test(literal))throw new Error(`Invalid TOML status at row ${i+1}`);current[key]=Number(literal);}
|
|
76
|
+
else { if(!/^"[^"\\\u0000-\u001f]*"$/u.test(literal))throw new Error(`Only unescaped basic TOML strings are supported at row ${i+1}`);current[key]=literal.slice(1,-1); }
|
|
77
|
+
}
|
|
78
|
+
finish();
|
|
79
|
+
}else {
|
|
80
|
+
const data =format==='json'||format==='vercel'?JSON.parse(text):parseYaml(text);
|
|
81
|
+
// Apply the same reserved-key/depth/JSON-value policy to JSON inputs.
|
|
82
|
+
if(format==='json'||format==='vercel')parseYaml(text);
|
|
83
|
+
if(format==='vercel') {
|
|
84
|
+
const doc=record(data);keys(doc,['redirects']);if(!Array.isArray(doc.redirects))throw new Error('Expected redirects array');
|
|
85
|
+
doc.redirects.forEach((value ,i )=>{const entry=record(value);keys(entry,['source','destination','permanent','statusCode']);
|
|
86
|
+
if(entry.permanent!==undefined&&(typeof entry.permanent!=='boolean'||entry.statusCode!==undefined))throw new Error(`Invalid permanent/statusCode at row ${i+1}`);
|
|
87
|
+
if(entry.permanent===undefined&&entry.statusCode===undefined)throw new Error(`Specify permanent or statusCode at row ${i+1}`);
|
|
88
|
+
add({path:entry.source,url:entry.destination,status:entry.statusCode??(entry.permanent?308:307)},i+1);});
|
|
89
|
+
}else {if(!Array.isArray(data))throw new Error('Expected an array of {path,url,status} rows');data.forEach((value ,i )=>add(value,i+1));}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function providerRows(format , rows ) {
|
|
93
|
+
if(!providers.has(format))return;
|
|
94
|
+
if(rows.some(entry=> /:[A-Za-z_]|[*$]/.test(entry.url.replace(/^https?:\/\//,''))))throw new Error('Provider placeholder-like destinations cannot be converted safely');
|
|
95
|
+
if(format==='cloudflare'&&(rows.length>2000||rows.some(entry=>`${entry.path} ${entry.url} ${entry.status}`.length>1000)))throw new Error('Cloudflare Pages supports at most 2,000 static redirects and 1,000 characters per rule');
|
|
96
|
+
}
|
|
97
|
+
function differences(format , accepted , source ) {
|
|
98
|
+
if(!providers.has(format))return [];
|
|
99
|
+
return [{severity:accepted?'warning':'error',code:'provider-semantics',source,message:(format==='netlify'||format==='netlify-toml'?'Netlify may forward incoming query parameters and let existing assets shadow redirects. ':'The provider request/query normalization and redirect method coverage are not guaranteed to match URLCode. ')+(format==='cloudflare'?'Cloudflare Pages Functions take precedence over _redirects. ':'')+'URLCode drops incoming queries and limits these routes to GET/HEAD; provider redirects can cover other methods. Only literal GET/HEAD requests without incoming queries, conflicting assets/functions or normalization are in scope. Explicit acknowledgment produces a non-lossless migration candidate; use the runtime for exact behavior.'}];
|
|
100
|
+
}
|
|
101
|
+
async function validateRows(rows , source , diagnostics ) {
|
|
102
|
+
const routes =Object.create(null) ;
|
|
103
|
+
for(const entry of rows) {
|
|
104
|
+
if(Object.hasOwn(routes,entry.path)){diagnostics.push({severity:'error',code:'duplicate-path',source,row:entry.row,path:entry.path,message:'Duplicate route path; no rule is overwritten'});continue;}
|
|
105
|
+
routes[entry.path]={redirect:{url:entry.url,status:entry.status }};
|
|
106
|
+
}
|
|
107
|
+
const document ={version:'1',routes};
|
|
108
|
+
try {
|
|
109
|
+
validateDocument(document);
|
|
110
|
+
await compileRoutes({root:'.',document,routes,files:[],version:'interchange'},{});
|
|
111
|
+
}catch(error){
|
|
112
|
+
// On failure identify offending rows without exposing destination values.
|
|
113
|
+
let located=false, examined=0;
|
|
114
|
+
const deadline=performance.now()+10000;
|
|
115
|
+
for(let offset=0;offset<rows.length;offset+=128){
|
|
116
|
+
if(performance.now()>deadline||diagnostics.length>=100)break;
|
|
117
|
+
const chunk=rows.slice(offset,offset+128);
|
|
118
|
+
const subset ={version:'1',routes:Object.fromEntries(chunk.map(entry=>[entry.path,routes[entry.path] ]))};
|
|
119
|
+
try{validateDocument(subset);await compileRoutes({root:'.',document:subset,routes:subset.routes,files:[],version:'interchange'},{});examined+=chunk.length;continue;}catch{/* Locate the failure inside this bounded chunk. */}
|
|
120
|
+
for(const entry of chunk){examined++;const one ={version:'1',routes:{[entry.path]:routes[entry.path] }};
|
|
121
|
+
try{validateDocument(one);await compileRoutes({root:'.',document:one,routes:one.routes,files:[],version:'interchange'},{});}
|
|
122
|
+
catch{located=true;diagnostics.push({severity:'error',code:'invalid-route',source,row:entry.row,path:entry.path,message:'Invalid route path or redirect destination under the URLCode contract'});}
|
|
123
|
+
if(diagnostics.length>=100||performance.now()>deadline)break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if(examined<rows.length)diagnostics.push({severity:'error',code:'diagnostic-limit',source,message:'Diagnostic scan stopped at its resource limit; remaining rows were not classified'});
|
|
127
|
+
if(!located)diagnostics.push({severity:'error',code:'invalid-project',source,message:error instanceof Error?error.message:'Invalid project'});
|
|
128
|
+
}
|
|
129
|
+
return document;
|
|
130
|
+
}
|
|
131
|
+
function counts(routeCount , diagnostics , ok ) {
|
|
132
|
+
const runtimeRequiredRoutes=diagnostics.filter(item=>item.code==='runtime-required').length;
|
|
133
|
+
const unsupportedRows=diagnostics.filter(item=>['unsupported-row','invalid-route','duplicate-path'].includes(item.code)).length;
|
|
134
|
+
const providerDifferenceRoutes=diagnostics.some(item=>item.code==='provider-semantics')?routeCount:0;
|
|
135
|
+
return {convertedRoutes:ok?routeCount:0,nativeEquivalentRoutes:ok&&diagnostics.length===0?routeCount:0,runtimeRequiredRoutes,unsupportedRows,providerDifferenceRoutes,
|
|
136
|
+
fullyScanned:!diagnostics.some(item=>['invalid-input','invalid-project','diagnostic-limit'].includes(item.code))&&diagnostics.length<100};
|
|
137
|
+
}
|
|
138
|
+
function report(document , diagnostics , output ) {
|
|
139
|
+
const ok=!diagnostics.some(d=>d.severity==='error');
|
|
140
|
+
const routeCount=Object.keys(document.routes).length;
|
|
141
|
+
return {ok,lossless:diagnostics.length===0,routeCount,counts:counts(routeCount,diagnostics,ok),diagnostics,...(ok?{document,output}:{})};
|
|
142
|
+
}
|
|
143
|
+
/** Pure conversion: does not read projects, resolve bindings, execute guests or write files. */
|
|
144
|
+
export async function importRoutes(options ) {
|
|
145
|
+
const source=options.source??'<input>', diagnostics =[],rows =[];
|
|
146
|
+
try{
|
|
147
|
+
if(!formats.has(options.format))throw new Error('Unknown interchange format');
|
|
148
|
+
if(Buffer.byteLength(options.text)>MAX_CONFIG_BYTES)throw new Error('Input exceeds 32 MiB');
|
|
149
|
+
parseRows(options.format,options.text,(value,index)=>{
|
|
150
|
+
if(rows.length>=100000)throw new Error('Input exceeds 100,000 routes');
|
|
151
|
+
try{rows.push(row(value,index));}catch(error){diagnostics.push({severity:'error',code:'unsupported-row',source,row:index,message:error instanceof Error?error.message:'Invalid row'});}
|
|
152
|
+
if(diagnostics.length>=100)throw new Error('Stopped after 100 invalid rows');
|
|
153
|
+
});
|
|
154
|
+
providerRows(options.format,rows);
|
|
155
|
+
diagnostics.push(...differences(options.format,options.acceptProviderDifferences===true,source));
|
|
156
|
+
rows.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
|
157
|
+
const document=await validateRows(rows,source,diagnostics);
|
|
158
|
+
return report(document,diagnostics,stringify(document));
|
|
159
|
+
}catch(error){diagnostics.push({severity:'error',code:'invalid-input',source,message:error instanceof SyntaxError?'Invalid input syntax':error instanceof Error?error.message:'Invalid input'});return{ok:false,lossless:false,routeCount:rows.length,counts:counts(rows.length,diagnostics,false),diagnostics};}
|
|
160
|
+
}
|
|
161
|
+
function encodedCsv(value ) { return /[",\r\n]/.test(value)?`"${value.replaceAll('"','""')}"`:value; }
|
|
162
|
+
export async function exportRoutes(options ) {
|
|
163
|
+
const source=options.source??'<project>',diagnostics =[],rows =[];
|
|
164
|
+
try{
|
|
165
|
+
if(!formats.has(options.format))throw new Error('Unknown interchange format');
|
|
166
|
+
keys(record(options.document),['version','routes']);validateDocument(options.document);
|
|
167
|
+
if(Object.keys(options.document.routes).length>100000)throw new Error('Input exceeds 100,000 routes');
|
|
168
|
+
for(const [path,config]of Object.entries(options.document.routes)) {
|
|
169
|
+
try{keys(record(config),['redirect']);if(!config.redirect)throw new Error('Only redirects can be exported');keys(record(config.redirect),['url','status']);rows.push(row({path,url:config.redirect.url,status:config.redirect.status??302},rows.length+1));}
|
|
170
|
+
catch{diagnostics.push({severity:'error',code:'runtime-required',source,path,message:'Only literal redirects with default methods and no extra behavior can be exported'});}
|
|
171
|
+
}
|
|
172
|
+
rows.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
|
173
|
+
providerRows(options.format,rows);
|
|
174
|
+
diagnostics.push(...differences(options.format,options.acceptProviderDifferences===true,source));
|
|
175
|
+
const document=await validateRows(rows,source,diagnostics);
|
|
176
|
+
const simple=rows.map(({path,url,status})=>({path,url,status}));let output ;
|
|
177
|
+
switch(options.format){
|
|
178
|
+
case 'csv':output='path,url,status\n'+simple.map(r=>[r.path,r.url,String(r.status)].map(encodedCsv).join(',')+'\n').join('');break;
|
|
179
|
+
case 'json':output=JSON.stringify(simple,null,2)+'\n';break;
|
|
180
|
+
case 'yaml':output=stringify(simple);break;
|
|
181
|
+
case 'vercel':output=JSON.stringify({redirects:simple.map(r=>({source:r.path,destination:r.url,statusCode:r.status}))},null,2)+'\n';break;
|
|
182
|
+
case 'netlify-toml':
|
|
183
|
+
if(simple.some(r=>/["\\]/.test(r.url)))throw new Error('TOML subset cannot represent escaped destination strings');
|
|
184
|
+
output=simple.map(r=>`[[redirects]]\nfrom = "${r.path}"\nto = "${r.url}"\nstatus = ${r.status}\n`).join('\n');break;
|
|
185
|
+
default:output=simple.map(r=>`${r.path} ${r.url} ${r.status}\n`).join('');
|
|
186
|
+
}
|
|
187
|
+
return report(document,diagnostics,output);
|
|
188
|
+
}catch(error){diagnostics.push({severity:'error',code:'invalid-project',source,message:error instanceof Error?error.message:'Invalid project'});return{ok:false,lossless:false,routeCount:rows.length,counts:counts(rows.length,diagnostics,false),diagnostics};}
|
|
189
|
+
}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {join,relative} from 'node:path';
|
|
3
|
+
import {capabilityTargets,analyzeCompiledCapabilities,routeCapabilities} from './capabilities.js';
|
|
4
|
+
|
|
5
|
+
import {parseYaml,safeFile} from './config.js';
|
|
6
|
+
import {effectiveExtensionPolicies} from './extensions.js';
|
|
7
|
+
import {egressUrl} from './egress.js';
|
|
8
|
+
import {prepare} from './tooling.js';
|
|
9
|
+
|
|
10
|
+
import {explainCompiledRoute} from './explain.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// A generated description of what a project declares, for agents, reviews and
|
|
14
|
+
// deployment tooling. It is derived from the compiled IR on every call, never
|
|
15
|
+
// checked in as a source of truth, and it is deterministic: the same project
|
|
16
|
+
// yields the same bytes. Nothing in it is a binding value or source text.
|
|
17
|
+
|
|
18
|
+
export const MANIFEST_SCHEMA_VERSION=2;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const compare=(a ,b ) =>a<b?-1:a>b?1:0;
|
|
38
|
+
const sorted=(values ) =>[...new Set(values)].sort(compare);
|
|
39
|
+
const packageVersion=(JSON.parse(await readFile(new URL('../package.json',import.meta.url),'utf8')) ).version;
|
|
40
|
+
function origin(url ) {try{return egressUrl(url).origin;}catch{return url;}}
|
|
41
|
+
/**
|
|
42
|
+
* Recipe provenance is read from an optional `recipe.yaml` beside the entry
|
|
43
|
+
* file (the shape `urlcode recipes` metadata uses: `id`, `description`, ...).
|
|
44
|
+
* Only its identifying fields are copied; an absent or unreadable file means no provenance.
|
|
45
|
+
*/
|
|
46
|
+
async function recipeProvenance(root ) {
|
|
47
|
+
let path ;
|
|
48
|
+
try{path=await safeFile(root,'recipe.yaml');}catch{return [];}
|
|
49
|
+
let data ;
|
|
50
|
+
try{data=parseYaml(await readFile(path,'utf8'));}catch{return [];}
|
|
51
|
+
if(!data||typeof data!=='object'||Array.isArray(data))return [];
|
|
52
|
+
const record=data ;
|
|
53
|
+
if(typeof record.id!=='string'||!/^[a-z0-9][a-z0-9-]{0,63}$/.test(record.id))return [];
|
|
54
|
+
const entry ={id:record.id};
|
|
55
|
+
for(const key of ['description','version','source'] )if(typeof record[key]==='string'&&record[key].length<=512)entry[key]=record[key];
|
|
56
|
+
return [entry];
|
|
57
|
+
}
|
|
58
|
+
/** Build the semantic manifest for a project from its compiled IR. Deterministic for a given project. */
|
|
59
|
+
export async function buildManifest(project ,options ={}) {
|
|
60
|
+
const {loaded,compiled,routes,chains,projectSha256}=await prepare(project,options);
|
|
61
|
+
const explanations=routes.map(route=>explainCompiledRoute(loaded,route,chains.get(route.pattern),{projectSha256,now:0})).sort((a,b)=>compare(a.path,b.path));
|
|
62
|
+
const functions=new Map (),middleware=new Map ();
|
|
63
|
+
const register=(table ,source ,name ,path )=>{const key=`${source}#${name}`;const entry=table.get(key)??{source,export:name,routes:[]};entry.routes.push(path);table.set(key,entry);};
|
|
64
|
+
const env=new Set (),secrets=new Set (),proxy=new Set (),signals=new Set ();
|
|
65
|
+
const manifestRoutes =[];
|
|
66
|
+
for(const explanation of explanations){
|
|
67
|
+
const declared=loaded.routes[explanation.path];
|
|
68
|
+
const routeEnv=sorted(Object.values(declared?.env??{}).flatMap(ref=>ref.env?[ref.env]:[])),routeSecrets=sorted(Object.values(declared?.secrets??{}).map(ref=>ref.secret));
|
|
69
|
+
for(const name of routeEnv)env.add(name);for(const name of routeSecrets)secrets.add(name);
|
|
70
|
+
if(declared?.proxy)proxy.add(origin(declared.proxy.url));for(const signal of declared?.signals??[])signals.add(origin(signal.url));
|
|
71
|
+
if(explanation.handler.kind==='function')register(functions,explanation.handler.source ,explanation.handler.export ,explanation.path);
|
|
72
|
+
for(const item of explanation.middleware)register(middleware,item.source,item.export,explanation.path);
|
|
73
|
+
const extensions ={};
|
|
74
|
+
for(const name of Object.keys(explanation.policies.extensions).sort(compare))extensions[name]=explanation.policies.extensions[name] .requirement;
|
|
75
|
+
const targets={} ;
|
|
76
|
+
for(const target of capabilityTargets)targets[target]=explanation.targets[target].compatible;
|
|
77
|
+
manifestRoutes.push({
|
|
78
|
+
path:explanation.path,methods:explanation.methods,handler:explanation.handler,enabled:explanation.enabled,
|
|
79
|
+
...(explanation.expires?{expires:explanation.expires}:{}),...(explanation.generated?{generated:explanation.generated}:{}),...(explanation.description?{description:explanation.description}:{}),
|
|
80
|
+
middleware:explanation.middleware,parameters:explanation.inputs.parameters,...(explanation.inputs.body?{body:explanation.inputs.body}:{}),
|
|
81
|
+
policies:explanation.policies.names,extensions,cache:explanation.cache,bindings:{env:routeEnv,secrets:routeSecrets},egress:explanation.egress,
|
|
82
|
+
capabilities:explanation.capabilities,targets,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const extensionDeclarations ={};
|
|
86
|
+
for(const name of Object.keys(loaded.document.extensions??{}).sort(compare)){
|
|
87
|
+
const declaration=loaded.document.extensions [name] ;
|
|
88
|
+
extensionDeclarations[name]={version:declaration.version,configKeys:sorted(Object.keys(declaration.config??{})),
|
|
89
|
+
mounts:sorted(Object.entries(loaded.routes).filter(([,route])=>route.extension===name).map(([path])=>path)),
|
|
90
|
+
protectedRoutes:sorted(Object.entries(loaded.routes).filter(([,route])=>Object.hasOwn(effectiveExtensionPolicies(loaded.document,route),name)).map(([path])=>path))};
|
|
91
|
+
}
|
|
92
|
+
const targets={} ;
|
|
93
|
+
for(const target of capabilityTargets){const report=analyzeCompiledCapabilities(loaded.document,compiled,target,options.extensions);targets[target]={compatible:report.compatible,issues:report.issues.length};}
|
|
94
|
+
const capabilities=new Set ();
|
|
95
|
+
for(const route of routes)for(const capability of routeCapabilities(route,loaded.document))capabilities.add(capability);
|
|
96
|
+
const modules=(table )=>[...table.values()].map(entry=>({...entry,routes:sorted(entry.routes)})).sort((a,b)=>compare(a.source,b.source)||compare(a.export,b.export));
|
|
97
|
+
return {
|
|
98
|
+
schemaVersion:MANIFEST_SCHEMA_VERSION,urlcode:packageVersion,entry:'urlcode.yaml',
|
|
99
|
+
files:loaded.files.map(file=>relative(loaded.root,file).split('\\').join('/')),
|
|
100
|
+
revision:projectSha256,configVersion:loaded.version,routeCount:manifestRoutes.length,routes:manifestRoutes,
|
|
101
|
+
capabilities:[...capabilities].sort(compare),extensions:extensionDeclarations,recipes:await recipeProvenance(loaded.root),
|
|
102
|
+
external:{env:sorted(env),secrets:sorted(secrets),egress:{proxy:sorted(proxy),signals:sorted(signals)},extensions:Object.keys(extensionDeclarations)},
|
|
103
|
+
functions:modules(functions),middleware:modules(middleware),targets,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The manifest as `build` writes it: two-space JSON with a trailing newline. */
|
|
107
|
+
export function renderManifest(manifest ) {return JSON.stringify(manifest,null,2)+'\n';}
|
|
108
|
+
export const manifestFileName='manifest.json';
|
|
109
|
+
export function manifestPath(out ) {return join(out,manifestFileName);}
|
package/dist/match.js
CHANGED
|
@@ -19,7 +19,7 @@ import { HttpError } from './errors.js';
|
|
|
19
19
|
|
|
20
20
|
/** The part of a compiled route that request-time matching reads. router.ts widens it. */
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
|
|
@@ -64,7 +64,7 @@ export function matchRoute (compiled
|
|
|
64
64
|
return p === actual;
|
|
65
65
|
})) return { route, path };
|
|
66
66
|
}
|
|
67
|
-
for (const route of compiled.mounts) if (route.prefix !== undefined && target.path.startsWith(route.prefix)) return { route, path: dict() };
|
|
67
|
+
for (const route of compiled.mounts) if (route.prefix !== undefined && (target.path.startsWith(route.prefix)||(route.extension&&target.path===route.prefix.slice(0,-1)))) return { route, path: dict() };
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
function scalar(value , type ) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {lstat,readFile,open,rename,rm,mkdtemp} from 'node:fs/promises';
|
|
2
|
+
import {join,extname,isAbsolute} from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {spawn} from 'node:child_process';
|
|
5
|
+
import {parseDocument,isMap} from 'yaml';
|
|
6
|
+
import {loadDocument,validateDocument,parseYaml,normalizeRouteAuth,MAX_CONFIG_BYTES} from './config.js';
|
|
7
|
+
import {compileRoutes} from './router.js';
|
|
8
|
+
import {prepareFunctionSnapshot,requestedPermissions} from './policy.js';
|
|
9
|
+
import {validateProject} from './tooling.js';
|
|
10
|
+
import {scaffoldProject} from './scaffold.js';
|
|
11
|
+
import {addRecipe} from './recipes.js';
|
|
12
|
+
import {authoringPath} from './authoring-files.js';
|
|
13
|
+
import {assert} from './errors.js';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Authoring tools for `urlcode mcp --allow-authoring`. Every write lands inside
|
|
18
|
+
* the operator-selected project root (after realpath), through the existing
|
|
19
|
+
* authoring, recipe and scaffold paths. Nothing here reads bindings, creates
|
|
20
|
+
* grants, deploys, or touches operator policy, compliance rules, host files or
|
|
21
|
+
* link stores. The runners spawn the CLI against the same root only.
|
|
22
|
+
*/
|
|
23
|
+
const text={type:'string',maxLength:1024};
|
|
24
|
+
const handler={anyOf:[{type:'string',maxLength:2048},{type:'object'}]};
|
|
25
|
+
const middleware={type:'array',maxItems:32,items:{anyOf:[{type:'string',maxLength:1024},{type:'object'}]}};
|
|
26
|
+
export const authoringDefinitions=[
|
|
27
|
+
{name:'create_route',description:'Add one route to urlcode.yaml or a named include under the project. The merged project is validated before the write; missing function sources are reported for scaffold_feature.',properties:{path:{type:'string',maxLength:2048},handler,middleware,file:text},required:['path','handler']},
|
|
28
|
+
{name:'add_recipe',description:'Copy a bundled recipe into a new directory inside the project (recipes add). dryRun reports the destination and writes nothing.',properties:{name:{type:'string',maxLength:64},destination:text,dryRun:{type:'boolean'}},required:['name','destination']},
|
|
29
|
+
{name:'scaffold_feature',description:'Create placeholder modules, pages and directories the project YAML references and that do not exist yet; existing files are never edited.',properties:{dryRun:{type:'boolean'}},required:[]},
|
|
30
|
+
{name:'run_validate',description:'Run `urlcode validate --local` against the project; returns exit code and bounded output.',properties:{},required:[]},
|
|
31
|
+
{name:'run_test',description:'Run `urlcode test` against the project (activates the local runtime and executes fixtures); returns exit code and bounded output.',properties:{},required:[]},
|
|
32
|
+
{name:'run_audit',description:'Run `urlcode audit` against the project; returns exit code and bounded output.',properties:{},required:[]},
|
|
33
|
+
];
|
|
34
|
+
const isCode=(error ,code ) =>error instanceof Error&&'code' in error&&error.code===code;
|
|
35
|
+
// Operator-owned material never lives under an authoring write, even when an
|
|
36
|
+
// operator mistakenly placed it in the checkout.
|
|
37
|
+
const operatorFile=/^(?:.*policy.*\.json|.*compliance.*\.(?:json|mjs|js|cjs|ts)|host(?:-file)?\.(?:mjs|js|cjs|ts)|.*\.(?:sqlite3?|db)(?:-wal|-shm|-journal)?|urlcode\.yaml\.lock)$/i;
|
|
38
|
+
/** A project-relative path an authoring tool may create or replace: no absolute, `..`, hidden (`.env*`, `.git`), credential or operator names, and no symlink anywhere on the walk. */
|
|
39
|
+
export async function confinedPath(root ,path ) {
|
|
40
|
+
assert(typeof path==='string'&&path.length>0&&!isAbsolute(path)&&!path.includes('\\')&&!/^[A-Za-z]:/.test(path),'Authoring paths must be project-relative');
|
|
41
|
+
authoringPath(path);
|
|
42
|
+
const parts=path.split('/');
|
|
43
|
+
assert(parts.every(part=>!operatorFile.test(part)&&!/^\.env/i.test(part)&&part!=='.git'),'Authoring never writes operator, credential or version-control files');
|
|
44
|
+
let file=root;
|
|
45
|
+
for(const part of parts){
|
|
46
|
+
file=join(file,part);let info;
|
|
47
|
+
try{info=await lstat(file);}catch(error){if(isCode(error,'ENOENT'))break;throw error;}
|
|
48
|
+
assert(!info.isSymbolicLink(),'Authoring through symlinks is forbidden');
|
|
49
|
+
}
|
|
50
|
+
return join(root,path);
|
|
51
|
+
}
|
|
52
|
+
async function verdict(root ,origin ) {
|
|
53
|
+
try{return await validateProject(root,origin?{origin}:{});}
|
|
54
|
+
catch{return {valid:false ,note:'Project does not validate; call run_validate for the CLI report.'};}
|
|
55
|
+
}
|
|
56
|
+
function object(value ) {return value!==null&&typeof value==='object'&&!Array.isArray(value);}
|
|
57
|
+
function expandHandler(path ,handler ) {
|
|
58
|
+
if(object(handler))return handler;
|
|
59
|
+
assert(typeof handler==='string','Handler must be a route object or a short form');
|
|
60
|
+
if(/^https?:\/\//.test(handler))return {redirect:{url:handler}};
|
|
61
|
+
assert(['.js','.mjs'].includes(extname(handler)),'Short-form handler must be an HTTP(S) URL or a .js/.mjs function source');
|
|
62
|
+
// Written as the YAML short form; validateDocument expands it to the canonical parameters/args (see normalizeRoute).
|
|
63
|
+
return {function:handler};
|
|
64
|
+
}
|
|
65
|
+
function expandMiddleware(value ) {
|
|
66
|
+
if(value===undefined)return undefined;
|
|
67
|
+
assert(Array.isArray(value),'middleware must be a list');
|
|
68
|
+
return value.map(entry=>{if(typeof entry==='string')return entry;assert(object(entry),'middleware entries must be sources or objects');return entry ;});
|
|
69
|
+
}
|
|
70
|
+
async function sources(root ,route ) {
|
|
71
|
+
const present =[],missing =[];
|
|
72
|
+
for(const definition of [...(route.middleware??[]),...(route.function?[route.function]:[])]){
|
|
73
|
+
const source=definition.source;
|
|
74
|
+
let exists=false;try{await confinedPath(root,source);exists=(await lstat(join(root,source))).isFile();}catch(error){if(!isCode(error,'ENOENT'))throw error;}
|
|
75
|
+
(exists?present:missing).push(source);
|
|
76
|
+
}
|
|
77
|
+
return {present,missing:[...new Set(missing)]};
|
|
78
|
+
}
|
|
79
|
+
async function createRoute(root ,args ,origin ) {
|
|
80
|
+
const path=args.path;assert(typeof path==='string'&&path.startsWith('/'),'Route path must start with /');
|
|
81
|
+
const loaded=await loadDocument(root);
|
|
82
|
+
const file=typeof args.file==='string'?args.file:'urlcode.yaml';
|
|
83
|
+
assert(file==='urlcode.yaml'||(loaded.document.includes??[]).includes(file),'file must be urlcode.yaml or an include listed in it');
|
|
84
|
+
const target=await confinedPath(root,file);
|
|
85
|
+
assert(!Object.hasOwn(loaded.routes,path),'Route already exists');
|
|
86
|
+
const route={...expandHandler(path,args.handler)};
|
|
87
|
+
const middleware=expandMiddleware(args.middleware);if(middleware)route.middleware=middleware;
|
|
88
|
+
const lockPath=join(root,'urlcode.yaml.lock'),lock=await open(lockPath,'wx',0o600);
|
|
89
|
+
let temp ;
|
|
90
|
+
try {
|
|
91
|
+
const original=await readFile(target);assert(original.length<=MAX_CONFIG_BYTES,'Configuration exceeds 32 MiB');
|
|
92
|
+
const latest=await loadDocument(root);assert(!Object.hasOwn(latest.routes,path),'Route already exists');
|
|
93
|
+
const doc=parseDocument(original.toString('utf8'),{uniqueKeys:false});
|
|
94
|
+
doc.setIn(['routes',path],route);
|
|
95
|
+
const routesNode=doc.getIn(['routes']);if(isMap(routesNode))routesNode.flow=false;
|
|
96
|
+
const data=validateDocument(parseYaml(String(doc)));
|
|
97
|
+
const added=data.routes[path];assert(added,'Route was not written');
|
|
98
|
+
const routes={...latest.routes,[path]:added};
|
|
99
|
+
normalizeRouteAuth(latest.document,routes);
|
|
100
|
+
const candidate ={...latest,routes};
|
|
101
|
+
const {present,missing}=await sources(root,added);
|
|
102
|
+
if(!missing.length){
|
|
103
|
+
// Same pre-write check as `urlcode add`: shape and references with dummy values, no credential reads, no execution, no grant.
|
|
104
|
+
const snapshot=await prepareFunctionSnapshot(candidate),bindings =Object.create(null) ;
|
|
105
|
+
for(const value of Object.values(routes)){for(const ref of Object.values(value.env||{}))if(ref.env)bindings[ref.env]='validation-only';for(const ref of Object.values(value.secrets||{}))bindings[ref.secret]='validation-only';}
|
|
106
|
+
await compileRoutes(candidate,bindings,requestedPermissions(candidate,snapshot),snapshot.projectSha256);
|
|
107
|
+
}
|
|
108
|
+
temp=await mkdtemp(join(root,'.urlcode-edit-'));
|
|
109
|
+
const temporary=join(temp,'edit.yaml'),out=await open(temporary,'wx',0o600);
|
|
110
|
+
try{await out.writeFile(String(doc));await out.sync();}finally{await out.close();}
|
|
111
|
+
assert((await readFile(target)).equals(original),'Configuration changed during edit; retry');
|
|
112
|
+
await rename(temporary,target);
|
|
113
|
+
return {created:true,path,file,route:added,sources:present,missingSources:missing,next:missing.length?'Call scaffold_feature to create placeholder modules, then implement them.':null,validation:await verdict(root,origin)};
|
|
114
|
+
} finally {if(temp)await rm(temp,{recursive:true,force:true});await lock.close();await rm(lockPath,{force:true});}
|
|
115
|
+
}
|
|
116
|
+
const outputLimit=32768;
|
|
117
|
+
function bounded(chunks ) {
|
|
118
|
+
const all=Buffer.concat(chunks);return {text:all.subarray(0,outputLimit).toString('utf8'),truncated:all.length>outputLimit};
|
|
119
|
+
}
|
|
120
|
+
async function runCli(root ,command ,origin ) {
|
|
121
|
+
const cli=fileURLToPath(new URL('./cli.js',import.meta.url));
|
|
122
|
+
const args=[cli,command,'--project',root,...(command==='validate'?['--local']:[]),...(origin?['--origin',origin]:[])];
|
|
123
|
+
// A fresh minimal environment: the runner never forwards this process's ambient variables to the project.
|
|
124
|
+
const child=spawn(process.execPath,args,{cwd:root,env:{PATH:process.env.PATH??''},stdio:['ignore','pipe','pipe']});
|
|
125
|
+
const stdout =[],stderr =[];let total=0;
|
|
126
|
+
const collect=(sink )=>(chunk )=>{if(total<outputLimit*4){sink.push(chunk);total+=chunk.length;}};
|
|
127
|
+
child.stdout.on('data',collect(stdout));child.stderr.on('data',collect(stderr));
|
|
128
|
+
const timer=setTimeout(()=>child.kill('SIGKILL'),120000);
|
|
129
|
+
const exit=await new Promise ((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));}).finally(()=>clearTimeout(timer));
|
|
130
|
+
const out=bounded(stdout),err=bounded(stderr);
|
|
131
|
+
return {command,exitCode:exit.code,signal:exit.signal,stdout:out.text,stderr:err.text,truncated:out.truncated||err.truncated};
|
|
132
|
+
}
|
|
133
|
+
export async function callAuthoringTool(root ,name ,args ,origin ) {
|
|
134
|
+
switch(name){
|
|
135
|
+
case 'create_route':return createRoute(root,args,origin);
|
|
136
|
+
case 'add_recipe':{
|
|
137
|
+
const destination=await confinedPath(root,args.destination);
|
|
138
|
+
const report=await addRecipe(args.name ,destination,{dryRun:args.dryRun===true});
|
|
139
|
+
return {...report,output:args.destination,validation:await verdict(root,origin)};
|
|
140
|
+
}
|
|
141
|
+
case 'scaffold_feature':return {...await scaffoldProject(root,{dryRun:args.dryRun===true}),validation:await verdict(root,origin)};
|
|
142
|
+
case 'run_validate':return runCli(root,'validate',origin);
|
|
143
|
+
case 'run_test':return runCli(root,'test',origin);
|
|
144
|
+
case 'run_audit':return runCli(root,'audit',origin);
|
|
145
|
+
default:throw new Error('Unknown tool');
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {realpath} from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import {once} from 'node:events';
|
|
4
|
+
import {Ajv} from 'ajv';
|
|
5
|
+
import {inspectProject,validateProject,explainRoute,getCapabilities,getCapability,getSchemaFragment,previewImport,previewExport,listRecipes,showRecipe,searchRecipes,searchExamples,describeExtensions,buildContext} from './tooling.js';
|
|
6
|
+
import {loadOperatorHost} from './operator-host.js';
|
|
7
|
+
import {buildManifest} from './manifest.js';
|
|
8
|
+
|
|
9
|
+
import {authoringDefinitions,callAuthoringTool} from './mcp-authoring.js';
|
|
10
|
+
const protocolVersion='2025-11-25';
|
|
11
|
+
const maxBytes=1048576;
|
|
12
|
+
const text={type:'string',maxLength:8192};
|
|
13
|
+
const format={enum:['csv','json','yaml','netlify','cloudflare','vercel','netlify-toml']};
|
|
14
|
+
const definitions=[
|
|
15
|
+
{name:'inspect',description:'Inspect semantically validated route metadata without binding values or code execution.',properties:{target:text,offset:{type:'integer',minimum:0},limit:{type:'integer',minimum:1,maximum:1000}}},
|
|
16
|
+
{name:'validate',description:'Validate project syntax and route/policy semantics without activation.',properties:{}},
|
|
17
|
+
{name:'capabilities',description:'Describe implementation compatibility, separately from deployment evidence.',properties:{target:text}},
|
|
18
|
+
{name:'get_capability',description:'Describe one catalog capability: schema fragment, constraints, grants, target support and bundled recipe/cookbook uses.',properties:{name:{type:'string',maxLength:64}},required:['name']},
|
|
19
|
+
{name:'get_schema',description:'Return the resolved JSON Schema fragment for a dotted urlcode.yaml path such as route, redirect or policies.cache.',properties:{path:{type:'string',maxLength:256}},required:['path']},
|
|
20
|
+
{name:'explain',description:'Explain the route a path selects from the compiled configuration: methods, handler, middleware, inputs, policies, cache outcome, bindings and target support. Nothing executes.',properties:{target:text},required:['target']},
|
|
21
|
+
{name:'get_manifest',description:'The generated semantic manifest: routes, capabilities, extensions, external requirements, functions, target support and the revision digest.',properties:{}},
|
|
22
|
+
{name:'import_preview',description:'Preview redirect conversion from supplied text; writes no files.',properties:{format,text:{type:'string',maxLength:524288},acceptProviderDifferences:{type:'boolean'}},required:['format','text']},
|
|
23
|
+
{name:'export_preview',description:'Preview redirect export from this project; writes no files.',properties:{format,acceptProviderDifferences:{type:'boolean'}},required:['format']},
|
|
24
|
+
{name:'recipes_list',description:'List bundled local recipes.',properties:{}},
|
|
25
|
+
{name:'recipes_show',description:'Show a bundled local recipe without writing it; metadata (capabilities, targets, grants, inputs, expected behavior) comes before file contents.',properties:{name:{type:'string',maxLength:64}},required:['name']},
|
|
26
|
+
{name:'search_recipes',description:'Search bundled recipes by id, description, tags and capabilities; local text matching, no service. Check here before generating a common route by hand.',properties:{text:{type:'string',maxLength:256}},required:['text']},
|
|
27
|
+
{name:'search_examples',description:'Search bundled runnable examples and the cookbook route index; returns the smallest matching example and its route.',properties:{text:{type:'string',maxLength:256}},required:['text']},
|
|
28
|
+
{name:'get_context',description:'Emit the compact project context an authoring agent needs: versions, project summary, constraints, target support and exact commands, derived from the compiled project. Optional token budget drops sections in a fixed order.',properties:{target:text,budget:{type:'integer',minimum:1}}},
|
|
29
|
+
];
|
|
30
|
+
// Only the operator's own --host-file exposes registered extension contracts; no tool argument can name one.
|
|
31
|
+
const hostDefinition={name:'get_extensions',description:'List operator-registered extension contracts with configuration and policy JSON Schemas and where the project mounts them; activates nothing.',properties:{}};
|
|
32
|
+
const ajv=new Ajv({strict:false});
|
|
33
|
+
const readTools=definitions.map(def=>({name:def.name,description:def.description,inputSchema:{type:'object',properties:def.properties,required:def.required??[],additionalProperties:false},annotations:{readOnlyHint:true,destructiveHint:false,openWorldHint:false}}));
|
|
34
|
+
const hostTool={name:hostDefinition.name,description:hostDefinition.description,inputSchema:{type:'object',properties:hostDefinition.properties,required:[],additionalProperties:false},annotations:{readOnlyHint:true,destructiveHint:false,openWorldHint:false}};
|
|
35
|
+
const authoringTools=authoringDefinitions.map(def=>({name:def.name,description:def.description,inputSchema:{type:'object',properties:def.properties,required:def.required,additionalProperties:false},annotations:{readOnlyHint:false,destructiveHint:false,openWorldHint:false}}));
|
|
36
|
+
const validators=new Map([...readTools,hostTool,...authoringTools].map(tool=>[tool.name,ajv.compile(tool.inputSchema)]));
|
|
37
|
+
function object(value ) {return value!==null&&typeof value==='object'&&!Array.isArray(value);}
|
|
38
|
+
/** `allowAuthoring` and `hostFile` are set only by the `--allow-authoring` and `--host-file` command-line flags; tool arguments and the environment never enable them. */
|
|
39
|
+
|
|
40
|
+
/** Operator selects the only project root. Read tools have no path, credential, write or execution authority; authoring tools write inside that root only. */
|
|
41
|
+
export async function serveMcp(options ) {
|
|
42
|
+
const project=await realpath(options.project),input=options.input??process.stdin,output=options.output??process.stdout;
|
|
43
|
+
const authoring=options.allowAuthoring===true,tools=[...readTools,...(options.hostFile===undefined?[]:[hostTool]),...(authoring?authoringTools:[])],names=new Set(tools.map(tool=>tool.name));
|
|
44
|
+
const host=await loadOperatorHost(options.hostFile,project);
|
|
45
|
+
try{await serve();}finally{await host.close?.();}
|
|
46
|
+
async function serve() {
|
|
47
|
+
let initialized=false,ready=false,pending=Buffer.alloc(0);
|
|
48
|
+
const send=async(value )=> {let line=JSON.stringify(value);if(Buffer.byteLength(line)>maxBytes)line=JSON.stringify({jsonrpc:'2.0',id:object(value)?value.id??null:null,error:{code:-32603,message:'Result exceeds output limit'}});if(!output.write(line+'\n'))await once(output,'drain');};
|
|
49
|
+
const error=(id ,code ,message )=>send({jsonrpc:'2.0',id,error:{code,message}});
|
|
50
|
+
const call=async(name ,args ) => {
|
|
51
|
+
const base=options.origin?{origin:options.origin}:{};
|
|
52
|
+
switch(name){
|
|
53
|
+
case 'inspect':return inspectProject(project,{...base,...args} );
|
|
54
|
+
case 'validate':return validateProject(project,base);
|
|
55
|
+
case 'capabilities':return getCapabilities(args.target );
|
|
56
|
+
case 'get_capability':return getCapability(args.name );
|
|
57
|
+
case 'get_schema':return getSchemaFragment(args.path );
|
|
58
|
+
case 'explain':return explainRoute(project,args.target ,base);
|
|
59
|
+
case 'get_manifest':return buildManifest(project,base);
|
|
60
|
+
case 'import_preview':return previewImport({format:args.format ,text:args.text ,acceptProviderDifferences:args.acceptProviderDifferences===true});
|
|
61
|
+
case 'export_preview':return previewExport(project,args.format ,args.acceptProviderDifferences===true);
|
|
62
|
+
case 'recipes_list':return listRecipes();
|
|
63
|
+
case 'recipes_show':return showRecipe(args.name );
|
|
64
|
+
case 'search_recipes':return searchRecipes(args.text );
|
|
65
|
+
case 'search_examples':return searchExamples(args.text );
|
|
66
|
+
case 'get_extensions':return describeExtensions(project,host.extensions??[]);
|
|
67
|
+
case 'get_context':return buildContext(project,{projectFlag:'.',...(typeof args.target==='string'?{target:args.target}:{}),...(typeof args.budget==='number'?{budget:args.budget}:{})});
|
|
68
|
+
default:if(authoring)return callAuthoringTool(project,name,args,options.origin);throw new Error('Unknown tool');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const line=async(bytes )=> {
|
|
72
|
+
let message ;try{message=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes));}catch{await error(null,-32700,'Parse error');return;}
|
|
73
|
+
if(!object(message)||message.jsonrpc!=='2.0'||typeof message.method!=='string'||('id'in message&&typeof message.id!=='string'&&!(typeof message.id==='number'&&Number.isSafeInteger(message.id)))) {await error(null,-32600,'Invalid request');return;}
|
|
74
|
+
const id=message.id;
|
|
75
|
+
if(id===undefined){if(message.method==='notifications/initialized'&&initialized)ready=true;return;}
|
|
76
|
+
const params=message.params??{};
|
|
77
|
+
if(!object(params)){await error(id,-32602,'Invalid params');return;}
|
|
78
|
+
if(message.method==='initialize') {
|
|
79
|
+
if(initialized){await error(id,-32600,'Already initialized');return;}
|
|
80
|
+
if(typeof params.protocolVersion!=='string'||!object(params.capabilities)||!object(params.clientInfo)||typeof params.clientInfo.name!=='string'||typeof params.clientInfo.version!=='string'){await error(id,-32602,'Invalid initialize params');return;}
|
|
81
|
+
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.0-alpha.2'}}});return;
|
|
82
|
+
}
|
|
83
|
+
if(message.method==='ping'){await send({jsonrpc:'2.0',id,result:{}});return;}
|
|
84
|
+
if(!ready){await error(id,-32002,'Initialize first');return;}
|
|
85
|
+
if(message.method==='tools/list'){await send({jsonrpc:'2.0',id,result:{tools}});return;}
|
|
86
|
+
if(message.method!=='tools/call'){await error(id,-32601,'Method not found');return;}
|
|
87
|
+
const name=params.name,args=params.arguments??{};
|
|
88
|
+
if(typeof name!=='string'||!names.has(name)||!validators.get(name) (args)){await error(id,-32602,'Invalid tool or arguments');return;}
|
|
89
|
+
try{const result=await call(name,args );await send({jsonrpc:'2.0',id,result:{content:[{type:'text',text:JSON.stringify(result)}]}});}catch{await send({jsonrpc:'2.0',id,result:{isError:true,content:[{type:'text',text:'Operation failed validation; inspect locally for details.'}]}});}
|
|
90
|
+
};
|
|
91
|
+
for await(const chunk of input){const bytes=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk );let start=0;
|
|
92
|
+
for(let index=0;index<bytes.length;index++){if(bytes[index]!==10)continue;if(pending.length+index-start>maxBytes){await error(null,-32600,'Message exceeds input limit');return;}const message=Buffer.concat([pending,bytes.subarray(start,index)]);pending=Buffer.alloc(0);start=index+1;await line(message);}
|
|
93
|
+
if(pending.length+bytes.length-start>maxBytes){await error(null,-32600,'Message exceeds input limit');return;}pending=Buffer.concat([pending,bytes.subarray(start)]);
|
|
94
|
+
}
|
|
95
|
+
if(pending.length)await error(null,-32700,'Truncated message');
|
|
96
|
+
}
|
|
97
|
+
}
|