@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/server.js
CHANGED
|
@@ -7,8 +7,6 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { createRuntime } from './runtime.js';
|
|
8
8
|
|
|
9
9
|
import { createJsonLogger } from './logging.js';
|
|
10
|
-
import { createLinkObserver } from './link-events.js';
|
|
11
|
-
|
|
12
10
|
import { createObserverSink, renderPrometheus } from './observability.js';
|
|
13
11
|
|
|
14
12
|
import { assert, HttpError } from './errors.js';
|
|
@@ -19,12 +17,12 @@ import { compileTrustedProxies, resolveClient } from './client-address.js';
|
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
|
|
@@ -83,7 +81,7 @@ function originForm(target ) {
|
|
|
83
81
|
export async function startServer({ project = '.', host = '127.0.0.1', port = 3000, watch = false,
|
|
84
82
|
local = false, log = createJsonLogger(),
|
|
85
83
|
maxBodyBytes = 1048576, maxInFlightRequests = 64, maxInFlightHealthRequests = 16,
|
|
86
|
-
requestLog = 'minimal', trustRequestId = false, origin,
|
|
84
|
+
requestLog = 'minimal', trustRequestId = false, origin, trustedProxies = [],
|
|
87
85
|
observers = [], metrics = false, metricsIntervalMs = 0, ...runtimeOptions } = {}) {
|
|
88
86
|
// Which peers may set X-Forwarded-For. Empty means the socket peer is the
|
|
89
87
|
// client for every policy; a forwarded header from anyone else is ignored.
|
|
@@ -105,9 +103,6 @@ export async function startServer({ project = '.', host = '127.0.0.1', port = 30
|
|
|
105
103
|
const sink = createObserverSink(observers, log);
|
|
106
104
|
const counters = sink.metrics;
|
|
107
105
|
const emit = (event , context ) => { try { sink(event, context); } catch { /* Logging cannot fail requests. */ } };
|
|
108
|
-
// Operator-supplied and explicitly enabled; route YAML cannot reach it and no
|
|
109
|
-
// callback is ever loaded from the project. Undefined leaves it off.
|
|
110
|
-
const observer = createLinkObserver(linkEvents, emit);
|
|
111
106
|
let current = await createRuntime(project, { local, log: emit, origin, ...runtimeOptions });
|
|
112
107
|
let shuttingDown = false, reloading = false, watching = false, interval , lastFingerprint , inFlight = 0, healthInFlight = 0;
|
|
113
108
|
const retired = new Set ();
|
|
@@ -128,24 +123,6 @@ export async function startServer({ project = '.', host = '127.0.0.1', port = 30
|
|
|
128
123
|
let status = 500;
|
|
129
124
|
res.on('error', () => {});
|
|
130
125
|
req.on('error', () => {});
|
|
131
|
-
{
|
|
132
|
-
// Enqueued after the response is over, so an observer can neither delay a
|
|
133
|
-
// redirect nor turn its own failure into one. A response that never
|
|
134
|
-
// finished is reported as aborted rather than counted as a click. The
|
|
135
|
-
// outcome is counted whether or not an operator collects link events.
|
|
136
|
-
let observed = false;
|
|
137
|
-
const settle = () => {
|
|
138
|
-
if (observed || !trace.link) return;
|
|
139
|
-
observed = true;
|
|
140
|
-
const event = { event: 'link_request', requestId, collection: trace.link.collection, route: trace.route ?? null,
|
|
141
|
-
code: trace.link.code, method, status,
|
|
142
|
-
outcome: trace.link.result === 'redirect' ? (res.writableFinished ? 'completed' : 'aborted') : trace.link.result,
|
|
143
|
-
durationMs: Math.round((performance.now() - started) * 100) / 100 };
|
|
144
|
-
counters.record(event);
|
|
145
|
-
if (observer) observer.emit(event);
|
|
146
|
-
};
|
|
147
|
-
res.once('finish', settle); res.once('close', settle);
|
|
148
|
-
}
|
|
149
126
|
try {
|
|
150
127
|
if (shuttingDown) throw new HttpError(503, 'Runtime shutting down');
|
|
151
128
|
let result ;
|
|
@@ -246,7 +223,6 @@ export async function startServer({ project = '.', host = '127.0.0.1', port = 30
|
|
|
246
223
|
}
|
|
247
224
|
return {
|
|
248
225
|
server, reload, address, root: current.root, testPlan: () => current.testPlan(),
|
|
249
|
-
linkEventStats: () => observer?.stats(),
|
|
250
226
|
metrics: snapshot,
|
|
251
227
|
get observers() { return observers.map(observer => ({ name: observer.name, version: observer.version })); },
|
|
252
228
|
// What a request sees as its own origin: behind a tunnel or proxy this is
|
|
@@ -260,8 +236,6 @@ export async function startServer({ project = '.', host = '127.0.0.1', port = 30
|
|
|
260
236
|
clearTimeout(deadline);
|
|
261
237
|
while (reloading) await new Promise(resolve => setTimeout(resolve,10));
|
|
262
238
|
await current.close(); await Promise.all(retired);
|
|
263
|
-
// Observers drain after the connections they describe are gone.
|
|
264
|
-
if (observer) emit({ event: 'link_observer', status: 'closed', ...await observer.close() });
|
|
265
239
|
// A final snapshot, then observers release in reverse order.
|
|
266
240
|
sink.publish(snapshot());
|
|
267
241
|
await sink.close();
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { egressUrl, safeEgressHeaders } from './egress.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function validateSignal(definition ) {if(!definition||typeof definition!=='object'||Object.keys(definition).some(key=>!['url','headers'].includes(key))) throw new Error('Invalid signal');egressUrl(definition.url);safeEgressHeaders(definition.headers||{});}
|
|
7
|
+
/** No queue, retries, ordering promise, or durable delivery. Counters contain no URL/header data. */
|
|
8
|
+
export class SignalBroker {
|
|
9
|
+
#onStats ; #client ; #concurrency ; #pending=new Set (); #controller=new AbortController();
|
|
10
|
+
#stats ={accepted:0,delivered:0,failed:0,dropped:0}; #closed=false;
|
|
11
|
+
constructor(client ,concurrency=8,onStats ) {if(!Number.isSafeInteger(concurrency)||concurrency<1||concurrency>128) throw new Error('Invalid signal concurrency');this.#onStats=onStats;this.#client=client;this.#concurrency=concurrency;}
|
|
12
|
+
get stats() {return {...this.#stats};}
|
|
13
|
+
emit(definition ,event ) {
|
|
14
|
+
if(this.#closed||this.#pending.size>=this.#concurrency) {this.#stats.dropped++;this.#notify();return false;}
|
|
15
|
+
try {validateSignal(definition);} catch {this.#stats.dropped++;this.#notify();return false;}
|
|
16
|
+
if(!event.route.startsWith('/')||event.route.length>2048||!/^[A-Z]{1,16}$/.test(event.method)||!Number.isInteger(event.status)||event.status<100||event.status>599) {this.#stats.dropped++;this.#notify();return false;}
|
|
17
|
+
this.#stats.accepted++;this.#notify();
|
|
18
|
+
const body=Buffer.from(JSON.stringify({version:1,route:event.route,status:event.status,method:event.method}));
|
|
19
|
+
const pending=Promise.resolve().then(()=>this.#client.request({url:definition.url,method:'POST',headers:{...definition.headers,'content-type':'application/json'},body,signal:this.#controller.signal})).then(result=>{if(result.status>=200&&result.status<300)this.#stats.delivered++;else this.#stats.failed++;},()=>{this.#stats.failed++;}).finally(()=>{this.#pending.delete(pending);this.#notify();});
|
|
20
|
+
this.#pending.add(pending);return true;
|
|
21
|
+
}
|
|
22
|
+
#notify() {try {this.#onStats?.(this.stats);} catch { /* Observers cannot alter delivery. */ }}
|
|
23
|
+
async close() {this.#closed=true;this.#controller.abort();await Promise.allSettled([...this.#pending]);}
|
|
24
|
+
}
|
package/dist/site.js
CHANGED
|
Binary file
|
package/dist/tooling.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {loadDocument} from './config.js';
|
|
2
|
+
import {applySite} from './site.js';
|
|
3
|
+
import {prepareFunctionSnapshot,requestedPermissions} from './policy.js';
|
|
4
|
+
import {compileRoutes,matchRoute,parseTarget} from './router.js';
|
|
5
|
+
import {compilePolicies,closePolicies} from './policies.js';
|
|
6
|
+
import {analyzeCompiledCapabilities,routeCapabilities} from './capabilities.js';
|
|
7
|
+
|
|
8
|
+
import {importRoutes,exportRoutes} from './interchange.js';
|
|
9
|
+
|
|
10
|
+
import {listRecipes,showRecipe,searchRecipes} from './recipes.js';
|
|
11
|
+
import {listExamples,searchExamples} from './examples.js';
|
|
12
|
+
|
|
13
|
+
import {effectiveExtensionPolicies} from './extensions.js';
|
|
14
|
+
|
|
15
|
+
import {loadOperatorHost} from './operator-host.js';
|
|
16
|
+
import {explainCompiledRoute,nearestRoutes} from './explain.js';
|
|
17
|
+
|
|
18
|
+
export {getCapabilities} from './capabilities.js';
|
|
19
|
+
export {getCapability} from './capability-query.js';
|
|
20
|
+
|
|
21
|
+
export {getSchemaFragment,schemaPathNames} from './schema-query.js';
|
|
22
|
+
|
|
23
|
+
export {listRecipes,showRecipe,searchRecipes,listExamples,searchExamples};
|
|
24
|
+
export {buildContext,renderContext,estimateTokens,documentationTokens} from './context.js';
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/** `extensions` are operator registrations from a host file; explain reports whether each requirement has a provider. Nothing is activated. */
|
|
28
|
+
|
|
29
|
+
function routesOf(table ) {return [...table.exact.values(),...[...table.byLength.values()].flat(),...table.mounts];}
|
|
30
|
+
export async function prepare(project ,options ={}) {
|
|
31
|
+
const loaded=await loadDocument(project);await applySite(loaded,{...(options.origin?{origin:options.origin}:{})});
|
|
32
|
+
const snapshot=await prepareFunctionSnapshot(loaded),bindings =Object.create(null);
|
|
33
|
+
for(const route of Object.values(loaded.routes)) {for(const ref of Object.values(route.env||{}))if(ref.env)bindings[ref.env]='validation-only';for(const ref of Object.values(route.secrets||{}))bindings[ref.secret]='validation-only';}
|
|
34
|
+
const compiled=await compileRoutes(loaded,bindings,requestedPermissions(loaded,snapshot),snapshot.projectSha256),routes=routesOf(compiled);
|
|
35
|
+
const shared ={target:'node',routes:routes.length,log:()=>{}};
|
|
36
|
+
// The same policy inventory the runtime attaches (src/runtime.ts): compiled for every route when the project declares any.
|
|
37
|
+
const anyPolicy=Boolean(loaded.document.policies)||routes.some(route=>route.policies),chains=new Map ();
|
|
38
|
+
try {for(const route of routes){const chain=await compilePolicies(loaded.document,route,{route,shared,target:'node',root:loaded.root});if(anyPolicy)chains.set(route.pattern,chain);}}finally{await closePolicies(shared);}
|
|
39
|
+
return {loaded,compiled,routes,chains,projectSha256:snapshot.projectSha256};
|
|
40
|
+
}
|
|
41
|
+
function compatibilitySummary(report ) {return {target:report.target,compatible:report.compatible,deployment:report.deployment,requirementCount:report.requirements.length,issueCount:report.issues.length};}
|
|
42
|
+
/** Semantic authoring inspection; no binding reads, sandbox execution or runtime activation. */
|
|
43
|
+
export async function inspectProject(project ,options ={}) {
|
|
44
|
+
const offset=options.offset??0,limit=options.limit??100;
|
|
45
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>1000)throw new Error('Invalid inspection page');
|
|
46
|
+
const {loaded,compiled,routes,projectSha256}=await prepare(project,options);
|
|
47
|
+
const report=analyzeCompiledCapabilities(loaded.document,compiled,options.target??'self-hosted',options.extensions);
|
|
48
|
+
return {format:1,projectSha256,routeCount:compiled.count,offset,limit,routes:routes.slice(offset,offset+limit).map(route=>({path:route.pattern,methods:route.methods,enabled:route.enabled!==false,capabilities:routeCapabilities(route,loaded.document)})),compatibility:{...compatibilitySummary(report),offset,limit,hasMore:offset+limit<report.issues.length,issues:report.issues.slice(offset,offset+limit)}};
|
|
49
|
+
}
|
|
50
|
+
export async function validateProject(project ,options ={}) {
|
|
51
|
+
const result=await inspectProject(project,{...options,offset:0,limit:1});
|
|
52
|
+
const {target,compatible,deployment,requirementCount,issueCount,issues}=result.compatibility;
|
|
53
|
+
return {valid:true,projectSha256:result.projectSha256,routeCount:result.routeCount,compatibility:{target,compatible,deployment,requirementCount,issueCount,firstIssue:issues[0]??null}};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Explain the route a path selects from the compiled IR: effective methods, handler, middleware, inputs, policies, cache outcome, bindings and target support. Nothing executes and no binding is read. */
|
|
57
|
+
export async function explainRoute(project ,target ,options ={}) {
|
|
58
|
+
const {loaded,compiled,chains,projectSha256}=await prepare(project,options),match=matchRoute(compiled,parseTarget(target));
|
|
59
|
+
if(!match)return {matched:false,nearest:nearestRoutes(target,routesOf(compiled).map(route=>route.pattern)),note:'No route selects this path.'};
|
|
60
|
+
return explainCompiledRoute(loaded,match.route,chains.get(match.route.pattern),{extensions:options.extensions,projectSha256});
|
|
61
|
+
}
|
|
62
|
+
/** Every route's explanation, in the router's precedence order. */
|
|
63
|
+
export async function explainProject(project ,options ={}) {
|
|
64
|
+
const {loaded,routes,chains,projectSha256}=await prepare(project,options);
|
|
65
|
+
return {projectSha256,routeCount:routes.length,routes:routes.map(route=>explainCompiledRoute(loaded,route,chains.get(route.pattern),{extensions:options.extensions,projectSha256}))};
|
|
66
|
+
}
|
|
67
|
+
export async function previewImport(options ) {return importRoutes(options);}
|
|
68
|
+
export async function previewExport(project ,format ,acceptProviderDifferences=false) {const loaded=await loadDocument(project);const {includes:_includes,...document}=loaded.document;return exportRoutes({format,document:{...document,routes:loaded.routes},acceptProviderDifferences});}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
/** Reports registered extension contracts against the project's declarations. Never activates an extension. */
|
|
75
|
+
export async function describeExtensions(project ,registrations ) {
|
|
76
|
+
const loaded=await loadDocument(project),{projectSha256}=await prepareFunctionSnapshot(loaded),routes=Object.entries(loaded.routes);
|
|
77
|
+
const declarations=Object.entries(loaded.document.extensions??{});
|
|
78
|
+
const mountsOf=(name )=>routes.filter(([,route])=>route.extension===name).map(([path])=>path.endsWith('/*')?path.slice(0,-2):path);
|
|
79
|
+
const policyRoutesOf=(name )=>routes.filter(([,route])=>Object.hasOwn(effectiveExtensionPolicies(loaded.document,route),name)).map(([path])=>path);
|
|
80
|
+
const registered=new Set((registrations??[]).map(registration=>registration.name));
|
|
81
|
+
const declared=declarations.map(([name,declaration])=>({name,version:String(declaration.version),registered:registered.has(name),mounts:mountsOf(name),policyRoutes:policyRoutesOf(name)}));
|
|
82
|
+
if(registrations===undefined)return {format:1,projectSha256,hostLoaded:false,note:'Configuration and policy schemas come from the operator host file; supply --host-file to print them.',extensions:[],declared};
|
|
83
|
+
const extensions=registrations.map(registration=>({
|
|
84
|
+
name:String(registration.name),version:String(registration.version),targets:Array.isArray(registration.targets)?registration.targets.map(String):[],
|
|
85
|
+
credentialHeaders:Array.isArray(registration.credentialHeaders)?registration.credentialHeaders.map(String):[],
|
|
86
|
+
schema:structuredClone(registration.schema??{}),policySchema:registration.policySchema?structuredClone(registration.policySchema):null,
|
|
87
|
+
declared:Object.hasOwn(loaded.document.extensions??{},registration.name),revisionPinned:registration.projectSha256===projectSha256,
|
|
88
|
+
mounts:mountsOf(registration.name),policyRoutes:policyRoutesOf(registration.name),
|
|
89
|
+
}));
|
|
90
|
+
return {format:1,projectSha256,hostLoaded:true,note:'Schemas describe operator-installed contracts; inspection activates nothing and grants no revision.',extensions,declared};
|
|
91
|
+
}
|
|
92
|
+
/** Executes the trusted operator host file to read its registrations, then releases it. */
|
|
93
|
+
export async function inspectExtensions(options ) {
|
|
94
|
+
const host=await loadOperatorHost(options.hostFile,options.project);
|
|
95
|
+
try{return await describeExtensions(options.project,options.hostFile===undefined?undefined:host.extensions??[]);}finally{await host.close?.();}
|
|
96
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Trusted (unsandboxed) execution for `function`/`middleware` routes that do
|
|
2
|
+
// not declare `sandbox: true` (docs/SPIKE-DEFAULT-TRUST-MODEL.md). This is the
|
|
3
|
+
// new default: an ordinary dynamic `import()` of the project's own module,
|
|
4
|
+
// called in the host process with full Node access — no worker thread, no
|
|
5
|
+
// QuickJS/WASM, no fresh-heap-per-call, no module-graph allowlist. The
|
|
6
|
+
// sandboxed path (FunctionPool, function-worker.ts) is untouched by this file
|
|
7
|
+
// and keeps its own guarantees exactly as before for any route that opts in
|
|
8
|
+
// with `sandbox: true`.
|
|
9
|
+
//
|
|
10
|
+
// The request/response contract mirrors the sandboxed path (guest-api.ts) as
|
|
11
|
+
// closely as an in-process call can: a `Request` and a `context` in, a
|
|
12
|
+
// `Response` out, the same middleware `(request, context, next)` chain with
|
|
13
|
+
// `next()` callable at most once and no arguments, and the same native-reply
|
|
14
|
+
// passthrough (a middleware chain wrapping a native reply — redirect, asset,
|
|
15
|
+
// static reply — can return that exact Response unchanged to avoid
|
|
16
|
+
// re-encoding its body). Node's own `Request`/`Response`/`Headers` are used
|
|
17
|
+
// instead of guest-api.ts's restricted classes, so a trusted function can
|
|
18
|
+
// return richer bodies (e.g. binary) than a sandboxed one can; that is a
|
|
19
|
+
// documented, intentional capability difference, not a contract violation.
|
|
20
|
+
//
|
|
21
|
+
// Deadline: unlike the sandboxed path, there is no interrupt mechanism that
|
|
22
|
+
// can stop trusted code running in the host's own event loop. `timeoutMs`
|
|
23
|
+
// here is an advisory race against the handler's promise settling — it
|
|
24
|
+
// rejects the *call* once the deadline passes, but cannot preempt trusted
|
|
25
|
+
// code that is blocking the event loop synchronously (a WASM interrupt has no
|
|
26
|
+
// equivalent in-process). This is a documented difference from the
|
|
27
|
+
// sandboxed path's forced worker termination; see docs/CAPACITY.md.
|
|
28
|
+
import { randomUUID } from 'node:crypto';
|
|
29
|
+
import { pathToFileURL } from 'node:url';
|
|
30
|
+
import { ConfigError, HttpError } from './errors.js';
|
|
31
|
+
import { routeFunctions } from './function-sources.js';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
export class TrustedFunctions {
|
|
45
|
+
timeoutMs ; maxBytes ; log ;
|
|
46
|
+
// Node's ESM loader caches a resolved module forever by URL, unlike a
|
|
47
|
+
// sandboxed worker, which gets a genuinely fresh module registry on every
|
|
48
|
+
// reload/restart. A snapshot reload constructs a brand-new TrustedFunctions
|
|
49
|
+
// (createRuntime runs again), so each instance gets its own cache-busting
|
|
50
|
+
// query string: reloaded code is re-imported and re-executed, matching the
|
|
51
|
+
// sandboxed pool's "new workers, new snapshot" reload contract, while a
|
|
52
|
+
// single instance still only imports each module once per process.
|
|
53
|
+
epoch = randomUUID();
|
|
54
|
+
constructor({ timeoutMs = 5000, maxBytes = 1048576, log = () => {} } = {}) {
|
|
55
|
+
this.timeoutMs = timeoutMs; this.maxBytes = maxBytes; this.log = log;
|
|
56
|
+
}
|
|
57
|
+
// Eagerly imports and validates every declared export exists as a function,
|
|
58
|
+
// the same guarantee FunctionPool.start() gives the sandboxed path: a
|
|
59
|
+
// broken function/middleware module fails runtime activation up front
|
|
60
|
+
// rather than the first request that happens to hit it.
|
|
61
|
+
async start(routes ) {
|
|
62
|
+
try { await Promise.all(routes.flatMap(routeFunctions).map(definition => this.loadExport(definition))); }
|
|
63
|
+
catch { throw new ConfigError('Function initialization failed (check module syntax, imports and exports)'); }
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
// No dependency allowlist, no relative-static-import-only rule and no
|
|
67
|
+
// per-module byte budget apply here — those are sandbox-snapshot
|
|
68
|
+
// constraints (function-sources.ts), not trusted-path ones. A trusted
|
|
69
|
+
// module may use bare specifiers, Node builtins, npm packages and dynamic
|
|
70
|
+
// import exactly like any other project code.
|
|
71
|
+
async loadExport(definition ) {
|
|
72
|
+
let mod ;
|
|
73
|
+
try { mod = await import(pathToFileURL(definition.source).href + '?urlcode-trusted-epoch=' + this.epoch) ; }
|
|
74
|
+
catch { throw new HttpError(502, 'Function execution failed'); }
|
|
75
|
+
const value = mod[definition.export];
|
|
76
|
+
if (typeof value !== 'function') throw new HttpError(502, 'Function execution failed');
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
async execute(route , request , context , native ) {
|
|
80
|
+
// The timer backing the deadline race must be cleared on every path —
|
|
81
|
+
// success or failure — or a completed call leaves a live Timeout behind
|
|
82
|
+
// for the full timeoutMs (see #138: 100 sequential calls, 100 leaked
|
|
83
|
+
// timers). `finally` covers both outcomes of the race in one place.
|
|
84
|
+
// The AbortController lets a fired deadline reach into `invoke`'s body
|
|
85
|
+
// read and cancel it immediately (#137), instead of letting a losing
|
|
86
|
+
// racer keep consuming an oversized or slow stream after this call has
|
|
87
|
+
// already rejected with 504.
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
let timer ;
|
|
90
|
+
const timeout = new Promise ((_, reject) => {
|
|
91
|
+
timer = setTimeout(() => { controller.abort(); reject(new HttpError(504, 'Function deadline exceeded')); }, this.timeoutMs);
|
|
92
|
+
timer.unref?.();
|
|
93
|
+
});
|
|
94
|
+
const invocation = this.invoke(route, request, context, native, controller.signal);
|
|
95
|
+
try { return await Promise.race([invocation, timeout]); }
|
|
96
|
+
catch (error) { throw error instanceof HttpError ? error : new HttpError(502, 'Function execution failed'); }
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
// `invocation` may still be running (e.g. blocked on a cancelled
|
|
100
|
+
// reader settling) after the race above has already returned; a later
|
|
101
|
+
// rejection from it must not surface as an unhandled rejection.
|
|
102
|
+
invocation.catch(() => {});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Reads a Response body incrementally, rejecting/cancelling as soon as
|
|
106
|
+
// `maxBytes` is exceeded instead of buffering the whole stream first
|
|
107
|
+
// (#137: a single `.arrayBuffer()` call reads everything unconditionally,
|
|
108
|
+
// so a configured limit only ever rejects *after* the oversized body has
|
|
109
|
+
// already been fully read into memory). Also cancels the reader as soon as
|
|
110
|
+
// `signal` aborts (the call's deadline firing), rather than letting a slow
|
|
111
|
+
// stream keep being pulled after the request has already timed out.
|
|
112
|
+
// Applies on every path that has a real body to measure, GET or HEAD alike
|
|
113
|
+
// (#139): the caller decides whether to transmit the bytes, this only
|
|
114
|
+
// decides how many there are, matching the convention the native/asset
|
|
115
|
+
// path already uses (the full body and its length are always determined
|
|
116
|
+
// up front; only `prepareResponse` in http-response.ts drops the body for
|
|
117
|
+
// HEAD at write time).
|
|
118
|
+
async readBody(response , signal ) {
|
|
119
|
+
if (response.body === null) return Buffer.alloc(0);
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
const cancel = () => { reader.cancel().catch(() => {}); };
|
|
122
|
+
if (signal.aborted) { cancel(); throw new HttpError(504, 'Function deadline exceeded'); }
|
|
123
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
124
|
+
try {
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
for (;;) {
|
|
128
|
+
const { done, value } = await reader.read();
|
|
129
|
+
if (done) break;
|
|
130
|
+
total += value.byteLength;
|
|
131
|
+
if (total > this.maxBytes) { cancel(); throw new HttpError(502, 'Function execution failed'); }
|
|
132
|
+
chunks.push(Buffer.from(value));
|
|
133
|
+
}
|
|
134
|
+
return Buffer.concat(chunks, total);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (signal.aborted) throw new HttpError(504, 'Function deadline exceeded');
|
|
137
|
+
throw error instanceof HttpError ? error : new HttpError(502, 'Function execution failed');
|
|
138
|
+
} finally { signal.removeEventListener('abort', cancel); }
|
|
139
|
+
}
|
|
140
|
+
async invoke(route , request , requestContext , native , signal ) {
|
|
141
|
+
// Matches guest-api.ts's __invokePipeline/__invoke: a fresh `state` object
|
|
142
|
+
// per invocation, shared across the whole middleware+handler chain for
|
|
143
|
+
// this one call only, never carried between requests.
|
|
144
|
+
const context = requestContext ;
|
|
145
|
+
context.state = {};
|
|
146
|
+
const hasBody = request.body !== undefined && request.body.length > 0 && !['GET','HEAD'].includes(request.method);
|
|
147
|
+
const req = new Request(request.url, {
|
|
148
|
+
method: request.method, headers: request.headers,
|
|
149
|
+
...(hasBody ? { body: request.body , duplex: 'half' } : {}),
|
|
150
|
+
});
|
|
151
|
+
let nativeResponse ;
|
|
152
|
+
if (native) {
|
|
153
|
+
const nativeBytes = native.body ;
|
|
154
|
+
const bodyless = [204,205,304].includes(native.status) || !nativeBytes || nativeBytes.length === 0;
|
|
155
|
+
nativeResponse = new Response(bodyless ? null : nativeBytes, { status: native.status, headers: native.headers });
|
|
156
|
+
}
|
|
157
|
+
const middleware = route.middleware || [];
|
|
158
|
+
const handlers = await Promise.all(middleware.map(definition => this.loadExport(definition))) ;
|
|
159
|
+
const entry = route.function ? (await this.loadExport(route.function) ) : undefined;
|
|
160
|
+
const dispatch = async (index ) => {
|
|
161
|
+
if (index === handlers.length) {
|
|
162
|
+
if (entry) return await entry(req, context);
|
|
163
|
+
if (nativeResponse) return nativeResponse;
|
|
164
|
+
throw new HttpError(502, 'Function execution failed');
|
|
165
|
+
}
|
|
166
|
+
let called = false, open = true, pending ;
|
|
167
|
+
const next = async (...args ) => {
|
|
168
|
+
if (args.length || called || !open) throw new TypeError('next may be called once during middleware');
|
|
169
|
+
called = true; pending = dispatch(index + 1); return pending;
|
|
170
|
+
};
|
|
171
|
+
let response ;
|
|
172
|
+
try { response = await handlers[index] (req, context, next); if (pending) await pending.catch(() => {}); }
|
|
173
|
+
finally { open = false; }
|
|
174
|
+
if (!(response instanceof Response)) throw new HttpError(502, 'Function execution failed');
|
|
175
|
+
return response;
|
|
176
|
+
};
|
|
177
|
+
const response = await dispatch(0);
|
|
178
|
+
if (!(response instanceof Response) || !Number.isInteger(response.status) || response.status < 200 || response.status > 599) throw new HttpError(502, 'Function execution failed');
|
|
179
|
+
const nativeBody = response === nativeResponse;
|
|
180
|
+
const headers = [...response.headers.entries()];
|
|
181
|
+
if (nativeBody && native) {
|
|
182
|
+
// The route returned the untouched native reply (possibly with extra
|
|
183
|
+
// headers a middleware added, e.g. `x-seen` above `location`): reject
|
|
184
|
+
// only if native's *own* metadata was changed, the same contract the
|
|
185
|
+
// sandboxed path enforces (function-worker.ts's "Native metadata
|
|
186
|
+
// changed" check) — an added header is fine, a changed one is not.
|
|
187
|
+
for (const key of new Set(native.headers.map(([k]) => k.toLowerCase()))) {
|
|
188
|
+
const originals = native.headers.filter(([k]) => k.toLowerCase() === key).map(([,v]) => v);
|
|
189
|
+
const current = headers.filter(([k]) => k.toLowerCase() === key).map(([,v]) => v);
|
|
190
|
+
if (JSON.stringify(originals) !== JSON.stringify(current)) throw new HttpError(502, 'Function execution failed');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Header count/byte limits apply either way, matching function-worker.ts.
|
|
194
|
+
{ let bytes = 0; for (const [k,v] of headers) bytes += Buffer.byteLength(k) + Buffer.byteLength(v) + 4;
|
|
195
|
+
if (headers.length > 256 || bytes > 16384) throw new HttpError(502, 'Function execution failed'); }
|
|
196
|
+
// Measured on HEAD too, not skipped: prepareResponse (http-response.ts)
|
|
197
|
+
// is what decides not to put the bytes on the wire for HEAD, but it
|
|
198
|
+
// still needs the real length rather than the 0 a skipped read leaves it
|
|
199
|
+
// to assume (#139).
|
|
200
|
+
const body = nativeBody && native
|
|
201
|
+
? ((native.body ) ?? Buffer.alloc(0))
|
|
202
|
+
: await this.readBody(response, signal);
|
|
203
|
+
return { status: response.status, headers, body, nativeBody,
|
|
204
|
+
...(nativeBody && native?.contentLength !== undefined ? { contentLength: native.contentLength } : {}) };
|
|
205
|
+
}
|
|
206
|
+
// Symmetry with FunctionPool.close(): nothing to release for the trusted
|
|
207
|
+
// path (no workers, no pending requests it owns), but the runtime can call
|
|
208
|
+
// either executor's close() uniformly during shutdown.
|
|
209
|
+
async close() { /* no owned resources */ }
|
|
210
|
+
}
|
package/dist/types/adapters.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { RuntimeExtension } from './extensions.ts';
|
|
1
2
|
import type { HostPlugin, OperatorPolicy, Runtime } from './runtime.ts';
|
|
2
|
-
import type { TargetName } from './types.ts';
|
|
3
3
|
/** The subset of process.env a hosted adapter reads. */
|
|
4
4
|
export type Environment = Record<string, string | undefined>;
|
|
5
|
-
|
|
6
|
-
target
|
|
5
|
+
interface NativeOnlyOptions {
|
|
6
|
+
target: 'aws' | 'vercel';
|
|
7
7
|
plugins?: HostPlugin[] | undefined;
|
|
8
|
+
extensions?: RuntimeExtension[] | undefined;
|
|
9
|
+
origin?: string | undefined;
|
|
8
10
|
}
|
|
9
11
|
export declare function readPolicyFromEnvironment(environment: Environment): OperatorPolicy | undefined;
|
|
10
|
-
export declare function activateNativeOnly(project: string, environment: Environment, { target, plugins }
|
|
12
|
+
export declare function activateNativeOnly(project: string, environment: Environment, { target, plugins, extensions, origin }: NativeOnlyOptions): Promise<Runtime>;
|
|
11
13
|
export declare function lazyRuntime<T>(activate: () => Promise<T>): () => Promise<T>;
|
|
12
14
|
export declare function resolveOrigin(origin: string | undefined, environment: Environment, platformVariables: string[]): string | undefined;
|
|
15
|
+
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AgentEntry, AgentsConfig } from './policies/agents.ts';
|
|
2
|
-
export declare const MAX_LIST_ENTRIES = 4096;
|
|
3
2
|
export declare function loadListFile(root: string, reference: string, routePattern?: string): Promise<AgentEntry[]>;
|
|
4
3
|
export declare function loadListFiles(references: string[], root: string | undefined, routePattern: string): Promise<Record<string, AgentEntry[]>>;
|
|
5
4
|
export declare function resolveLists(config: AgentsConfig | undefined, root: string, routePattern?: string): Promise<AgentsConfig | undefined>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Where the packaged agent skill lives, relative to the installed @jimhoyd/urlcode package. */
|
|
2
|
+
export declare const skillPath = "skills/urlcode/SKILL.md";
|
|
3
|
+
/** The MCP registration file `urlcode init` writes beside the project (Claude Code and Codex read this shape). */
|
|
4
|
+
export declare const mcpConfigFile = ".mcp.json";
|
|
5
|
+
/**
|
|
6
|
+
* Renders `.mcp.json` registering the read-only `urlcode mcp` server for the project at `project`, relative
|
|
7
|
+
* to the file. `--allow-authoring` is deliberately absent: the operator adds it by hand when they want it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderMcpConfig(project?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* The application-level AGENTS.md written by `urlcode init`. Built from the
|
|
12
|
+
* installed runtime's capability catalog, so it names only the handlers,
|
|
13
|
+
* policies and site keys this version implements. Under 80 lines by design.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderAgentsGuide({ routes }: {
|
|
16
|
+
routes: number;
|
|
17
|
+
}): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Authoring output never copies hidden state, package hooks, or credential files. */
|
|
2
|
+
export declare function authoringPath(path: string): string;
|
|
3
|
+
export declare function authoringFile(root: string, path: string, directory?: boolean): Promise<string>;
|
|
4
|
+
export declare function readAuthoringFile(root: string, path: string, limit: number): Promise<Buffer>;
|
|
5
|
+
/** Reserve a fresh directory exclusively, then publish the entry config last.
|
|
6
|
+
* Failed writes remove only the directory created by this invocation. No existing
|
|
7
|
+
* directory (even an empty one) is accepted; consumers see no activatable project
|
|
8
|
+
* until every dependency has been written. Callers must own the output parent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function publishAuthoringProject(output: string, files: ReadonlyMap<string, Buffer | string>, dryRun?: boolean): Promise<string>;
|
package/dist/types/aws.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RuntimeExtension } from './extensions.ts';
|
|
1
2
|
import type { Environment } from './adapters.ts';
|
|
2
3
|
import type { HostPlugin } from './runtime.ts';
|
|
3
4
|
export interface LambdaHandlerOptions {
|
|
@@ -6,6 +7,7 @@ export interface LambdaHandlerOptions {
|
|
|
6
7
|
environment?: Environment | undefined;
|
|
7
8
|
maxBodyBytes?: number | undefined;
|
|
8
9
|
plugins?: HostPlugin[] | undefined;
|
|
10
|
+
extensions?: RuntimeExtension[] | undefined;
|
|
9
11
|
}
|
|
10
12
|
/** A Lambda payload format 2.0 event, as far as this adapter reads it. */
|
|
11
13
|
export interface LambdaEvent {
|
|
@@ -32,4 +34,4 @@ export interface LambdaResponse {
|
|
|
32
34
|
isBase64Encoded: true;
|
|
33
35
|
}
|
|
34
36
|
export type LambdaHandler = (event: unknown) => Promise<LambdaResponse>;
|
|
35
|
-
export declare function createLambdaHandler({ project, origin, environment, maxBodyBytes, plugins }?: LambdaHandlerOptions): LambdaHandler;
|
|
37
|
+
export declare function createLambdaHandler({ project, origin, environment, maxBodyBytes, plugins, extensions }?: LambdaHandlerOptions): LambdaHandler;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LogFn } from './types.ts';
|
|
2
|
+
export interface BuildOptions {
|
|
3
|
+
out?: string | undefined;
|
|
4
|
+
origin?: string | undefined;
|
|
5
|
+
log?: LogFn | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildReport {
|
|
8
|
+
out: string;
|
|
9
|
+
format: number;
|
|
10
|
+
version: string;
|
|
11
|
+
routes: number;
|
|
12
|
+
redirects: number;
|
|
13
|
+
files: number;
|
|
14
|
+
manifest: string;
|
|
15
|
+
}
|
|
16
|
+
/** One S3 per-object website redirect: a zero-byte object at `key` carrying
|
|
17
|
+
* `x-amz-website-redirect-location: location`, which S3 always answers with a
|
|
18
|
+
* 301 regardless of the object's own metadata. */
|
|
19
|
+
export interface StaticRedirect {
|
|
20
|
+
key: string;
|
|
21
|
+
location: string;
|
|
22
|
+
status: 301;
|
|
23
|
+
}
|
|
24
|
+
/** One served file: the bytes are written under `out/objects/<key>`; the
|
|
25
|
+
* metadata here is what a deploy step sets as the S3 object's own headers. */
|
|
26
|
+
export interface StaticObject {
|
|
27
|
+
key: string;
|
|
28
|
+
contentType: string;
|
|
29
|
+
cacheControl?: string;
|
|
30
|
+
contentDisposition?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RedirectManifest {
|
|
33
|
+
format: number;
|
|
34
|
+
redirects: StaticRedirect[];
|
|
35
|
+
}
|
|
36
|
+
export interface ObjectManifest {
|
|
37
|
+
format: number;
|
|
38
|
+
objects: StaticObject[];
|
|
39
|
+
}
|
|
40
|
+
declare function objectKey(pattern: string): string;
|
|
41
|
+
export declare function buildStatic(project: string, { out, origin, log }?: BuildOptions): Promise<BuildReport>;
|
|
42
|
+
export declare const staticObjectKey: typeof objectKey;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ConversionDiagnostic } from './interchange.ts';
|
|
2
|
+
export type BulkFormat = 'csv' | 'json' | 'yaml';
|
|
3
|
+
export interface BulkFilePlan {
|
|
4
|
+
path: string;
|
|
5
|
+
routeCount: number;
|
|
6
|
+
firstPath?: string;
|
|
7
|
+
lastPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BulkImportReport {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
output?: string;
|
|
13
|
+
routeCount: number;
|
|
14
|
+
diagnostics: ConversionDiagnostic[];
|
|
15
|
+
files: BulkFilePlan[];
|
|
16
|
+
source: {
|
|
17
|
+
name: string;
|
|
18
|
+
format: BulkFormat;
|
|
19
|
+
sha256: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Import ordinary redirect rows into small, portable include files. There is no
|
|
23
|
+
* merge mode: duplicate rules and existing output directories are refused. */
|
|
24
|
+
export declare function importBulkProject(text: string, format: BulkFormat, output: string, { dryRun, source }?: {
|
|
25
|
+
dryRun?: boolean | undefined;
|
|
26
|
+
source?: string | undefined;
|
|
27
|
+
}): Promise<BulkImportReport>;
|