@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/cli.js
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { getCapabilities, formatCapabilities } from './capabilities.js';
|
|
3
|
+
import { getCapability, formatCapability } from './capability-query.js';
|
|
4
|
+
import { getSchemaFragment } from './schema-query.js';
|
|
5
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
2
6
|
import { auditProject, benchmarkProject } from './readiness.js';
|
|
3
7
|
|
|
4
8
|
import { parseArgs } from 'node:util';
|
|
5
9
|
import { createRuntime } from './runtime.js';
|
|
10
|
+
import { loadOperatorHost } from './operator-host.js';
|
|
11
|
+
|
|
6
12
|
import { startServer } from './server.js';
|
|
7
13
|
|
|
8
14
|
import {scaffoldProject} from './scaffold.js';
|
|
9
15
|
import { initProject, addRedirect } from './authoring.js';
|
|
16
|
+
import { initProjectWith, parseWithNames } from './init-with.js';
|
|
10
17
|
import { runProjectTests } from './project-tests.js';
|
|
18
|
+
import { verifyDeployment, failLevels } from './verify-deployment.js';
|
|
19
|
+
|
|
11
20
|
import { loadOperatorPolicy, prepareFunctionSnapshot, requestedPermissions } from './policy.js';
|
|
12
21
|
import { loadDocument } from './config.js';
|
|
13
|
-
import {
|
|
22
|
+
import { describeExtensions } from './tooling.js';
|
|
23
|
+
|
|
14
24
|
import { ConfigError, HttpError } from './errors.js';
|
|
15
|
-
import {supportsConcurrentWal} from './sqlite-version.js';
|
|
16
25
|
import { registry as policyRegistry } from './policies.js';
|
|
17
26
|
import { loadComplianceRules, profileNames as complianceProfiles } from './compliance.js';
|
|
27
|
+
import { parseRouteSnapshot, diffRoutes, renderRouteDiff } from './route-diff.js';
|
|
28
|
+
import { readFile } from 'node:fs/promises';
|
|
18
29
|
|
|
19
|
-
const usage = `URLCode 0.
|
|
20
|
-
urlcode init <directory>
|
|
30
|
+
const usage = `URLCode 0.4.0-alpha.2 — local/self-hosted runtime
|
|
31
|
+
urlcode init <directory> [--with auth,admin] # --with: layered site from installed @jimhoyd/urlcode-<name> packages
|
|
21
32
|
urlcode scaffold [--project directory] [--dry-run]
|
|
22
33
|
urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
|
|
23
34
|
urlcode dev [--project directory] [--port 3000] [--host 127.0.0.1]
|
|
@@ -28,35 +39,53 @@ const usage = `URLCode 0.3.0 — local/self-hosted runtime
|
|
|
28
39
|
policies: [--trusted-proxies 10.0.0.0/8,fd00::/8] # peers allowed to set X-Forwarded-For for client policies
|
|
29
40
|
urlcode add <destination-url> [--alias short-code] [--project directory]
|
|
30
41
|
urlcode test [--project directory] [--origin https://links.example]
|
|
31
|
-
urlcode build --target cloudflare [--project directory] [--out dist/cloudflare] [--origin https://links.example]
|
|
42
|
+
urlcode build --target cloudflare|static [--project directory] [--out dist/cloudflare|dist/static] [--origin https://links.example]
|
|
43
|
+
# static: redirect/respond/page/static/download only, compiled for S3 + CloudFront; no server, see docs/STATIC.md
|
|
32
44
|
urlcode routes [--project directory] [--origin https://links.example]
|
|
45
|
+
diff: [--compare previous-routes.json] [--format json|markdown] # added/removed/changed routes against an earlier report; always exits 0
|
|
33
46
|
urlcode audit [--project directory] [--expect-routes 2]
|
|
34
47
|
compliance: [--compliance baseline|strict|privacy|none] [--compliance-rules /absolute/rules.mjs] [--compliance-ignore id,id]
|
|
35
48
|
[--compliance-warn] [--origin https://links.example] [--request-log minimal|detailed] # declare the deployment under review
|
|
36
49
|
urlcode benchmark [--project directory] [--requests 1000] [--concurrency 2] [--seconds 30] [--max-p95-ms 50]
|
|
37
50
|
[--warmup 50] [--target https://links.example] # target measures a running deployment, not a local snapshot
|
|
38
|
-
urlcode
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
urlcode verify-deployment --target https://links.example [--project directory] [--origin https://links.example]
|
|
52
|
+
[--expect-routes 2] [--expect-metrics] [--timeout-ms 10000] [--fail-on high|medium|low|info|none]
|
|
53
|
+
[--compliance baseline|strict|privacy|none] [--compliance-rules ...] [--compliance-ignore id,id] [--compliance-warn]
|
|
54
|
+
# compares the running deployment's responses with what this project declares; never follows redirects, no --insecure
|
|
55
|
+
urlcode permissions [--project directory] # inspect requested bindings and egress origins; grants nothing
|
|
56
|
+
urlcode explain [/route] [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--host-file ...] [--json]
|
|
57
|
+
# effective methods, handler, middleware, inputs, policies, cache outcome, bindings and target support from the compiled configuration
|
|
58
|
+
urlcode manifest [--project directory] [--json] # generated semantic manifest; build writes the same file as manifest.json
|
|
59
|
+
urlcode extensions [--project directory] [--host-file /absolute/operator/host.mjs] [--json] # registered contracts and schemas; executes trusted host code, activates nothing
|
|
60
|
+
urlcode import [netlify|cloudflare|vercel|netlify-toml] <file> [--format csv|json|yaml] [--out new-file] [--dry-run] [--report json]
|
|
61
|
+
urlcode export --target netlify|cloudflare|vercel|netlify-toml|csv|json|yaml [--project directory] [--out new-file] [--report json]
|
|
62
|
+
conversion: [--accept-provider-differences] # explicit non-lossless migration candidate; exact behavior requires runtime
|
|
63
|
+
urlcode recipes [list|search <text>|show <name>|add <name> --out new-directory] [--dry-run] [--json]
|
|
64
|
+
urlcode examples [list|search <text>] [--json] # bundled runnable examples and the cookbook route index
|
|
65
|
+
urlcode build-typescript [--project directory] --out new-directory [--dry-run]
|
|
66
|
+
urlcode bulk-import csv|json|yaml <file> --out new-directory [--dry-run]
|
|
67
|
+
urlcode verify-provider --target self-hosted|aws|vercel|cloudflare --origin https://owned-fixture.example
|
|
68
|
+
[--timeout-ms 3000] [--release label] [--git-commit sha] # explicitly invokes synthetic deployment probes
|
|
69
|
+
urlcode mcp [--project directory] [--allow-authoring] [--host-file ...] # bounded stdio tooling; --allow-authoring adds project-confined authoring tools, host file adds get_extensions
|
|
70
|
+
urlcode capabilities [--target self-hosted|cloudflare|aws|vercel|static] [--json]
|
|
71
|
+
urlcode capabilities <name> [--json] # one catalog entry: schema fragment, constraints, grants, targets, bundled uses
|
|
72
|
+
urlcode schema <path> [--json|--yaml] # schema fragment for route, redirect, policies.cache, site.sitemap, ...
|
|
73
|
+
urlcode context [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--budget 500] [--json] [--stats]
|
|
74
|
+
# compact facts for an authoring agent from the compiled project; --stats compares estimated tokens with the docs
|
|
45
75
|
urlcode doctor
|
|
46
|
-
serve/dev/validate/test/routes/audit/benchmark: --
|
|
47
|
-
Store pool controls: --link-readers 2 (1–8), --link-read-limit 32, --link-write-limit 32 (1–32 each)
|
|
76
|
+
serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp: --host-file /absolute/operator/host.mjs (trusted code outside project)
|
|
48
77
|
Dev loads .env.local and watches; serve does neither. Functions run in WASM isolation; external bindings require --policy outside the project.
|
|
49
78
|
`;
|
|
50
79
|
const print = (value ) => process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value) + '\n');
|
|
51
80
|
const options = {
|
|
52
|
-
|
|
81
|
+
json:{ type:'boolean' }, yaml:{ type:'boolean' }, report:{type:'string'}, 'accept-provider-differences':{type:'boolean'},
|
|
82
|
+
project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
|
|
53
83
|
port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
|
|
54
84
|
'expect-routes':{type:'string'}, requests:{type:'string'}, concurrency:{type:'string'}, seconds:{type:'string'}, 'max-p95-ms':{type:'string'}, warmup:{type:'string'}, target:{type:'string'},
|
|
55
|
-
'link-readers':{type:'string'}, 'link-read-limit':{type:'string'}, 'link-write-limit':{type:'string'},
|
|
56
85
|
workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
|
|
57
86
|
'max-in-flight':{type:'string'}, 'max-in-flight-health':{type:'string'}, 'request-log':{type:'string'}, 'trust-request-id':{type:'boolean'}, 'trusted-proxies':{type:'string'}, metrics:{type:'boolean'},
|
|
58
|
-
|
|
59
|
-
out:{type:'string'}, 'dry-run':{type:'boolean'}, compliance:{type:'string'}, 'compliance-rules':{type:'string'}, 'compliance-ignore':{type:'string'}, 'compliance-warn':{type:'boolean'}, policy:{ type:'string' }, origin:{ type:'string' }, alias:{ type:'string' }, local:{ type:'boolean' }, help:{ type:'boolean', short:'h' },
|
|
87
|
+
release:{type:'string'}, 'git-commit':{type:'string'}, 'timeout-ms':{type:'string'}, 'fail-on':{type:'string'}, 'expect-metrics':{type:'boolean'},
|
|
88
|
+
budget:{type:'string'}, stats:{type:'boolean'}, out:{type:'string'}, 'dry-run':{type:'boolean'}, compare:{type:'string'}, format:{type:'string'}, compliance:{type:'string'}, 'compliance-rules':{type:'string'}, 'compliance-ignore':{type:'string'}, 'compliance-warn':{type:'boolean'}, policy:{ type:'string' }, origin:{ type:'string' }, alias:{ type:'string' }, local:{ type:'boolean' }, 'allow-authoring':{ type:'boolean' }, help:{ type:'boolean', short:'h' },
|
|
60
89
|
} ;
|
|
61
90
|
|
|
62
91
|
|
|
@@ -91,21 +120,67 @@ async function complianceOptions(values )
|
|
|
91
120
|
if(!complianceProfiles.includes(profile))throw new ConfigError(`Use --compliance ${complianceProfiles.join('|')}`);
|
|
92
121
|
const ignore=(values['compliance-ignore'] ?? '').split(',').map(id=>id.trim()).filter(Boolean);
|
|
93
122
|
const operator=await loadComplianceRules(values['compliance-rules'],values.project);
|
|
94
|
-
const host={requestLog:values['request-log'] ?? 'minimal'
|
|
123
|
+
const host={requestLog:values['request-log'] ?? 'minimal'};
|
|
95
124
|
if(!['minimal','detailed'].includes(host.requestLog))throw new ConfigError('Use --request-log minimal or detailed');
|
|
96
125
|
return {profile,ignore,origin:values.origin,host,rules:operator?.rules ?? [],disable:operator?.disable ?? [],override:operator?.override ?? {}};
|
|
97
126
|
}
|
|
127
|
+
function formatExtensions(report ) {
|
|
128
|
+
const lines = [`Project revision: ${report.projectSha256}`];
|
|
129
|
+
for (const item of report.declared) lines.push(`Declared: ${item.name} (contract ${item.version}) ${item.registered ? 'registered' : report.hostLoaded ? 'NOT registered by the host file' : 'schemas need --host-file'}`, ` mounts: ${item.mounts.join(', ') || '(none)'}`, ` policy routes: ${item.policyRoutes.join(', ') || '(none)'}`);
|
|
130
|
+
if (!report.declared.length) lines.push('Declared: (none)');
|
|
131
|
+
for (const item of report.extensions) lines.push(`Registered: ${item.name} (contract ${item.version}; targets ${item.targets.join(', ') || '(none)'}; ${item.declared ? 'declared' : 'not declared'}; revision ${item.revisionPinned ? 'pinned' : 'NOT pinned'})`,
|
|
132
|
+
` mounts: ${item.mounts.join(', ') || '(none)'}`, ` policy routes: ${item.policyRoutes.join(', ') || '(none)'}`, ` credential headers: ${item.credentialHeaders.join(', ') || '(none)'}`,
|
|
133
|
+
` configuration schema: ${JSON.stringify(item.schema)}`, ` policy schema: ${item.policySchema ? JSON.stringify(item.policySchema) : '(none)'}`);
|
|
134
|
+
lines.push(report.note);
|
|
135
|
+
return lines.join('\n') + '\n';
|
|
136
|
+
}
|
|
98
137
|
const errorMessages = { ERR_PARSE_ARGS_UNKNOWN_OPTION:'Unknown option; use --help', EEXIST:'Destination or edit lock already exists', ENOENT:'Required file or directory not found', EADDRINUSE:'Port is already in use', EACCES:'Permission denied' };
|
|
138
|
+
let operatorHost = {};
|
|
139
|
+
let serving = false;
|
|
99
140
|
try {
|
|
100
141
|
const { values, positionals } = parseArgs({ allowPositionals:true, options });
|
|
101
142
|
const [command, arg, ...extra] = positionals;
|
|
102
|
-
values.port ??=
|
|
143
|
+
values.port ??= '3000';
|
|
103
144
|
if (values.help || !command) print(usage);
|
|
104
145
|
else {
|
|
105
|
-
if (
|
|
106
|
-
|
|
146
|
+
if (values['host-file'] !== undefined) {
|
|
147
|
+
if (!['serve','dev','validate','test','routes','audit','benchmark','explain','context','extensions','mcp'].includes(command)) throw new ConfigError('--host-file is only supported by serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp');
|
|
148
|
+
// The MCP server and context command load and release the host themselves.
|
|
149
|
+
if (command !== 'mcp' && command !== 'context') operatorHost = await loadOperatorHost(values['host-file'], values.project);
|
|
150
|
+
}
|
|
151
|
+
if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
|
|
152
|
+
if (values['allow-authoring'] && command !== 'mcp') throw new ConfigError('--allow-authoring is only supported by mcp');
|
|
153
|
+
const hostOptions = { extensions: operatorHost.extensions, plugins: operatorHost.plugins };
|
|
154
|
+
if ((!['import','recipes','recipe','examples','example','bulk-import'].includes(command) && extra.length) || (!['init','add','import','recipes','recipe','examples','example','bulk-import','explain','capabilities','schema'].includes(command) && arg)) throw new ConfigError('Unexpected positional arguments');
|
|
155
|
+
|
|
156
|
+
if(command==='import'||command==='export'){
|
|
157
|
+
const { runInterchange } = await import('./interchange-cli.js');
|
|
158
|
+
const converted = await runInterchange(command,positionals.slice(1),{project:values.project,target:values.target,format:values.format,out:values.out,report:values.report,dryRun:values['dry-run'],acceptProviderDifferences:values['accept-provider-differences']});
|
|
159
|
+
print(converted.text); if(!converted.report.ok)process.exitCode=1;
|
|
160
|
+
}else if(['recipes','recipe','examples','example','build-typescript','bulk-import','verify-provider','mcp'].includes(command)){
|
|
161
|
+
const {runEcosystemCommand}=await import('./ecosystem-cli.js');
|
|
162
|
+
await runEcosystemCommand(command,positionals.slice(1),values,print);
|
|
163
|
+
}else if(command==='explain'||command==='manifest'){
|
|
164
|
+
const {runExplainCommand}=await import('./explain-cli.js');
|
|
165
|
+
const exitCode=await runExplainCommand(command,arg,{project:values.project,target:values.target,origin:values.origin,json:values.json,extensions:operatorHost.extensions},print);
|
|
166
|
+
if(exitCode)process.exitCode=exitCode;
|
|
167
|
+
}else if(command==='capabilities'){
|
|
168
|
+
if(arg!==undefined){ if(values.target!==undefined)throw new ConfigError('--target applies to the full catalog, not one entry'); const entry=getCapability(arg); print(values.json ? entry : formatCapability(entry)); }
|
|
169
|
+
else { const catalog = getCapabilities(values.target); print(values.json ? catalog : formatCapabilities(catalog)); }
|
|
170
|
+
}else if(command==='schema'){
|
|
171
|
+
if(arg===undefined)throw new ConfigError('Use urlcode schema <path>');
|
|
172
|
+
const fragment=getSchemaFragment(arg);
|
|
173
|
+
print(values.yaml ? stringifyYaml(fragment.schema) : JSON.stringify(fragment.schema,null,2)+'\n');
|
|
174
|
+
}else if(command==='context'){
|
|
175
|
+
if (values.budget !== undefined && !/^\d{1,9}$/.test(values.budget)) throw new ConfigError('Invalid --budget');
|
|
176
|
+
const { buildContext, renderContext, estimateTokens, documentationTokens } = await import('./context.js');
|
|
177
|
+
const context = await buildContext(values.project, { target:values.target, hostFile:values['host-file'], ...(values.budget === undefined ? {} : { budget:Number(values.budget) }) });
|
|
178
|
+
const text = values.json ? JSON.stringify(context) + '\n' : renderContext(context);
|
|
179
|
+
print(text);
|
|
180
|
+
// Estimates only (characters / 4); a tokenizer is not a dependency. Stats go to stderr so stdout stays parseable.
|
|
181
|
+
if (values.stats) process.stderr.write(JSON.stringify({ event:'stats', estimate:'characters/4', documentationTokens:await documentationTokens(), contextTokens:estimateTokens(text) }) + '\n');
|
|
182
|
+
}else{
|
|
107
183
|
const permissions = await loadOperatorPolicy(values.policy,values.project);
|
|
108
|
-
const linkStore=parseLinkBinding(values['link-store'],linkPoolOptions(values));
|
|
109
184
|
switch (command) {
|
|
110
185
|
case 'routes': case 'audit': case 'benchmark': {
|
|
111
186
|
const number = (key ,fallback ) => {
|
|
@@ -117,12 +192,22 @@ try {
|
|
|
117
192
|
const expected=number('expect-routes');
|
|
118
193
|
if(expected!==undefined && !Number.isSafeInteger(expected))throw new ConfigError('Expected route count must be an integer');
|
|
119
194
|
const compliance=command==='audit'?await complianceOptions(values):undefined;
|
|
195
|
+
const format=values.format ?? 'json';
|
|
196
|
+
if(!['json','markdown'].includes(format))throw new ConfigError('Use --format json or markdown');
|
|
197
|
+
if(values.format!==undefined && values.compare===undefined)throw new ConfigError('--format applies to routes --compare');
|
|
120
198
|
const started=performance.now();
|
|
121
|
-
const app=await startServer({project:values.project,port:0,local:true,permissions,
|
|
199
|
+
const app=await startServer({...hostOptions,project:values.project,port:0,local:true,permissions,origin:values.origin,log:()=>{}});
|
|
122
200
|
const startupMs=performance.now()-started;
|
|
123
201
|
try {
|
|
124
202
|
if(command==='routes') {
|
|
125
|
-
const plan=app.testPlan();
|
|
203
|
+
const plan=app.testPlan();
|
|
204
|
+
if(values.compare===undefined) print({routes:plan.inventory.length,inventory:plan.inventory,policies:plan.policies});
|
|
205
|
+
else {
|
|
206
|
+
// The diff reports; it never judges, so the exit code stays 0.
|
|
207
|
+
const before=parseRouteSnapshot(JSON.parse(await readFile(values.compare,'utf8'))); // file boundary: an earlier `routes` report
|
|
208
|
+
const diff=diffRoutes(before,{inventory:plan.inventory,policies:plan.policies});
|
|
209
|
+
print(format==='markdown'?renderRouteDiff(diff):diff);
|
|
210
|
+
}
|
|
126
211
|
} else if(command==='audit') {
|
|
127
212
|
const report=await auditProject(app,{expectRoutes:expected,log:print,compliance});print(report);if(!report.ready)process.exitCode=1;
|
|
128
213
|
if(report.compliance && !report.compliance.pass && !values['compliance-warn'])process.exitCode=1;
|
|
@@ -134,43 +219,85 @@ try {
|
|
|
134
219
|
} finally {await app.close();}
|
|
135
220
|
break;
|
|
136
221
|
}
|
|
222
|
+
case 'verify-deployment': {
|
|
223
|
+
if (!values.target) throw new ConfigError('Provide --target https://host');
|
|
224
|
+
const integer = (key ) => {
|
|
225
|
+
const value = values[key];
|
|
226
|
+
if (value === undefined) return undefined;
|
|
227
|
+
if (!/^\d{1,9}$/.test(value)) throw new ConfigError(`Invalid --${key}`);
|
|
228
|
+
return Number(value);
|
|
229
|
+
};
|
|
230
|
+
const failOn = values['fail-on'] ?? 'high';
|
|
231
|
+
const isFailOn = (value ) => (failLevels ).includes(value);
|
|
232
|
+
if (!isFailOn(failOn)) throw new ConfigError(`Use --fail-on ${failLevels.join('|')}`);
|
|
233
|
+
const report = await verifyDeployment(values.project, { target: values.target, origin: values.origin, expectRoutes: integer('expect-routes'), timeoutMs: integer('timeout-ms'),
|
|
234
|
+
expectMetrics: values['expect-metrics'], failOn, compliance: await complianceOptions(values), complianceWarn: values['compliance-warn'], permissions, log: print });
|
|
235
|
+
print(report); if (!report.pass) process.exitCode = 1; break;
|
|
236
|
+
}
|
|
237
|
+
case 'verify-provider': {
|
|
238
|
+
const target=values.target;
|
|
239
|
+
if(target!=='self-hosted'&&target!=='aws'&&target!=='vercel'&&target!=='cloudflare')throw new ConfigError('Provide --target self-hosted|aws|vercel|cloudflare');
|
|
240
|
+
if(!values.origin)throw new ConfigError('Provide --origin https://owned-fixture.example');
|
|
241
|
+
if(values['timeout-ms']!==undefined&&!/^\d{1,5}$/.test(values['timeout-ms']))throw new ConfigError('Invalid --timeout-ms');
|
|
242
|
+
const {verifyProviderDeployment}=await import('./provider-verification.js');
|
|
243
|
+
const report=await verifyProviderDeployment(target,values.origin,{
|
|
244
|
+
...(values['timeout-ms']===undefined?{}:{timeoutMs:Number(values['timeout-ms'])}),
|
|
245
|
+
...(values.release===undefined?{}:{release:values.release}),
|
|
246
|
+
...(values['git-commit']===undefined?{}:{gitCommit:values['git-commit']}),
|
|
247
|
+
});
|
|
248
|
+
print(report);if(!report.pass)process.exitCode=1;break;
|
|
249
|
+
}
|
|
137
250
|
case 'build': {
|
|
138
|
-
if (values.target
|
|
251
|
+
if (values.target === 'static') {
|
|
252
|
+
const { buildStatic } = await import('./build-static.js');
|
|
253
|
+
print({ event:'built', ...await buildStatic(values.project,{ out:values.out, origin:values.origin }) }); break;
|
|
254
|
+
}
|
|
255
|
+
if (values.target !== 'cloudflare') throw new ConfigError('Use --target cloudflare or static');
|
|
139
256
|
const { buildCloudflare } = await import('./build-cloudflare.js');
|
|
140
257
|
print({ event:'built', ...await buildCloudflare(values.project,{ out:values.out, origin:values.origin }) }); break;
|
|
141
258
|
}
|
|
142
259
|
case 'scaffold':
|
|
143
260
|
print(await scaffoldProject(values.project,{dryRun:values['dry-run']}));break;
|
|
261
|
+
case 'extensions': {
|
|
262
|
+
const report = await describeExtensions(values.project, values['host-file'] === undefined ? undefined : operatorHost.extensions ?? []);
|
|
263
|
+
print(values.json ? report : formatExtensions(report)); break;
|
|
264
|
+
}
|
|
144
265
|
case 'permissions': {
|
|
145
266
|
const loaded = await loadDocument(values.project);
|
|
146
267
|
print(requestedPermissions(loaded,await prepareFunctionSnapshot(loaded))); break;
|
|
147
268
|
}
|
|
148
269
|
case 'init':
|
|
149
270
|
if (!arg) throw new ConfigError('Provide a new project directory');
|
|
150
|
-
await initProject(arg); print({ event:'created' }); break;
|
|
271
|
+
if (values.with === undefined) { await initProject(arg); print({ event:'created' }); break; }
|
|
272
|
+
{
|
|
273
|
+
const created = await initProjectWith(arg, parseWithNames(values.with));
|
|
274
|
+
print({ event:'created', ...created, review:`Review ${created.project}/urlcode.yaml and pin its revision explicitly (for example PROJECT_SHA256=${created.projectSha256}); re-review after any project change` });
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
151
277
|
case 'validate': {
|
|
152
|
-
const runtime = await createRuntime(values.project, { local:values.local, permissions,
|
|
153
|
-
print({ event:'valid',
|
|
278
|
+
const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions, origin:values.origin });
|
|
279
|
+
print({ event:'valid', routes:runtime.count, version:runtime.version }); await runtime.close(); break;
|
|
154
280
|
}
|
|
155
281
|
case 'add':
|
|
156
282
|
if (!arg) throw new ConfigError('Provide an HTTP(S) destination URL');
|
|
157
283
|
print({ event:'added', path:await addRedirect(values.project,arg,values.alias) }); break;
|
|
158
284
|
case 'test': {
|
|
159
|
-
const result = await runProjectTests(values.project, { log:print, permissions,
|
|
285
|
+
const result = await runProjectTests(values.project, { ...hostOptions, log:print, permissions, origin:values.origin });
|
|
160
286
|
print(result); if (result.failed) process.exitCode = 1; break;
|
|
161
287
|
}
|
|
162
288
|
case 'doctor':
|
|
163
|
-
print({ node:process.version,
|
|
289
|
+
print({ node:process.version, platform:process.platform, architecture:process.arch, runtime:'node-process', functionSandbox:'quickjs-wasm', network:false, filesystem:false, guestNetwork:false, hostEgress:'revision-pinned-origin-grants', tooling:['recipes','examples','bulk-import','build-typescript','mcp','verify-provider'], providers:[], capabilityTargets:getCapabilities().targets, policies:Object.keys(policyRegistry), license:'Apache-2.0' }); break;
|
|
164
290
|
case 'dev': case 'serve': {
|
|
165
291
|
const port = Number(values.port);
|
|
166
292
|
if (!/^\d+$/.test(values.port) || !Number.isInteger(port) || port < 0 || port > 65535) throw new ConfigError('Invalid port');
|
|
167
293
|
if (command === 'serve' && values.local) throw new ConfigError('serve never reads local dotenv files');
|
|
168
|
-
const app = await startServer({ project:values.project, host:values.host, port,
|
|
169
|
-
local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions,
|
|
294
|
+
const app = await startServer({ ...hostOptions, project:values.project, host:values.host, port,
|
|
295
|
+
local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions,
|
|
170
296
|
...serverCapacity(values) });
|
|
171
297
|
print({ event:'listening', address:app.address.address, port:app.address.port, mode:command, origin:app.origin });
|
|
298
|
+
serving = true;
|
|
172
299
|
let stopping = false;
|
|
173
|
-
const stop = async () => { if (stopping) return; stopping = true; await app.close(); };
|
|
300
|
+
const stop = async () => { if (stopping) return; stopping = true; try { await app.close(); } finally { await operatorHost.close?.(); } };
|
|
174
301
|
process.once('SIGINT',stop); process.once('SIGTERM',stop);
|
|
175
302
|
break;
|
|
176
303
|
}
|
|
@@ -182,4 +309,8 @@ try {
|
|
|
182
309
|
const code = typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
|
|
183
310
|
const message = (error instanceof ConfigError || error instanceof HttpError) ? error.message : ((code !== undefined ? errorMessages[code] : undefined) || 'Operation failed; check project files, module dependencies and command options');
|
|
184
311
|
process.stderr.write(JSON.stringify({ event:'error', message }) + '\n'); process.exitCode = 1;
|
|
312
|
+
} finally {
|
|
313
|
+
if (!serving) {
|
|
314
|
+
try { await operatorHost.close?.(); } catch { process.stderr.write('Operator host cleanup failed\n'); process.exitCode = 1; }
|
|
315
|
+
}
|
|
185
316
|
}
|
package/dist/client-address.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { oshp, rfc9111, rfc9110, rfc6585, rfc9309, breach,
|
|
1
|
+
import { oshp, rfc9111, rfc9110, rfc6585, rfc9309, breach, active, functionLike, hasSecrets, yamlHeader, handlerCacheControl, directive, emittedSecurityHeaders } from './shared.js';
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const profile = 'baseline';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const securityHeaders = {
|
|
7
7
|
id: 'oshp/security-headers', title: 'Every active route carries a security headers profile', standard: oshp, severity: 'medium', appliesTo: 'route',
|
|
8
8
|
check({ route, effective }) {
|
|
9
9
|
if (!active(route) || effective.security) return [];
|
|
@@ -11,7 +11,7 @@ export const securityHeaders = {
|
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const hstsOrigin = {
|
|
15
15
|
id: 'oshp/hsts-origin', title: 'HSTS is only reachable on an https origin', standard: { ...oshp, section: 'Strict-Transport-Security' }, severity: 'low', appliesTo: 'project',
|
|
16
16
|
check({ plan, policies, origin }) {
|
|
17
17
|
const emitting = plan.inventory.filter(route => active(route) && emittedSecurityHeaders(policies[route.path], plan.policies?.[route.path]).has('strict-transport-security'));
|
|
@@ -23,7 +23,7 @@ export const hstsOrigin = {
|
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
const secretsCompression = {
|
|
27
27
|
id: 'breach/secrets-compression', title: 'Routes bound to secrets are never compressed', standard: breach, severity: 'high', appliesTo: 'route',
|
|
28
28
|
check({ route, config, effective }) {
|
|
29
29
|
if (!hasSecrets(config) || effective.compression?.allowWithSecrets !== true) return [];
|
|
@@ -31,7 +31,7 @@ export const secretsCompression = {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const secretsNoStore = {
|
|
35
35
|
id: 'rfc9111/secrets-no-store', title: 'Routes bound to secrets declare no-store or private caching', standard: { ...rfc9111, section: '5.2.2.5 no-store, 5.2.2.7 private' }, severity: 'medium', appliesTo: 'route',
|
|
36
36
|
check({ route, config, effective }) {
|
|
37
37
|
if (!hasSecrets(config)) return [];
|
|
@@ -47,7 +47,7 @@ export const secretsNoStore = {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
const cacheControlDeclared = {
|
|
51
51
|
id: 'rfc9111/cache-control-declared', title: 'Every declared response states its Cache-Control', standard: rfc9111, severity: 'low', appliesTo: 'route',
|
|
52
52
|
check({ route, config, effective }) {
|
|
53
53
|
if (!active(route) || effective.cache) return [];
|
|
@@ -60,7 +60,7 @@ export const cacheControlDeclared = {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
const throttleFunctions = {
|
|
64
64
|
id: 'rfc6585/throttle-functions', title: 'Function and middleware routes declare a request budget', standard: rfc6585, severity: 'medium', appliesTo: 'route',
|
|
65
65
|
check({ route, effective }) {
|
|
66
66
|
if (!active(route) || !functionLike(route) || effective.throttle) return [];
|
|
@@ -78,7 +78,7 @@ export const robots = {
|
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const expiredRoutes = {
|
|
82
82
|
id: 'rfc9110/expired-routes', title: 'Expired routes still present in YAML', standard: { ...rfc9110, section: '15.5.11 410 Gone' }, severity: 'info', appliesTo: 'project',
|
|
83
83
|
check({ plan }) {
|
|
84
84
|
const expired = plan.inventory.filter(route => route.state === 'expired').map(route => route.path);
|
|
@@ -87,12 +87,4 @@ export const expiredRoutes = {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
export const
|
|
91
|
-
id: 'ops/management-private', title: 'Management and probe listeners stay private', standard: management, severity: 'info', appliesTo: 'project',
|
|
92
|
-
check({ document }) {
|
|
93
|
-
if (document.dynamicLinks !== true) return [];
|
|
94
|
-
return [{ message: 'dynamicLinks is enabled; the link management API and the /_urlcode probes are meant for a private bind', remediation: 'Run `urlcode links api` on a private interface with an operator auth file, and keep /_urlcode/health and /_urlcode/ready internal' }];
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export const rules = Object.freeze([securityHeaders, hstsOrigin, secretsCompression, secretsNoStore, cacheControlDeclared, throttleFunctions, robots, expiredRoutes, managementPrivate]);
|
|
90
|
+
export const rules = Object.freeze([securityHeaders, hstsOrigin, secretsCompression, secretsNoStore, cacheControlDeclared, throttleFunctions, robots, expiredRoutes]);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { monitoring,
|
|
1
|
+
import { monitoring, active } from './shared.js';
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const profile = 'privacy';
|
|
5
5
|
|
|
6
6
|
// These rules check the host settings the operator declares for the
|
|
7
|
-
// deployment under review (`host.requestLog
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
export const requestLogMinimal = {
|
|
7
|
+
// deployment under review (`host.requestLog`), because the runtime's log
|
|
8
|
+
// guarantees are settings of the serving process, not of the project. An
|
|
9
|
+
// undeclared setting is reported as unknown rather than assumed.
|
|
10
|
+
const requestLogMinimal = {
|
|
12
11
|
id: 'privacy/request-log-minimal', title: 'Request log records stay minimal', standard: monitoring, severity: 'medium', appliesTo: 'project',
|
|
13
12
|
check({ host }) {
|
|
14
13
|
if (host.requestLog === 'minimal') return [];
|
|
@@ -17,17 +16,7 @@ export const requestLogMinimal = {
|
|
|
17
16
|
},
|
|
18
17
|
};
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
id: 'privacy/link-events-off', title: 'Link events stay off unless declared', standard: linkChannel, severity: 'medium', appliesTo: 'project',
|
|
22
|
-
check({ document, host }) {
|
|
23
|
-
if (document.dynamicLinks !== true || host.linkEvents === false) return [];
|
|
24
|
-
if (host.linkEvents === null) return [{ severity: 'info', message: 'dynamicLinks is enabled and the deployment did not declare whether the link event channel is on', remediation: 'Declare host.linkEvents (and host.includeCode) for the deployment under review' }];
|
|
25
|
-
if (host.includeCode === true) return [{ severity: 'high', message: 'The link event channel is on with includeCode, so every event names the short code a person followed', remediation: 'Leave includeCode off; the channel then redacts the code and reports only route, outcome and timing' }];
|
|
26
|
-
return [{ message: 'The link event channel is on, so per-click records leave the process', remediation: 'Enable linkEvents only for a declared click-collection purpose and keep the collector bounded' }];
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const detailedParameters = {
|
|
19
|
+
const detailedParameters = {
|
|
31
20
|
id: 'privacy/detailed-log-parameters', title: 'No detailed logging on parameterised routes', standard: monitoring, severity: 'low', appliesTo: 'route',
|
|
32
21
|
check({ route, config, host }) {
|
|
33
22
|
if (host.requestLog !== 'detailed' || !active(route) || !((config.parameters?.length ?? 0) > 0)) return [];
|
|
@@ -35,4 +24,4 @@ export const detailedParameters = {
|
|
|
35
24
|
},
|
|
36
25
|
};
|
|
37
26
|
|
|
38
|
-
export const rules = Object.freeze([requestLogMinimal,
|
|
27
|
+
export const rules = Object.freeze([requestLogMinimal, detailedParameters]);
|
|
@@ -12,8 +12,6 @@ export const rfc6585 = Object.freeze({ name: 'RFC 6585 Additional HTTP
|
|
|
12
12
|
export const rfc9309 = Object.freeze({ name: 'RFC 9309 Robots Exclusion Protocol', reference: 'https://www.rfc-editor.org/rfc/rfc9309' });
|
|
13
13
|
export const breach = Object.freeze({ name: 'BREACH (compression side channel)', reference: 'https://www.breachattack.com/', section: 'Mitigations' });
|
|
14
14
|
export const monitoring = Object.freeze({ name: 'URLCode logging guarantees', reference: 'docs/MONITORING.md', section: 'Log records' });
|
|
15
|
-
export const linkChannel = Object.freeze({ name: 'URLCode link event channel', reference: 'docs/MONITORING.md', section: 'The link event channel' });
|
|
16
|
-
export const management = Object.freeze({ name: 'URLCode management security', reference: 'docs/MANAGEMENT-SECURITY.md' });
|
|
17
15
|
export const agentLists = Object.freeze({ name: 'URLCode bundled agent lists', reference: 'docs/policies/agents.md', section: 'Bundled lists' });
|
|
18
16
|
|
|
19
17
|
export const active = (route ) => route.state === 'active';
|
|
@@ -6,7 +6,7 @@ export const profile = 'strict';
|
|
|
6
6
|
|
|
7
7
|
// The runtime caps a response at 16 KiB of headers; the security policy
|
|
8
8
|
// bounds its own static headers at half of that so the handler keeps room.
|
|
9
|
-
|
|
9
|
+
const headerBudgetBytes = 8192;
|
|
10
10
|
|
|
11
11
|
export const csp = {
|
|
12
12
|
id: 'oshp/csp', title: 'Every active route emits a Content-Security-Policy', standard: { ...oshp, section: 'Content-Security-Policy' }, severity: 'medium', appliesTo: 'route',
|
|
@@ -16,7 +16,7 @@ export const csp = {
|
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const throttleAll = {
|
|
20
20
|
id: 'rfc6585/throttle-all', title: 'Every active route declares a request budget', standard: rfc6585, severity: 'medium', appliesTo: 'route',
|
|
21
21
|
check({ route, effective }) {
|
|
22
22
|
if (!active(route) || functionLike(route) || effective.throttle) return [];
|
|
@@ -24,7 +24,7 @@ export const throttleAll = {
|
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const listsPinned = {
|
|
28
28
|
id: 'agents/lists-pinned', title: 'Agent deny and allow lists are pinned to a revision', standard: agentLists, severity: 'low', appliesTo: 'route',
|
|
29
29
|
check({ route, effective, policy }) {
|
|
30
30
|
if (!active(route) || !effective.agents) return [];
|
|
@@ -39,7 +39,7 @@ export const listsPinned = {
|
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
const redirectHttps = {
|
|
43
43
|
id: 'rfc9110/redirect-https', title: 'Redirect targets are https', standard: { ...rfc9110, section: '15.4 Redirection 3xx' }, severity: 'medium', appliesTo: 'route',
|
|
44
44
|
check({ route, config }) {
|
|
45
45
|
const url = config.redirect?.url;
|
|
@@ -48,7 +48,7 @@ export const redirectHttps = {
|
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
const headerBudget = {
|
|
52
52
|
id: 'http/header-budget', title: 'Declared response headers stay under the static budget', standard: { ...rfc9110, section: '5.4 Field Limits' }, severity: 'low', appliesTo: 'route',
|
|
53
53
|
check({ route, config, effective, policy }) {
|
|
54
54
|
if (!active(route)) return [];
|
package/dist/compliance.js
CHANGED
|
@@ -37,7 +37,7 @@ import * as privacy from './compliance-rules/privacy.js';
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
/** The host settings under review, null where the operator declared nothing. */
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
|
|
@@ -58,14 +58,14 @@ import * as privacy from './compliance-rules/privacy.js';
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
export const severities = Object.freeze(['high','medium','low','info']);
|
|
66
66
|
export const idPattern = /^[a-z][a-z0-9-]{0,31}\/[a-z][a-z0-9-]{0,63}$/;
|
|
67
67
|
const referencePattern = /^(?:https?:\/\/\S+|RFC ?\d{3,5}|docs\/[A-Za-z0-9./-]+\.md)$/;
|
|
68
|
-
const hostKeys = ['requestLog'
|
|
68
|
+
const hostKeys = ['requestLog'];
|
|
69
69
|
const isSeverity = (value ) => (severities ).includes(value);
|
|
70
70
|
|
|
71
71
|
|
|
@@ -147,11 +147,9 @@ export async function loadComplianceRules(file , project
|
|
|
147
147
|
|
|
148
148
|
function validateHost(host = {}) {
|
|
149
149
|
assert(host && typeof host === 'object' && !Array.isArray(host) && Object.keys(host).every(key => hostKeys.includes(key)), `Compliance host settings accept ${hostKeys.join(', ')}`);
|
|
150
|
-
const { requestLog
|
|
150
|
+
const { requestLog } = host ;
|
|
151
151
|
assert(requestLog === undefined || requestLog === 'minimal' || requestLog === 'detailed', 'Compliance host.requestLog must be minimal or detailed');
|
|
152
|
-
|
|
153
|
-
assert(includeCode === undefined || typeof includeCode === 'boolean', 'Compliance host.includeCode must be a boolean');
|
|
154
|
-
return { requestLog: requestLog ?? null, linkEvents: linkEvents ?? null, includeCode: includeCode ?? null };
|
|
152
|
+
return { requestLog: requestLog ?? null };
|
|
155
153
|
}
|
|
156
154
|
|
|
157
155
|
function finding(rule , raw , route ) {
|
|
@@ -209,7 +207,7 @@ export async function runCompliance(app , { rules = [], profile =
|
|
|
209
207
|
return {
|
|
210
208
|
profile, rules: set.length, ruleIds: set.map(rule => rule.id), ignored: [...ignore], findings, counts, pass: counts.high === 0,
|
|
211
209
|
evidence: {
|
|
212
|
-
routes: plan.inventory.length, active,
|
|
210
|
+
routes: plan.inventory.length, active,
|
|
213
211
|
policies: [...new Set(Object.values(policies).flatMap(Object.keys))].sort(),
|
|
214
212
|
files: loaded.files.map(file => relative(loaded.root, file)),
|
|
215
213
|
origin: origin ?? null, target, host: hostSettings,
|