@jimhoyd/urlcode 0.4.0-alpha.1 → 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 +24 -8
- package/.claude/skills/urlcode-operations/SKILL.md +10 -16
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +75 -62
- package/ROADMAP.md +61 -16
- package/SECURITY.md +23 -8
- package/dist/BUILD-MANIFEST.json +38 -43
- package/dist/agents-guide.js +18 -18
- package/dist/build-static.js +134 -0
- package/dist/capabilities.js +85 -31
- package/dist/capability-query.js +0 -1
- package/dist/cli.js +23 -31
- package/dist/compliance-rules/baseline.js +2 -10
- package/dist/compliance-rules/privacy.js +5 -16
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance.js +6 -8
- package/dist/config.js +15 -8
- package/dist/context.js +9 -10
- package/dist/examples.js +2 -2
- package/dist/explain-cli.js +3 -3
- package/dist/explain.js +5 -6
- package/dist/extensions.js +61 -2
- package/dist/function-sources.js +34 -2
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +3 -6
- package/dist/manifest.js +5 -7
- package/dist/mcp.js +1 -1
- package/dist/observability.js +1 -21
- package/dist/policies/cache.js +8 -3
- package/dist/policy.js +28 -9
- package/dist/project-tests.js +3 -3
- package/dist/readiness.js +32 -9
- package/dist/router.js +5 -7
- package/dist/runtime.js +77 -58
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/tooling.js +1 -1
- package/dist/trusted-functions.js +210 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/capabilities.d.ts +14 -5
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/config.d.ts +2 -1
- package/dist/types/context.d.ts +2 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/extensions.d.ts +55 -0
- package/dist/types/function-sources.d.ts +4 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +3 -6
- package/dist/types/manifest.d.ts +1 -3
- package/dist/types/observability.d.ts +1 -14
- package/dist/types/project-tests.d.ts +1 -2
- package/dist/types/readiness.d.ts +10 -3
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +0 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/server.d.ts +1 -4
- package/dist/types/tooling.d.ts +3 -3
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +14 -7
- package/dist/types/verify-deployment.d.ts +2 -2
- package/dist/types.js +17 -4
- package/dist/typescript-authoring.js +55 -17
- package/dist/verify-deployment.js +3 -3
- package/docs/AI-AUTHORING.md +104 -9
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +3 -2
- package/docs/CAPABILITIES.md +29 -16
- package/docs/CAPACITY.md +128 -31
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/COMPLIANCE.md +6 -9
- package/docs/DEPLOYMENT-CHECKS.md +1 -1
- package/docs/EXTENSIONS.md +183 -11
- package/docs/FRAMEWORK.md +35 -19
- package/docs/FUNCTION-SECURITY.md +129 -32
- package/docs/INSTALL.md +0 -5
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/MIDDLEWARE.md +27 -16
- package/docs/MONITORING.md +2 -19
- package/docs/NEXT-PHASE-PLAN.md +12 -4
- package/docs/NEXT-STEPS.md +259 -40
- package/docs/OBSERVABILITY.md +7 -16
- package/docs/OPEN-DECISIONS.md +212 -0
- package/docs/OPERATIONAL-PROOF.md +26 -30
- package/docs/OPERATIONS.md +23 -32
- package/docs/POLICIES.md +15 -4
- package/docs/PRERENDER.md +2 -2
- package/docs/PROJECT-DIRECTION.md +34 -9
- package/docs/README.md +13 -11
- package/docs/RELEASE-READINESS.md +49 -29
- package/docs/RELEASE-SECURITY.md +89 -3
- 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 +40 -0
- package/docs/SPECIFICATION.md +60 -26
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +8 -0
- package/docs/SPIKE-CORE-LAYERING.md +337 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +2 -2
- package/docs/SPIKE-EXTENSIONS.md +5 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +4 -2
- package/docs/SPIKE-MONOREPO.md +322 -0
- package/docs/STARTERS.md +1 -1
- package/docs/STATIC.md +94 -0
- package/docs/TOOLING.md +13 -9
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +27 -12
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/USABILITY-REVIEW.md +7 -1
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +205 -0
- package/docs/YAML-GUIDE.md +0 -3
- package/docs/YAML-REFERENCE.md +16 -6
- package/docs/policies/cache.md +13 -0
- package/examples/assets/example.yaml +1 -1
- package/examples/aws/example.yaml +1 -1
- package/examples/cloudflare/example.yaml +1 -1
- package/examples/conditions/example.yaml +1 -1
- package/examples/cookbook/example.yaml +1 -1
- package/examples/cookbook/middleware/bucket.mjs +12 -2
- package/examples/cookbook/middleware/locale.mjs +7 -3
- package/examples/egress/example.yaml +1 -1
- package/examples/extensions/example.yaml +1 -1
- package/examples/prerender/README.md +2 -2
- package/examples/prerender/example.yaml +1 -1
- package/examples/provider-conformance/example.yaml +1 -1
- package/examples/vercel/example.yaml +1 -1
- package/llms-full.txt +594 -219
- package/llms.txt +23 -12
- package/package.json +10 -3
- package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +24 -8
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +10 -16
- package/recipes/authenticated-json-api/recipe.yaml +1 -1
- package/recipes/contact-form/recipe.yaml +1 -1
- package/recipes/cors-api/recipe.yaml +1 -1
- package/recipes/health-page/recipe.yaml +1 -1
- package/recipes/json-api/recipe.yaml +1 -1
- package/recipes/middleware/middleware/bucket.mjs +12 -2
- package/recipes/middleware/middleware/locale.mjs +7 -3
- package/recipes/middleware/recipe.yaml +1 -1
- package/recipes/protected-download/recipe.yaml +1 -1
- package/recipes/redirect/recipe.yaml +1 -1
- package/recipes/static-plus-api/recipe.yaml +1 -1
- package/recipes/typescript/README.md +2 -1
- package/recipes/typescript/recipe.yaml +1 -1
- package/recipes/webhook-receiver/README.md +5 -1
- package/recipes/webhook-receiver/recipe.yaml +2 -1
- package/recipes/webhook-receiver/urlcode.yaml +7 -0
- package/schemas/recipe.schema.json +4 -3
- package/schemas/urlcode.schema.json +14 -41
- package/skills/urlcode/SKILL.md +29 -8
- package/starters/default/AGENTS.md +18 -18
- 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 -40
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -37
- 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 -8
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -61
- package/docs/MANAGEMENT-SECURITY.md +0 -82
- package/docs/links/cli.md +0 -110
- package/docs/links/limits.md +0 -175
- package/docs/links/management-api.md +0 -80
- package/docs/links/pools.md +0 -75
- package/docs/links/setup.md +0 -135
- package/docs/yaml/links.md +0 -30
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/example.yaml +0 -21
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
package/dist/cli.js
CHANGED
|
@@ -21,15 +21,13 @@ import { loadOperatorPolicy, prepareFunctionSnapshot, requestedPermissions } fro
|
|
|
21
21
|
import { loadDocument } from './config.js';
|
|
22
22
|
import { describeExtensions } from './tooling.js';
|
|
23
23
|
|
|
24
|
-
import {parseLinkBinding,runLinkCommand,linkPoolOptions} from './link-cli.js';
|
|
25
24
|
import { ConfigError, HttpError } from './errors.js';
|
|
26
|
-
import {supportsConcurrentWal} from './sqlite-version.js';
|
|
27
25
|
import { registry as policyRegistry } from './policies.js';
|
|
28
26
|
import { loadComplianceRules, profileNames as complianceProfiles } from './compliance.js';
|
|
29
27
|
import { parseRouteSnapshot, diffRoutes, renderRouteDiff } from './route-diff.js';
|
|
30
28
|
import { readFile } from 'node:fs/promises';
|
|
31
29
|
|
|
32
|
-
const usage = `URLCode 0.4.0-alpha.
|
|
30
|
+
const usage = `URLCode 0.4.0-alpha.2 — local/self-hosted runtime
|
|
33
31
|
urlcode init <directory> [--with auth,admin] # --with: layered site from installed @jimhoyd/urlcode-<name> packages
|
|
34
32
|
urlcode scaffold [--project directory] [--dry-run]
|
|
35
33
|
urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
|
|
@@ -41,7 +39,8 @@ const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
|
|
|
41
39
|
policies: [--trusted-proxies 10.0.0.0/8,fd00::/8] # peers allowed to set X-Forwarded-For for client policies
|
|
42
40
|
urlcode add <destination-url> [--alias short-code] [--project directory]
|
|
43
41
|
urlcode test [--project directory] [--origin https://links.example]
|
|
44
|
-
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
|
|
45
44
|
urlcode routes [--project directory] [--origin https://links.example]
|
|
46
45
|
diff: [--compare previous-routes.json] [--format json|markdown] # added/removed/changed routes against an earlier report; always exits 0
|
|
47
46
|
urlcode audit [--project directory] [--expect-routes 2]
|
|
@@ -54,16 +53,10 @@ const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
|
|
|
54
53
|
[--compliance baseline|strict|privacy|none] [--compliance-rules ...] [--compliance-ignore id,id] [--compliance-warn]
|
|
55
54
|
# compares the running deployment's responses with what this project declares; never follows redirects, no --insecure
|
|
56
55
|
urlcode permissions [--project directory] # inspect requested bindings and egress origins; grants nothing
|
|
57
|
-
urlcode explain [/route] [--project directory] [--target self-hosted|cloudflare|aws|vercel] [--host-file ...] [--json]
|
|
56
|
+
urlcode explain [/route] [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--host-file ...] [--json]
|
|
58
57
|
# effective methods, handler, middleware, inputs, policies, cache outcome, bindings and target support from the compiled configuration
|
|
59
58
|
urlcode manifest [--project directory] [--json] # generated semantic manifest; build writes the same file as manifest.json
|
|
60
59
|
urlcode extensions [--project directory] [--host-file /absolute/operator/host.mjs] [--json] # registered contracts and schemas; executes trusted host code, activates nothing
|
|
61
|
-
urlcode links init|create|get|list|update|delete|export|import|api --store /absolute/links.sqlite [--collection links]
|
|
62
|
-
create/update: --destination https://example.com [--code abc] [--status 302] [--enabled true] [--expires UTC]
|
|
63
|
-
update/delete: --code abc --if-version N (update replaces all mutable fields)
|
|
64
|
-
export: consistent NDJSON snapshot to stdout [--collection links] [--page-size 100]
|
|
65
|
-
import: --input /absolute/export.ndjson restores into empty collections (versions are reassigned)
|
|
66
|
-
api: --auth-file /operator/management.json (or legacy --token-file /operator/token) --port 3001 (separate authenticated server)
|
|
67
60
|
urlcode import [netlify|cloudflare|vercel|netlify-toml] <file> [--format csv|json|yaml] [--out new-file] [--dry-run] [--report json]
|
|
68
61
|
urlcode export --target netlify|cloudflare|vercel|netlify-toml|csv|json|yaml [--project directory] [--out new-file] [--report json]
|
|
69
62
|
conversion: [--accept-provider-differences] # explicit non-lossless migration candidate; exact behavior requires runtime
|
|
@@ -74,15 +67,13 @@ const usage = `URLCode 0.4.0-alpha.1 — local/self-hosted runtime
|
|
|
74
67
|
urlcode verify-provider --target self-hosted|aws|vercel|cloudflare --origin https://owned-fixture.example
|
|
75
68
|
[--timeout-ms 3000] [--release label] [--git-commit sha] # explicitly invokes synthetic deployment probes
|
|
76
69
|
urlcode mcp [--project directory] [--allow-authoring] [--host-file ...] # bounded stdio tooling; --allow-authoring adds project-confined authoring tools, host file adds get_extensions
|
|
77
|
-
urlcode capabilities [--target self-hosted|cloudflare|aws|vercel] [--json]
|
|
70
|
+
urlcode capabilities [--target self-hosted|cloudflare|aws|vercel|static] [--json]
|
|
78
71
|
urlcode capabilities <name> [--json] # one catalog entry: schema fragment, constraints, grants, targets, bundled uses
|
|
79
72
|
urlcode schema <path> [--json|--yaml] # schema fragment for route, redirect, policies.cache, site.sitemap, ...
|
|
80
|
-
urlcode context [--project directory] [--target self-hosted|cloudflare|aws|vercel] [--budget 500] [--json] [--stats]
|
|
73
|
+
urlcode context [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--budget 500] [--json] [--stats]
|
|
81
74
|
# compact facts for an authoring agent from the compiled project; --stats compares estimated tokens with the docs
|
|
82
75
|
urlcode doctor
|
|
83
76
|
serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp: --host-file /absolute/operator/host.mjs (trusted code outside project)
|
|
84
|
-
serve/dev/validate/test/routes/audit/benchmark: --link-store links=/absolute/links.sqlite
|
|
85
|
-
Store pool controls: --link-readers 2 (1–8), --link-read-limit 32, --link-write-limit 32 (1–32 each)
|
|
86
77
|
Dev loads .env.local and watches; serve does neither. Functions run in WASM isolation; external bindings require --policy outside the project.
|
|
87
78
|
`;
|
|
88
79
|
const print = (value ) => process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value) + '\n');
|
|
@@ -91,10 +82,8 @@ const options = {
|
|
|
91
82
|
project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
|
|
92
83
|
port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
|
|
93
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'},
|
|
94
|
-
'link-readers':{type:'string'}, 'link-read-limit':{type:'string'}, 'link-write-limit':{type:'string'},
|
|
95
85
|
workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
|
|
96
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'},
|
|
97
|
-
'link-store':{type:'string'}, store:{type:'string'}, collection:{type:'string'}, code:{type:'string'}, destination:{type:'string'}, status:{type:'string'}, enabled:{type:'string'}, expires:{type:'string'}, 'if-version':{type:'string'}, limit:{type:'string'}, after:{type:'string'}, 'token-file':{type:'string'}, 'auth-file':{type:'string'}, input:{type:'string'}, 'page-size':{type:'string'},
|
|
98
87
|
release:{type:'string'}, 'git-commit':{type:'string'}, 'timeout-ms':{type:'string'}, 'fail-on':{type:'string'}, 'expect-metrics':{type:'boolean'},
|
|
99
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' },
|
|
100
89
|
} ;
|
|
@@ -131,7 +120,7 @@ async function complianceOptions(values )
|
|
|
131
120
|
if(!complianceProfiles.includes(profile))throw new ConfigError(`Use --compliance ${complianceProfiles.join('|')}`);
|
|
132
121
|
const ignore=(values['compliance-ignore'] ?? '').split(',').map(id=>id.trim()).filter(Boolean);
|
|
133
122
|
const operator=await loadComplianceRules(values['compliance-rules'],values.project);
|
|
134
|
-
const host={requestLog:values['request-log'] ?? 'minimal'
|
|
123
|
+
const host={requestLog:values['request-log'] ?? 'minimal'};
|
|
135
124
|
if(!['minimal','detailed'].includes(host.requestLog))throw new ConfigError('Use --request-log minimal or detailed');
|
|
136
125
|
return {profile,ignore,origin:values.origin,host,rules:operator?.rules ?? [],disable:operator?.disable ?? [],override:operator?.override ?? {}};
|
|
137
126
|
}
|
|
@@ -151,7 +140,7 @@ let serving = false;
|
|
|
151
140
|
try {
|
|
152
141
|
const { values, positionals } = parseArgs({ allowPositionals:true, options });
|
|
153
142
|
const [command, arg, ...extra] = positionals;
|
|
154
|
-
values.port ??=
|
|
143
|
+
values.port ??= '3000';
|
|
155
144
|
if (values.help || !command) print(usage);
|
|
156
145
|
else {
|
|
157
146
|
if (values['host-file'] !== undefined) {
|
|
@@ -162,7 +151,7 @@ try {
|
|
|
162
151
|
if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
|
|
163
152
|
if (values['allow-authoring'] && command !== 'mcp') throw new ConfigError('--allow-authoring is only supported by mcp');
|
|
164
153
|
const hostOptions = { extensions: operatorHost.extensions, plugins: operatorHost.plugins };
|
|
165
|
-
if ((!['import','recipes','recipe','examples','example','bulk-import'].includes(command) && extra.length) || (!['init','add','
|
|
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');
|
|
166
155
|
|
|
167
156
|
if(command==='import'||command==='export'){
|
|
168
157
|
const { runInterchange } = await import('./interchange-cli.js');
|
|
@@ -190,9 +179,8 @@ try {
|
|
|
190
179
|
print(text);
|
|
191
180
|
// Estimates only (characters / 4); a tokenizer is not a dependency. Stats go to stderr so stdout stays parseable.
|
|
192
181
|
if (values.stats) process.stderr.write(JSON.stringify({ event:'stats', estimate:'characters/4', documentationTokens:await documentationTokens(), contextTokens:estimateTokens(text) }) + '\n');
|
|
193
|
-
}else
|
|
182
|
+
}else{
|
|
194
183
|
const permissions = await loadOperatorPolicy(values.policy,values.project);
|
|
195
|
-
const linkStore=parseLinkBinding(values['link-store'],linkPoolOptions(values));
|
|
196
184
|
switch (command) {
|
|
197
185
|
case 'routes': case 'audit': case 'benchmark': {
|
|
198
186
|
const number = (key ,fallback ) => {
|
|
@@ -208,12 +196,12 @@ try {
|
|
|
208
196
|
if(!['json','markdown'].includes(format))throw new ConfigError('Use --format json or markdown');
|
|
209
197
|
if(values.format!==undefined && values.compare===undefined)throw new ConfigError('--format applies to routes --compare');
|
|
210
198
|
const started=performance.now();
|
|
211
|
-
const app=await startServer({...hostOptions,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:()=>{}});
|
|
212
200
|
const startupMs=performance.now()-started;
|
|
213
201
|
try {
|
|
214
202
|
if(command==='routes') {
|
|
215
203
|
const plan=app.testPlan();
|
|
216
|
-
if(values.compare===undefined) print({routes:plan.inventory.length,
|
|
204
|
+
if(values.compare===undefined) print({routes:plan.inventory.length,inventory:plan.inventory,policies:plan.policies});
|
|
217
205
|
else {
|
|
218
206
|
// The diff reports; it never judges, so the exit code stays 0.
|
|
219
207
|
const before=parseRouteSnapshot(JSON.parse(await readFile(values.compare,'utf8'))); // file boundary: an earlier `routes` report
|
|
@@ -243,7 +231,7 @@ try {
|
|
|
243
231
|
const isFailOn = (value ) => (failLevels ).includes(value);
|
|
244
232
|
if (!isFailOn(failOn)) throw new ConfigError(`Use --fail-on ${failLevels.join('|')}`);
|
|
245
233
|
const report = await verifyDeployment(values.project, { target: values.target, origin: values.origin, expectRoutes: integer('expect-routes'), timeoutMs: integer('timeout-ms'),
|
|
246
|
-
expectMetrics: values['expect-metrics'], failOn, compliance: await complianceOptions(values), complianceWarn: values['compliance-warn'], permissions,
|
|
234
|
+
expectMetrics: values['expect-metrics'], failOn, compliance: await complianceOptions(values), complianceWarn: values['compliance-warn'], permissions, log: print });
|
|
247
235
|
print(report); if (!report.pass) process.exitCode = 1; break;
|
|
248
236
|
}
|
|
249
237
|
case 'verify-provider': {
|
|
@@ -260,7 +248,11 @@ try {
|
|
|
260
248
|
print(report);if(!report.pass)process.exitCode=1;break;
|
|
261
249
|
}
|
|
262
250
|
case 'build': {
|
|
263
|
-
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');
|
|
264
256
|
const { buildCloudflare } = await import('./build-cloudflare.js');
|
|
265
257
|
print({ event:'built', ...await buildCloudflare(values.project,{ out:values.out, origin:values.origin }) }); break;
|
|
266
258
|
}
|
|
@@ -283,24 +275,24 @@ try {
|
|
|
283
275
|
}
|
|
284
276
|
break;
|
|
285
277
|
case 'validate': {
|
|
286
|
-
const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions,
|
|
287
|
-
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;
|
|
288
280
|
}
|
|
289
281
|
case 'add':
|
|
290
282
|
if (!arg) throw new ConfigError('Provide an HTTP(S) destination URL');
|
|
291
283
|
print({ event:'added', path:await addRedirect(values.project,arg,values.alias) }); break;
|
|
292
284
|
case 'test': {
|
|
293
|
-
const result = await runProjectTests(values.project, { ...hostOptions, log:print, permissions,
|
|
285
|
+
const result = await runProjectTests(values.project, { ...hostOptions, log:print, permissions, origin:values.origin });
|
|
294
286
|
print(result); if (result.failed) process.exitCode = 1; break;
|
|
295
287
|
}
|
|
296
288
|
case 'doctor':
|
|
297
|
-
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;
|
|
298
290
|
case 'dev': case 'serve': {
|
|
299
291
|
const port = Number(values.port);
|
|
300
292
|
if (!/^\d+$/.test(values.port) || !Number.isInteger(port) || port < 0 || port > 65535) throw new ConfigError('Invalid port');
|
|
301
293
|
if (command === 'serve' && values.local) throw new ConfigError('serve never reads local dotenv files');
|
|
302
294
|
const app = await startServer({ ...hostOptions, project:values.project, host:values.host, port,
|
|
303
|
-
local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions,
|
|
295
|
+
local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions,
|
|
304
296
|
...serverCapacity(values) });
|
|
305
297
|
print({ event:'listening', address:app.address.address, port:app.address.port, mode:command, origin:app.origin });
|
|
306
298
|
serving = true;
|
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -87,12 +87,4 @@ const expiredRoutes = {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
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,13 +1,12 @@
|
|
|
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
|
-
// as unknown rather than assumed.
|
|
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.
|
|
11
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 }) {
|
|
@@ -17,16 +16,6 @@ const requestLogMinimal = {
|
|
|
17
16
|
},
|
|
18
17
|
};
|
|
19
18
|
|
|
20
|
-
const linkEventsOff = {
|
|
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
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 }) {
|
|
@@ -35,4 +24,4 @@ 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';
|
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,
|
package/dist/config.js
CHANGED
|
@@ -70,16 +70,20 @@ function modulePath(pattern , kind , file
|
|
|
70
70
|
/**
|
|
71
71
|
* Expands the YAML short forms into the canonical long form. `function: functions/x.mjs`
|
|
72
72
|
* becomes `{source, args}` with an argument per `{param}` in the path, declaring any
|
|
73
|
-
* parameter the route does not declare itself; a string middleware entry becomes `{source}
|
|
73
|
+
* parameter the route does not declare itself; a string middleware entry becomes `{source}`;
|
|
74
|
+
* a route-level `cache` becomes `policies.cache` (refused alongside a direct `policies.cache`).
|
|
74
75
|
* Everything downstream (routes, audit, the compiled table) sees only the long form.
|
|
75
76
|
*/
|
|
76
77
|
export function normalizeRoute(pattern , route ) {
|
|
77
78
|
const authored = route ;
|
|
78
|
-
|
|
79
|
+
const needsFunction = typeof authored.function === 'string';
|
|
80
|
+
const needsMiddleware = authored.middleware?.some(entry => typeof entry === 'string') ?? false;
|
|
81
|
+
const needsCache = authored.cache !== undefined;
|
|
82
|
+
if (!needsFunction && !needsMiddleware && !needsCache) return route ;
|
|
79
83
|
const result = { ...(route ) };
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
82
|
-
const source = modulePath(pattern, 'function', authored.function);
|
|
84
|
+
if (needsMiddleware) result.middleware = authored.middleware .map((entry) => typeof entry === 'string' ? { source: modulePath(pattern, 'middleware', entry) } : entry);
|
|
85
|
+
if (needsFunction) {
|
|
86
|
+
const source = modulePath(pattern, 'function', authored.function );
|
|
83
87
|
const names = pattern.split('/').flatMap(part => { const match = /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(part); return match ? [match[1] ] : []; });
|
|
84
88
|
const declared = authored.parameters ?? [];
|
|
85
89
|
const parameters = [...declared, ...names.filter(name => !declared.some(p => p.in === 'path' && p.name === name)).map(name => ({ name, in: 'path' , required: true, schema: { ...SHORT_FORM_PATH_SCHEMA } }))];
|
|
@@ -87,6 +91,11 @@ export function normalizeRoute(pattern , route
|
|
|
87
91
|
if (parameters.length) result.parameters = parameters;
|
|
88
92
|
result.function = expanded;
|
|
89
93
|
}
|
|
94
|
+
if (needsCache) {
|
|
95
|
+
assert(result.policies?.cache === undefined, `Route ${pattern} declares both cache and policies.cache; use one form`);
|
|
96
|
+
result.policies = { ...result.policies, cache: authored.cache };
|
|
97
|
+
delete result.cache;
|
|
98
|
+
}
|
|
90
99
|
return result;
|
|
91
100
|
}
|
|
92
101
|
export async function safeFile(root , file ) {
|
|
@@ -178,7 +187,6 @@ export async function loadDocumentInWorker(project )
|
|
|
178
187
|
files.push(path);
|
|
179
188
|
const part = validateDocument(await readConfig(path, budget));
|
|
180
189
|
assert(!part.includes?.length, 'Nested includes are unsupported');
|
|
181
|
-
assert(part.dynamicLinks===undefined, 'dynamicLinks may only be set in the entry urlcode.yaml');
|
|
182
190
|
assert(part.site===undefined, 'site may only be set in the entry urlcode.yaml');
|
|
183
191
|
for(const [name,extension]of Object.entries(part.extensions??{})){assert(!Object.hasOwn(extensions,name),'Duplicate extension declaration across files');extensions[name]=extension;assert(Object.keys(extensions).length<=16,'Maximum 16 extensions per project');}
|
|
184
192
|
for (const [pattern, route] of Object.entries(part.routes)) {
|
|
@@ -190,8 +198,7 @@ export async function loadDocumentInWorker(project )
|
|
|
190
198
|
if(Object.keys(extensions).length)document.extensions=extensions;
|
|
191
199
|
normalizeRouteAuth(document, routes);
|
|
192
200
|
assert(Object.keys(routes).length <= 100000, 'Maximum 100000 routes per project');
|
|
193
|
-
|
|
194
|
-
return { root, document, routes, files, version: createHash('sha256').update(JSON.stringify(document.extensions?{routes,extensions:document.extensions,policies:document.policies,profiles:document.profiles,site:document.site,dynamicLinks:document.dynamicLinks}: document.site ? {...(document.dynamicLinks===true?{routes,dynamicLinks:true}:{routes}), site:document.site} : document.dynamicLinks===true?{routes,dynamicLinks:true}:routes)).digest('hex').slice(0, 16) };
|
|
201
|
+
return { root, document, routes, files, version: createHash('sha256').update(JSON.stringify(document.extensions?{routes,extensions:document.extensions,policies:document.policies,profiles:document.profiles,site:document.site}: document.site ? {routes, site:document.site} : routes)).digest('hex').slice(0, 16) };
|
|
195
202
|
}
|
|
196
203
|
export async function loadBindings(root , local = false, environment = process.env) {
|
|
197
204
|
const vars = {};
|
package/dist/context.js
CHANGED
|
@@ -27,11 +27,11 @@ import {loadOperatorHost} from './operator-host.js';
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
|
|
@@ -40,14 +40,14 @@ import {loadOperatorHost} from './operator-host.js';
|
|
|
40
40
|
/** Characters divided by four, rounded up: an estimate, not a tokenizer. */
|
|
41
41
|
export function estimateTokens(text ) {return Math.ceil(text.length/4);}
|
|
42
42
|
export function renderContext(context ) {return stringify(context,{lineWidth:0,aliasDuplicateObjects:false});}
|
|
43
|
-
const handlerNames=['redirect','respond','page','static','download','function','
|
|
43
|
+
const handlerNames=['redirect','respond','page','static','download','function','proxy','conditional','extension'] ;
|
|
44
44
|
const policyNames=Object.keys(registry).sort() ;
|
|
45
45
|
// Fixed for every project: what generation must not attempt, whatever the documentation says.
|
|
46
46
|
const constraints ={
|
|
47
|
-
guestNetwork:{value:
|
|
48
|
-
nodeApis:{value:
|
|
47
|
+
guestNetwork:{value:true,note:'Trusted (default) functions and middleware run in-process with full Node network access; route `sandbox: true` runs that route in a WASM sandbox without fetch or sockets, where outbound calls must be proxy or signals routes under operator grants'},
|
|
48
|
+
nodeApis:{value:true,note:'Trusted (default) functions and middleware have full Node built-ins, process, filesystem and npm packages available, same as any other project code; route `sandbox: true` restricts that route to relative ES-module imports only, no Node built-ins/filesystem/npm packages'},
|
|
49
49
|
regexRoutes:{value:false,note:'Paths are whole segments: exact literals or {param} placeholders declared as required string parameters'},
|
|
50
|
-
oneHandlerPerRoute:{value:true,note:'Exactly one of redirect, respond, page, static, download, function,
|
|
50
|
+
oneHandlerPerRoute:{value:true,note:'Exactly one of redirect, respond, page, static, download, function, proxy, conditional or extension; middleware wraps it'},
|
|
51
51
|
pathShape:{value:'exact or {param}',note:'No greedy captures or general-purpose wildcards; a segment is a literal or a named placeholder'},
|
|
52
52
|
wildcardMounts:{value:false,note:'Only static and extension routes mount a subtree; nothing else matches below its path'},
|
|
53
53
|
yamlInterpolation:{value:false,note:'No ${...} templating; bind typed inputs through parameters, args and context'},
|
|
@@ -79,7 +79,6 @@ export async function buildContext(project ,options ={})
|
|
|
79
79
|
for(const name of handlerNames)handlers[name]=0;
|
|
80
80
|
for(const name of policyNames)policyCounts[name]=0;
|
|
81
81
|
if(Object.keys(document.extensions??{}).length)used.add('extension');
|
|
82
|
-
if(document.dynamicLinks)used.add('dynamicLinks');
|
|
83
82
|
for(const route of routes) {
|
|
84
83
|
handlers[handlerOf(route)]=(handlers[handlerOf(route)]??0)+1;
|
|
85
84
|
for(const capability of routeCapabilities(route,document))used.add(capability);
|
|
@@ -113,11 +112,11 @@ export async function buildContext(project ,options ={})
|
|
|
113
112
|
project:{
|
|
114
113
|
entry:'urlcode.yaml',routes:compiled.count,handlers,extensions:sorted(Object.keys(document.extensions??{})),
|
|
115
114
|
policies:{project:policyNames.filter(name=>topLevel[name]),routes:policyCounts},
|
|
116
|
-
bindings:{env:sorted(env),secrets:sorted(secrets)},
|
|
115
|
+
bindings:{env:sorted(env),secrets:sorted(secrets)},site:sorted(Object.keys(document.site??{})),
|
|
117
116
|
files:{includes:loaded.files.slice(1).map(file=>relative(loaded.root,file).split('\\').join('/')),functions:sorted(functions),middleware:sorted(middleware)},
|
|
118
117
|
...(options.hostFile===undefined?{}:{host:{extensions:sorted((host.extensions??[]).map(item=>item.name)),plugins:(host.plugins??[]).length}}),
|
|
119
118
|
},
|
|
120
|
-
routes:routes.map(route=>({path:route.pattern,methods:route.methods,handler:handlerOf(route)})).sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0),
|
|
119
|
+
routes:routes.map(route=>({path:route.pattern,methods:route.methods,handler:handlerOf(route),sandbox:route.sandbox===true,...(route.sandboxReason?{sandboxReason:route.sandboxReason}:{})})).sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0),
|
|
121
120
|
constraints:{...constraints},
|
|
122
121
|
targets,
|
|
123
122
|
commands:{
|
|
@@ -138,7 +137,7 @@ const drops =[
|
|
|
138
137
|
['constraintNotes',context=>{for(const [key,item] of Object.entries(context.constraints))context.constraints[key]=typeof item==='object'?item.value:item;}],
|
|
139
138
|
['files',context=>{delete context.project.files;}],
|
|
140
139
|
['commands',context=>{delete context.commands;}],
|
|
141
|
-
['summary',context=>{context.project={entry:context.project.entry,routes:context.project.routes,handlers:context.project.handlers,extensions:[],policies:{project:[],routes:{}},bindings:{env:[],secrets:[]},
|
|
140
|
+
['summary',context=>{context.project={entry:context.project.entry,routes:context.project.routes,handlers:context.project.handlers,extensions:[],policies:{project:[],routes:{}},bindings:{env:[],secrets:[]},site:[]};}],
|
|
142
141
|
];
|
|
143
142
|
function fitBudget(context ,budget ) {
|
|
144
143
|
const omitted =[];
|
package/dist/examples.js
CHANGED
|
@@ -21,7 +21,7 @@ import {assert} from './errors.js';
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
// Fixed package data, like the recipe catalog: names come from here, metadata from each example.yaml.
|
|
24
|
-
export const exampleNames=['assets','aws','cloudflare','compliance','conditions','cookbook','egress','extensions','
|
|
24
|
+
export const exampleNames=['assets','aws','cloudflare','compliance','conditions','cookbook','egress','extensions','monitoring','prerender','provider-conformance','tunnel','vercel'] ;
|
|
25
25
|
export const routeIndexFile='route-index.json';
|
|
26
26
|
const examplesRoot=fileURLToPath(new URL('../examples/',import.meta.url));
|
|
27
27
|
const root=(name )=>examplesRoot+name+'/';
|
|
@@ -31,7 +31,7 @@ export async function listExamples() {
|
|
|
31
31
|
for(const name of exampleNames)result.push(await metadata(name));
|
|
32
32
|
return result;
|
|
33
33
|
}
|
|
34
|
-
const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond'
|
|
34
|
+
const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond'] ;
|
|
35
35
|
function handlerOf(route ) {return handlerNames.find(name=>route[name]!==undefined)??'unknown';}
|
|
36
36
|
/**
|
|
37
37
|
* Derives the per-route tag index of one project from its loaded routes: handler,
|
package/dist/explain-cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {ConfigError} from './errors.js';
|
|
2
|
-
import {normalizeCapabilityTarget} from './capabilities.js';
|
|
2
|
+
import {capabilityTargets, normalizeCapabilityTarget} from './capabilities.js';
|
|
3
3
|
|
|
4
4
|
import {explainRoute,explainProject} from './tooling.js';
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ import {buildManifest,renderManifest} from './manifest.js';
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const table=(rows ) =>{const widths=rows[0] .map((_,i)=>Math.max(...rows.map(row=>row[i] .length)));return rows.map(row=>row.map((cell,i)=>cell.padEnd(widths[i] )).join(' ').trimEnd()).join('\n')+'\n';};
|
|
11
|
-
const targetsOf=(target ) =>target===undefined?[
|
|
11
|
+
const targetsOf=(target ) =>target===undefined?[...capabilityTargets]:[normalizeCapabilityTarget(target)];
|
|
12
12
|
function summary(explanation ,targets ) {
|
|
13
13
|
const handler=explanation.handler,detail=handler.kind==='function'?`${handler.source }#${handler.export }`:handler.kind==='redirect'?`${handler.status } ${handler.url }`:handler.kind==='extension'?handler.name :handler.kind==='page'||handler.kind==='download'?handler.file :handler.kind==='static'?handler.directory :handler.kind==='proxy'?handler.url :handler.kind==='respond'?String(handler.status):'';
|
|
14
14
|
const support=targets.map(target=>explanation.targets[target].compatible?target:`${target}:refused`).join(',');
|
|
@@ -43,7 +43,7 @@ export async function runExplainCommand(command ,route
|
|
|
43
43
|
if(options.json){print(renderManifest(manifest));return 0;}
|
|
44
44
|
const lines=[`revision: ${manifest.revision}`,`urlcode: ${manifest.urlcode}`,`files: ${manifest.files.join(', ')}`,`routes: ${manifest.routeCount}`,`capabilities: ${manifest.capabilities.join(', ')}`,
|
|
45
45
|
`extensions: ${Object.keys(manifest.extensions).join(', ')||'none'}`,`recipes: ${manifest.recipes.map(recipe=>recipe.id).join(', ')||'none'}`,
|
|
46
|
-
`external: env ${manifest.external.env.join(', ')||'-'}; secrets ${manifest.external.secrets.join(', ')||'-'}; proxy ${manifest.external.egress.proxy.join(', ')||'-'}; signals ${manifest.external.egress.signals.join(', ')||'-'}
|
|
46
|
+
`external: env ${manifest.external.env.join(', ')||'-'}; secrets ${manifest.external.secrets.join(', ')||'-'}; proxy ${manifest.external.egress.proxy.join(', ')||'-'}; signals ${manifest.external.egress.signals.join(', ')||'-'}`,
|
|
47
47
|
`functions: ${manifest.functions.map(item=>`${item.source}#${item.export}`).join(', ')||'none'}`,`middleware: ${manifest.middleware.map(item=>`${item.source}#${item.export}`).join(', ')||'none'}`,
|
|
48
48
|
`targets: ${Object.entries(manifest.targets).map(([target,support])=>`${target} ${support.compatible?'supported':`${support.issues} issue${support.issues===1?'':'s'}`}`).join('; ')}`,'Use --json for the full manifest.'];
|
|
49
49
|
print(lines.join('\n')+'\n');return 0;
|
package/dist/explain.js
CHANGED
|
@@ -15,7 +15,7 @@ import {effectivePolicies} from './policies.js';
|
|
|
15
15
|
// binding values are replaced by their names, module paths are made
|
|
16
16
|
// project-relative and secrets never appear.
|
|
17
17
|
|
|
18
|
-
const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond'
|
|
18
|
+
const handlerNames=['extension','proxy','conditional','redirect','function','page','static','download','respond'] ;
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
|
|
@@ -52,12 +52,11 @@ function handlerOf(route ,root ) {
|
|
|
52
52
|
return {kind,cases:(route.conditionalRoutes?.cases??[]).map(item=>({match:item.match,...branch(item.route)})),...(route.conditionalRoutes?.fallback?{fallback:branch(route.conditionalRoutes.fallback)}:{})};
|
|
53
53
|
}
|
|
54
54
|
case 'redirect':return {kind,url:route.redirect .url,status:route.redirect .status??302,...(route.redirect .query?{query:route.redirect .query}:{})};
|
|
55
|
-
case 'function':return {kind,source:relativeSource(root,route.function .source),export:route.function .export,...(route.function .args?{args:route.function .args}:{})};
|
|
55
|
+
case 'function':return {kind,source:relativeSource(root,route.function .source),export:route.function .export,...(route.function .args?{args:route.function .args}:{}),sandbox:route.sandbox===true,...(route.sandboxReason?{sandboxReason:route.sandboxReason}:{})};
|
|
56
56
|
case 'page':return {kind,file:route.page .file,...(route.page .contentType?{contentType:route.page .contentType}:{})};
|
|
57
57
|
case 'static':return {kind,directory:route.static .directory,...(route.static .index?{index:route.static .index}:{})};
|
|
58
58
|
case 'download':return {kind,file:route.download .file,...(route.download .filename?{filename:route.download .filename}:{}),...(route.download .contentType?{contentType:route.download .contentType}:{})};
|
|
59
59
|
case 'respond':return {kind,status:route.reply?.status??route.respond .status??200};
|
|
60
|
-
case 'link':return {kind,collection:route.link .collection};
|
|
61
60
|
default:return {kind:'none'};
|
|
62
61
|
}
|
|
63
62
|
}
|
|
@@ -80,11 +79,11 @@ function providerOf(name ,requirement ,opt
|
|
|
80
79
|
if(requirement)try{requirementValid=registration.policySchema?Boolean(new Ajv.default({strict:false,allErrors:false}).compile(registration.policySchema)(requirement)):false;}catch{requirementValid=false;}
|
|
81
80
|
return {registered:true,version:registration.version,targets:[...registration.targets],revisionMatch:options.projectSha256===undefined?false:registration.projectSha256===options.projectSha256,requirementValid};
|
|
82
81
|
}
|
|
83
|
-
function targetsOf(loaded ,route ) {
|
|
82
|
+
function targetsOf(loaded ,route ,options ) {
|
|
84
83
|
const table={exact:new Map([[route.pattern,route]]),byLength:new Map(),mounts:[],modules:[],count:1};
|
|
85
84
|
const result={} ;
|
|
86
85
|
for(const target of capabilityTargets){
|
|
87
|
-
const report=analyzeCompiledCapabilities(loaded.document,table,target);
|
|
86
|
+
const report=analyzeCompiledCapabilities(loaded.document,table,target,options.extensions);
|
|
88
87
|
const issues=report.issues.filter(issue=>issue.path===route.pattern).map(({capability,support,reason})=>({capability,support,reason}));
|
|
89
88
|
result[target]={compatible:issues.length===0,issues};
|
|
90
89
|
}
|
|
@@ -116,7 +115,7 @@ export function explainCompiledRoute(loaded ,route ,c
|
|
|
116
115
|
cache:cacheOf(route,chain,extensionNames),
|
|
117
116
|
bindings:{env,secrets},egress,
|
|
118
117
|
responseHeaders:route.responseHeaders.map(([name,value])=>[name,value]),
|
|
119
|
-
capabilities:routeCapabilities(route,loaded.document),targets:targetsOf(loaded,route),
|
|
118
|
+
capabilities:routeCapabilities(route,loaded.document),targets:targetsOf(loaded,route,options),
|
|
120
119
|
note:'Derived from the compiled configuration; request conditions, parameter values and handler execution are not evaluated.',
|
|
121
120
|
};
|
|
122
121
|
}
|