@jimhoyd/urlcode 0.5.5 → 0.5.8
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 +16 -1
- package/.claude/skills/urlcode-operations/SKILL.md +5 -1
- package/README.md +24 -14
- package/dist/BUILD-MANIFEST.json +55 -54
- package/dist/agents-guide.js +5 -2
- package/dist/assets.js +2 -2
- package/dist/authoring.js +3 -58
- package/dist/aws.js +21 -4
- package/dist/body-schema.js +2 -3
- package/dist/capabilities.js +1 -1
- package/dist/capability-query.js +3 -3
- package/dist/catalog.js +1 -1
- package/dist/cli.js +42 -15
- package/dist/cloudflare.js +26 -1
- package/dist/config.js +1 -1
- package/dist/context.js +6 -6
- package/dist/egress.js +1 -1
- package/dist/examples.js +2 -2
- package/dist/explain.js +4 -6
- package/dist/extension-artifacts.js +8 -8
- package/dist/extension-transport.js +6 -4
- package/dist/function-worker.js +5 -2
- package/dist/functions.js +1 -1
- package/dist/guest-api.js +22 -15
- package/dist/http-policy.js +1 -3
- package/dist/http-response.js +61 -10
- package/dist/init-with.js +8 -8
- package/dist/interchange.js +3 -2
- package/dist/logging.js +4 -4
- package/dist/match.js +1 -1
- package/dist/mcp-authoring.js +1 -2
- package/dist/mcp.js +2 -2
- package/dist/object-guards.js +17 -0
- package/dist/pattern-guard.js +33 -3
- package/dist/policies/agents.js +1 -1
- package/dist/policies/cache.js +16 -4
- package/dist/policies/compression.js +1 -1
- package/dist/policies/security.js +1 -1
- package/dist/policies/throttle.js +1 -1
- package/dist/policies.js +1 -1
- package/dist/policy.js +3 -3
- package/dist/prerender.js +1 -2
- package/dist/project-dependencies.js +8 -8
- package/dist/proxy.js +1 -1
- package/dist/readiness.js +16 -17
- package/dist/review.js +2 -2
- package/dist/route-diff.js +4 -4
- package/dist/router.js +1 -1
- package/dist/runtime.js +4 -1
- package/dist/scaffold.js +1 -1
- package/dist/schema-query.js +1 -1
- package/dist/server.js +49 -7
- package/dist/signals.js +2 -2
- package/dist/site.js +2 -2
- package/dist/trusted-functions.js +1 -1
- package/dist/types/assets.d.ts +3 -2
- package/dist/types/authoring.d.ts +3 -6
- package/dist/types/body-schema.d.ts +2 -1
- package/dist/types/catalog.d.ts +2 -1
- package/dist/types/config.d.ts +0 -8
- package/dist/types/context.d.ts +3 -1
- package/dist/types/egress.d.ts +1 -1
- package/dist/types/explain.d.ts +4 -4
- package/dist/types/extension-artifacts.d.ts +4 -13
- package/dist/types/extension-transport.d.ts +6 -4
- package/dist/types/functions.d.ts +1 -1
- package/dist/types/http-response.d.ts +7 -3
- package/dist/types/init-with.d.ts +3 -9
- package/dist/types/logging.d.ts +4 -3
- package/dist/types/match.d.ts +2 -1
- package/dist/types/object-guards.d.ts +7 -0
- package/dist/types/pattern-guard.d.ts +0 -1
- package/dist/types/policies/cache.d.ts +4 -3
- package/dist/types/policies/compression.d.ts +1 -1
- package/dist/types/policies/throttle.d.ts +1 -1
- package/dist/types/policy.d.ts +4 -3
- package/dist/types/project-dependencies.d.ts +1 -40
- package/dist/types/proxy.d.ts +2 -1
- package/dist/types/readiness.d.ts +12 -16
- package/dist/types/review.d.ts +0 -2
- package/dist/types/route-diff.d.ts +4 -3
- package/dist/types/router.d.ts +1 -1
- package/dist/types/server.d.ts +22 -0
- package/dist/types/signals.d.ts +3 -2
- package/dist/types/site.d.ts +1 -2
- package/dist/types/trusted-functions.d.ts +1 -1
- package/dist/types/types.d.ts +17 -11
- package/dist/types/vercel.d.ts +1 -0
- package/dist/types/verify-deployment.d.ts +1 -1
- package/dist/types.js +19 -12
- package/dist/vercel.js +12 -3
- package/dist/verify-deployment.js +29 -11
- package/docs/AI-AUTHORING.md +10 -1
- package/docs/FUNCTION-SECURITY.md +4 -0
- package/docs/README.md +2 -2
- package/docs/TOOLING.md +27 -1
- package/examples/assets/Makefile +1 -1
- package/examples/assets/example.yaml +3 -3
- package/examples/aws/example.yaml +3 -3
- package/examples/cloudflare/example.yaml +3 -3
- package/examples/compliance/README.md +1 -1
- package/examples/compliance/example.yaml +1 -1
- package/examples/compliance/rules.mjs +2 -2
- package/examples/conditions/example.yaml +3 -3
- package/examples/cookbook/README.md +4 -4
- package/examples/cookbook/example.yaml +3 -3
- package/examples/coverage-waiver/example.yaml +3 -3
- package/examples/egress/example.yaml +2 -2
- package/examples/extensions/example.yaml +1 -1
- package/examples/lifecycle/example.yaml +2 -2
- package/examples/not-found/README.md +2 -2
- package/examples/not-found/example.yaml +3 -3
- package/examples/prerender/README.md +4 -4
- package/examples/prerender/example.yaml +2 -2
- package/examples/provider-conformance/example.yaml +2 -2
- package/examples/shared-blocks/example.yaml +3 -3
- package/examples/vercel/example.yaml +3 -3
- package/llms-full.txt +49 -18
- package/llms.txt +9 -2
- package/package.json +14 -13
- package/skills/urlcode/SKILL.md +7 -7
- package/starters/default/.github/workflows/urlcode.yml +2 -1
- package/starters/default/AGENTS.md +3 -3
- package/starters/default/Makefile +1 -1
- package/starters/default/README.md +15 -13
- package/starters/default/tests/requests.json +0 -50
- package/starters/default/urlcode.yaml +1 -4
- package/dist/scripts/operational-drills.js +0 -33
- package/starters/default/functions/hello.mjs +0 -3
- package/starters/default/middleware/headers.mjs +0 -6
- package/starters/default/routes/functions.yaml +0 -20
- package/starters/default/routes/marketing/links.yaml +0 -7
- package/starters/page/README.md +0 -14
- package/starters/page/public/index.html +0 -12
- package/starters/page/tests/requests.json +0 -17
- package/starters/page/urlcode.yaml +0 -6
package/dist/cli.js
CHANGED
|
@@ -30,11 +30,10 @@ import { readFile } from 'node:fs/promises';
|
|
|
30
30
|
import { installArtifact, inspectArtifacts } from './extension-artifacts.js';
|
|
31
31
|
import { installBundle, readBundleLock } from './extension-bundles.js';
|
|
32
32
|
|
|
33
|
-
const usage = `URLCode 0.5.
|
|
34
|
-
urlcode init <directory> [--
|
|
35
|
-
#
|
|
36
|
-
# init works in place in a directory holding only package.json, package-lock.json, node_modules or .git
|
|
37
|
-
# --template page: the smallest project (urlcode.yaml, public/index.html, README.md, tests/requests.json), one page route; not combinable with --with
|
|
33
|
+
const usage = `URLCode 0.5.8 — local/self-hosted runtime
|
|
34
|
+
urlcode init <directory> [--with ui,auth,admin] [--bundle-release extension-bundles@vX.Y.Z] [--ack extension:id] [--manifest|--no-manifest] [--pin @scope/pkg=specifier]
|
|
35
|
+
# Writes one bare project scaffold (urlcode.yaml, empty fixtures, AGENTS.md, .mcp.json and project CI). Add routes deliberately after asking the local MCP for task-scoped context.
|
|
36
|
+
# init works in place in a directory holding only package.json, package-lock.json, node_modules or .git; an existing package.json is preserved, any other existing file is refused
|
|
38
37
|
# --with: layered site from installed @jimhoyd/urlcode-<name> packages, with a package.json pinning them exactly; --bundle-release instead verifies frozen first-party bundles and writes no npm extension dependency. --with is an unordered set, core orders the host from each extension's declared requirements and refuses a missing requirement, conflict or cycle before writing
|
|
39
38
|
# --ack: repeatable, qualified acknowledgement of a risk an extension names when it refuses (for example store:public-write); do not pass it pre-emptively, the refusal prints the exact command. Rejected when no scaffold consumes it
|
|
40
39
|
# --manifest: also pin the runtime for a route-only project; --no-manifest: --with without a package.json
|
|
@@ -43,10 +42,16 @@ const usage = `URLCode 0.5.5 — local/self-hosted runtime
|
|
|
43
42
|
urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
|
|
44
43
|
urlcode dev [--project directory] [--port 3000] [--host 127.0.0.1]
|
|
45
44
|
urlcode serve [--project directory] [--port 3000] [--host 127.0.0.1] [--origin https://links.example]
|
|
45
|
+
# --port defaults to the PORT environment variable, then 3000, so a container/PaaS can set the listen port without changing the command
|
|
46
46
|
capacity: [--workers 2] [--function-timeout-ms 5000] [--max-response-bytes 1048576]
|
|
47
47
|
[--max-body-bytes 1048576] [--max-in-flight 64] [--max-in-flight-health 16]
|
|
48
48
|
logging: [--request-log minimal|detailed] [--trust-request-id] [--metrics] # metrics: GET /_urlcode/metrics, Prometheus text; keep internal
|
|
49
49
|
policies: [--trusted-proxies 10.0.0.0/8,fd00::/8] # peers allowed to set X-Forwarded-For for client policies
|
|
50
|
+
health: [--health-details] # include version/route count on GET /_urlcode/health (default: only with --metrics); keep internal
|
|
51
|
+
shutdown: [--drain-delay-ms 0] [--close-timeout-ms 10000]
|
|
52
|
+
# drain-delay-ms: /_urlcode/ready reports unhealthy this long before the listener stops accepting connections, for a load balancer to notice
|
|
53
|
+
# close-timeout-ms: in-flight connections get this long to finish once accepting stops, then are forced closed; keep below the process supervisor's stop grace period (Docker --stop-timeout, Kubernetes terminationGracePeriodSeconds)
|
|
54
|
+
timeouts: [--headers-timeout-ms 10000] [--request-timeout-ms 15000] [--keep-alive-timeout-ms 5000]
|
|
50
55
|
urlcode add <destination-url> [--alias short-code] [--project directory]
|
|
51
56
|
urlcode test [--project directory] [--origin https://links.example]
|
|
52
57
|
urlcode build --target cloudflare|static [--project directory] [--out dist/cloudflare|dist/static] [--origin https://links.example]
|
|
@@ -102,22 +107,26 @@ Dev loads .env.local and watches; serve does neither. Functions run trusted and
|
|
|
102
107
|
const print = (value ) => process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value) + '\n');
|
|
103
108
|
const options = {
|
|
104
109
|
json:{ type:'boolean' }, yaml:{ type:'boolean' }, report:{type:'string'}, 'accept-provider-differences':{type:'boolean'},
|
|
105
|
-
project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
|
|
110
|
+
project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
|
|
106
111
|
manifest:{type:'boolean'}, 'no-manifest':{type:'boolean'}, pin:{type:'string', multiple:true}, ack:{type:'string', multiple:true},
|
|
107
112
|
port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
|
|
108
113
|
'expect-routes':{type:'string'}, requests:{type:'string'}, concurrency:{type:'string'}, seconds:{type:'string'}, 'max-p95-ms':{type:'string'}, warmup:{type:'string'}, target:{type:'string'},
|
|
109
114
|
workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
|
|
110
115
|
'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'},
|
|
116
|
+
'health-details':{type:'boolean'}, 'close-timeout-ms':{type:'string'}, 'drain-delay-ms':{type:'string'},
|
|
117
|
+
'headers-timeout-ms':{type:'string'}, 'request-timeout-ms':{type:'string'}, 'keep-alive-timeout-ms':{type:'string'},
|
|
111
118
|
release:{type:'string'}, 'git-commit':{type:'string'}, 'timeout-ms':{type:'string'}, 'fail-on':{type:'string'}, 'expect-metrics':{type:'boolean'},
|
|
112
119
|
budget:{type:'string'}, task:{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' }, verbose:{ type:'boolean' }, 'allow-authoring':{ type:'boolean' }, help:{ type:'boolean', short:'h' },
|
|
113
120
|
'artifact-release':{type:'string'}, 'bundle-release':{type:'string'},
|
|
114
121
|
} ;
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
|
|
117
124
|
// Deployment controls the container/CLI must be able to set; the embedding JS
|
|
118
125
|
// API is not reachable from `urlcode serve`.
|
|
119
126
|
const capacityFlags = [['workers','workers'],['function-timeout-ms','timeoutMs'],['max-response-bytes','maxBytes'],
|
|
120
|
-
['max-body-bytes','maxBodyBytes'],['max-in-flight','maxInFlightRequests'],['max-in-flight-health','maxInFlightHealthRequests']
|
|
127
|
+
['max-body-bytes','maxBodyBytes'],['max-in-flight','maxInFlightRequests'],['max-in-flight-health','maxInFlightHealthRequests'],
|
|
128
|
+
['close-timeout-ms','closeTimeoutMs'],['drain-delay-ms','readinessDrainMs'],
|
|
129
|
+
['headers-timeout-ms','headersTimeoutMs'],['request-timeout-ms','requestTimeoutMs'],['keep-alive-timeout-ms','keepAliveTimeoutMs']] ;
|
|
121
130
|
function serverCapacity(values ) {
|
|
122
131
|
const options = {};
|
|
123
132
|
for (const [flag,key] of capacityFlags) {
|
|
@@ -132,6 +141,7 @@ function serverCapacity(values ) {
|
|
|
132
141
|
}
|
|
133
142
|
if (values['trust-request-id']) options.trustRequestId = true;
|
|
134
143
|
if (values.metrics) options.metrics = true;
|
|
144
|
+
if (values['health-details']) options.healthDetails = true;
|
|
135
145
|
if (values['trusted-proxies'] !== undefined) options.trustedProxies = values['trusted-proxies'];
|
|
136
146
|
return options;
|
|
137
147
|
}
|
|
@@ -168,12 +178,25 @@ function addressInUseMessage(error ) {
|
|
|
168
178
|
return `${where} is already in use; pick another with --port N, or stop the process using it`;
|
|
169
179
|
}
|
|
170
180
|
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' };
|
|
181
|
+
// An unhandled rejection anywhere in the process (this CLI's own code, a
|
|
182
|
+
// trusted project function, an observer) must not fail silently as a bare
|
|
183
|
+
// Node warning: log a structured event and exit non-zero so a supervisor
|
|
184
|
+
// notices and restarts.
|
|
185
|
+
process.on('unhandledRejection', reason => {
|
|
186
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
187
|
+
process.stderr.write(JSON.stringify({ event:'error', message:'Unhandled rejection: ' + message }) + '\n');
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
|
171
191
|
let operatorHost = {};
|
|
172
192
|
let serving = false;
|
|
173
193
|
try {
|
|
174
194
|
const { values, positionals } = parseArgs({ allowPositionals:true, options });
|
|
175
195
|
const [command, arg, ...extra] = positionals;
|
|
176
|
-
|
|
196
|
+
// PORT follows the common container convention (Heroku/Cloud Run/Docker
|
|
197
|
+
// `-e PORT=`) so an operator can change the listen port without editing the
|
|
198
|
+
// image's CMD; --port still wins when given explicitly.
|
|
199
|
+
values.port ??= process.env.PORT ?? '3000';
|
|
177
200
|
if (values.help || !command) print(usage);
|
|
178
201
|
else {
|
|
179
202
|
if (values['host-file'] !== undefined) {
|
|
@@ -181,7 +204,6 @@ try {
|
|
|
181
204
|
// The MCP server and context command load and release the host themselves.
|
|
182
205
|
if (command !== 'mcp' && command !== 'context') operatorHost = await loadOperatorHost(values['host-file'], values.project);
|
|
183
206
|
}
|
|
184
|
-
if (values.template !== undefined && command !== 'init') throw new ConfigError('--template is only supported by init');
|
|
185
207
|
if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
|
|
186
208
|
if ((values.manifest || values['no-manifest'] || values.pin !== undefined) && command !== 'init') throw new ConfigError('--manifest/--no-manifest/--pin are only supported by init');
|
|
187
209
|
if (values.manifest && values['no-manifest']) throw new ConfigError('Use either --manifest or --no-manifest');
|
|
@@ -339,12 +361,9 @@ try {
|
|
|
339
361
|
const wanted = values.with === undefined ? values.manifest === true : !values['no-manifest'];
|
|
340
362
|
const pins = new Map((values.pin ?? []).map(parsePin));
|
|
341
363
|
if (pins.size && !wanted) throw new ConfigError('--pin needs a manifest; drop --no-manifest or add --manifest');
|
|
342
|
-
if (values.template !== undefined && values.template !== 'default' && values.template !== 'page' && values.template !== 'redirects') throw new ConfigError('--template must be page or redirects');
|
|
343
|
-
if ((values.template === 'page' || values.template === 'redirects') && values.with !== undefined) throw new ConfigError(`--template ${values.template} cannot be combined with --with`);
|
|
344
|
-
if (values.template === 'redirects' && (wanted || pins.size)) throw new ConfigError('--template redirects writes its own package.json pinning this runtime; drop --manifest and --pin');
|
|
345
364
|
if (values.with === undefined) {
|
|
346
365
|
const set = wanted ? await collectDependencySet([], [], { overrides: pins }) : undefined;
|
|
347
|
-
const created = await initProject(arg, { manifest: set
|
|
366
|
+
const created = await initProject(arg, { manifest: set });
|
|
348
367
|
print(set ? { event:'created', dependencies:set.pins, nextSteps:installSteps(created, set) } : { event:'created' });
|
|
349
368
|
break;
|
|
350
369
|
}
|
|
@@ -375,7 +394,15 @@ try {
|
|
|
375
394
|
print({ event:'listening', address:app.address.address, port:app.address.port, mode:command, origin:app.origin });
|
|
376
395
|
serving = true;
|
|
377
396
|
let stopping = false;
|
|
378
|
-
const stop = async () => {
|
|
397
|
+
const stop = async () => {
|
|
398
|
+
if (stopping) return; stopping = true;
|
|
399
|
+
try { await app.close(); }
|
|
400
|
+
catch (error) {
|
|
401
|
+
process.stderr.write(JSON.stringify({ event:'error', message:'Shutdown failed: ' + (error instanceof Error ? error.message : String(error)) }) + '\n');
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
finally { await operatorHost.close?.(); }
|
|
405
|
+
};
|
|
379
406
|
process.once('SIGINT',stop); process.once('SIGTERM',stop);
|
|
380
407
|
break;
|
|
381
408
|
}
|
package/dist/cloudflare.js
CHANGED
|
@@ -42,6 +42,31 @@ const encoder = new TextEncoder();
|
|
|
42
42
|
const dict = () => Object.create(null) ;
|
|
43
43
|
const redirecting = (route ) => Boolean(route.redirect);
|
|
44
44
|
|
|
45
|
+
// Enforces `maxBytes` while reading, rather than buffering the whole body
|
|
46
|
+
// with `arrayBuffer()` and only then checking its length: an oversized body
|
|
47
|
+
// would otherwise sit fully in Worker memory before this runtime ever gets
|
|
48
|
+
// to refuse it. A declared Content-Length over the limit is refused before a
|
|
49
|
+
// single byte is read; a running total over the limit cancels the stream.
|
|
50
|
+
async function readCappedBody(request , limit ) {
|
|
51
|
+
const declared = request.headers.get('content-length');
|
|
52
|
+
if (declared !== null && Number(declared) > limit) throw new HttpError(413, 'Request body too large');
|
|
53
|
+
if (!request.body) return new Uint8Array(0);
|
|
54
|
+
const reader = request.body.getReader();
|
|
55
|
+
const chunks = [];
|
|
56
|
+
let total = 0;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
total += value.byteLength;
|
|
61
|
+
if (total > limit) { await reader.cancel().catch(() => {}); throw new HttpError(413, 'Request body too large'); }
|
|
62
|
+
chunks.push(value);
|
|
63
|
+
}
|
|
64
|
+
const body = new Uint8Array(total);
|
|
65
|
+
let offset = 0;
|
|
66
|
+
for (const chunk of chunks) { body.set(chunk, offset); offset += chunk.byteLength; }
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
// The artifact stores what the compiler produced; validators arrive separately
|
|
46
71
|
// because a schema validator cannot be serialised and this platform forbids
|
|
47
72
|
// compiling one at runtime.
|
|
@@ -132,7 +157,7 @@ export function createFetchHandler(artifact , validators )
|
|
|
132
157
|
body: encoder.encode('Method not allowed\n') }), { requestId, method }), requestId, method);
|
|
133
158
|
}
|
|
134
159
|
const body = route.request?.body
|
|
135
|
-
?
|
|
160
|
+
? await readCappedBody(request, route.request.body.maxBytes ?? 1048576)
|
|
136
161
|
: new Uint8Array(0);
|
|
137
162
|
// Duplicate request headers are joined by the platform before this runs,
|
|
138
163
|
// so per-header counts are unavailable and the duplicate-scalar check
|
package/dist/config.js
CHANGED
|
@@ -82,7 +82,7 @@ function closestKey(key , allowed ) {
|
|
|
82
82
|
* Only key names, which come from the schema or the author's own mapping keys, are echoed, never values
|
|
83
83
|
* (values may hold secrets), and never more than MAX_NAMED_KEY characters of a key.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
function describeSchemaError(e ) {
|
|
86
86
|
const base = `Invalid configuration at ${e.instancePath || '/'} (${e.keyword})`;
|
|
87
87
|
const parent = e.parentSchema ;
|
|
88
88
|
if (e.keyword === 'additionalProperties') {
|
package/dist/context.js
CHANGED
|
@@ -9,6 +9,7 @@ import {compilePolicies,closePolicies,effectivePolicies,registry} from './polici
|
|
|
9
9
|
import {capabilityTargets,getCapabilities,normalizeCapabilityTarget,routeCapabilities} from './capabilities.js';
|
|
10
10
|
|
|
11
11
|
import {loadOperatorHost} from './operator-host.js';
|
|
12
|
+
import {handlerNames,resolveHandlerName} from './types.js';
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
|
|
@@ -25,7 +26,7 @@ import {loadOperatorHost} from './operator-host.js';
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
|
|
@@ -40,7 +41,6 @@ import {loadOperatorHost} from './operator-host.js';
|
|
|
40
41
|
/** Characters divided by four, rounded up: an estimate, not a tokenizer. */
|
|
41
42
|
export function estimateTokens(text ) {return Math.ceil(text.length/4);}
|
|
42
43
|
export function renderContext(context ) {return stringify(context,{lineWidth:0,aliasDuplicateObjects:false});}
|
|
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 ={
|
|
@@ -67,7 +67,7 @@ async function compile(project ) {
|
|
|
67
67
|
try {for(const route of routes)await compilePolicies(loaded.document,route,{route,shared,target:'node',root:loaded.root});}finally{await closePolicies(shared);}
|
|
68
68
|
return {loaded,compiled,routes};
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
const handlerOf = (route ) => resolveHandlerName(route,'none');
|
|
71
71
|
/** Derived only from the compiled project and the capability catalog, never from prose. Key order is fixed. */
|
|
72
72
|
export async function buildContext(project ,options ={}) {
|
|
73
73
|
const budget=options.budget;
|
|
@@ -111,7 +111,7 @@ export async function buildContext(project ,options ={})
|
|
|
111
111
|
const context ={
|
|
112
112
|
urlcode:await packageVersion(),schema:'1',
|
|
113
113
|
project:{
|
|
114
|
-
entry:'urlcode.yaml',routes:compiled.count,handlers,extensions:sorted(Object.keys(document.extensions??{})),
|
|
114
|
+
entry:'urlcode.yaml',routes:compiled.count,routeCountNote:'routes includes declared routes plus generated site.* convention routes; audit reports the declared/generated split on mismatch.',handlers,extensions:sorted(Object.keys(document.extensions??{})),
|
|
115
115
|
policies:{project:policyNames.filter(name=>topLevel[name]),routes:policyCounts},
|
|
116
116
|
bindings:{env:sorted(env),secrets:sorted(secrets)},site:sorted(Object.keys(document.site??{})),
|
|
117
117
|
files:{includes:loaded.files.slice(1).map(file=>relative(loaded.root,file).split('\\').join('/')),functions:sorted(functions),middleware:sorted(middleware)},
|
|
@@ -138,7 +138,7 @@ const drops =[
|
|
|
138
138
|
['constraintNotes',context=>{for(const [key,item] of Object.entries(context.constraints))context.constraints[key]=typeof item==='object'?item.value:item;}],
|
|
139
139
|
['files',context=>{delete context.project.files;}],
|
|
140
140
|
['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:[]},site:[]};}],
|
|
141
|
+
['summary',context=>{context.project={entry:context.project.entry,routes:context.project.routes,routeCountNote:context.project.routeCountNote,handlers:context.project.handlers,extensions:[],policies:{project:[],routes:{}},bindings:{env:[],secrets:[]},site:[]};}],
|
|
142
142
|
];
|
|
143
143
|
function fitBudget(context ,budget ) {
|
|
144
144
|
const omitted =[];
|
|
@@ -179,7 +179,7 @@ export const redirectShapes =[
|
|
|
179
179
|
{need:'redirect loop detection',support:'gap',note:'Validation accepts a route that redirects to its own URL; nothing detects cycles. Write a fixture with expectHeaders location for each redirect and review chains by hand.'},
|
|
180
180
|
];
|
|
181
181
|
/** A complete, paste-ready project skeleton: every supported shape merged into one urlcode.yaml, plus the start script. */
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
|
package/dist/egress.js
CHANGED
|
@@ -6,7 +6,7 @@ export const EGRESS_DNS_LIMIT=64;
|
|
|
6
6
|
let unresolvedDns=0;
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
|
|
11
11
|
export class EgressError extends Error { code ; constructor(code ) { super(`Egress ${code}`); this.code=code; } }
|
|
12
12
|
/** Conservative public-unicast filter. IPv4-mapped IPv6 and transition mechanisms are denied. */
|
package/dist/examples.js
CHANGED
|
@@ -7,6 +7,7 @@ import {routeCapabilities} from './capabilities.js';
|
|
|
7
7
|
import {readMetadata,searchMetadata,searchTerms} from './catalog.js';
|
|
8
8
|
|
|
9
9
|
import {assert} from './errors.js';
|
|
10
|
+
import {resolveHandlerName} from './types.js';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
/** example.yaml, the same schema as recipe.yaml; `name` repeats `id`. */
|
|
@@ -31,8 +32,7 @@ export async function listExamples() {
|
|
|
31
32
|
for(const name of exampleNames)result.push(await metadata(name));
|
|
32
33
|
return result;
|
|
33
34
|
}
|
|
34
|
-
const
|
|
35
|
-
function handlerOf(route ) {return handlerNames.find(name=>route[name]!==undefined)??'unknown';}
|
|
35
|
+
const handlerOf = (route ) => resolveHandlerName(route, 'unknown');
|
|
36
36
|
/**
|
|
37
37
|
* Derives the per-route tag index of one project from its loaded routes: handler,
|
|
38
38
|
* methods, capabilities, policy names and middleware module names. The file each
|
package/dist/explain.js
CHANGED
|
@@ -5,8 +5,8 @@ import {analyzeCompiledCapabilities,capabilityTargets,routeCapabilities} from '.
|
|
|
5
5
|
import {effectiveExtensionPolicies} from './extensions.js';
|
|
6
6
|
|
|
7
7
|
import {effectivePolicies} from './policies.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {handlerNames} from './types.js';
|
|
9
|
+
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
@@ -15,10 +15,8 @@ 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'] ;
|
|
19
|
-
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
|
|
@@ -42,7 +40,7 @@ const handlerNames=['extension','proxy','conditional','redirect','function','pag
|
|
|
42
40
|
|
|
43
41
|
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
|
|
46
44
|
|
|
47
45
|
const relativeSource=(root ,source ) =>relative(root,source).split('\\').join('/');
|
|
48
46
|
function origin(url ) {try{return new URL(url).origin;}catch{return url;}}
|
|
@@ -6,7 +6,7 @@ import { ConfigError, assert } from './errors.js';
|
|
|
6
6
|
import { isRecord as record, digestHex as digest, textField, exactKeys as sharedExactKeys, listCachedFiles, writeLockAtomic, createGithubTransport, verifiedReleaseAsset, } from './extension-transport.js';
|
|
7
7
|
|
|
8
8
|
/** Offline, declarative extension bundles. These are deliberately not Node packages. */
|
|
9
|
-
|
|
9
|
+
const ARTIFACT_REPOSITORY = 'jimhoyd-com/urlcode';
|
|
10
10
|
export const ARTIFACT_WORKFLOW = 'jimhoyd-com/urlcode/.github/workflows/extension-artifacts.yml';
|
|
11
11
|
const MAX_ARCHIVE = 16 * 1024 * 1024, MAX_EXPANDED = 32 * 1024 * 1024, MAX_FILES = 128, MAX_FILE = 2 * 1024 * 1024;
|
|
12
12
|
const MAX_TOOL_FILE = 512 * 1024;
|
|
@@ -14,10 +14,10 @@ const hex = /^[a-f0-9]{64}$/;
|
|
|
14
14
|
const name = /^[a-z][a-z0-9-]{0,63}$/;
|
|
15
15
|
const tag = /^extensions@v[0-9][0-9A-Za-z._-]{0,100}$/;
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
|
|
21
21
|
function text(value , what ) { return textField(value, `${what} in extension artifact metadata`); }
|
|
22
22
|
function exactKeys(value , expected , what ) { sharedExactKeys(value, expected, what); }
|
|
23
23
|
|
|
@@ -44,7 +44,7 @@ export function parseCatalog(bytes , requestedTag ) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
function octal(bytes ) { const value=new TextDecoder().decode(bytes).replace(/\0.*$/,'').trim(); assert(/^[0-7]*$/.test(value),'Malformed extension archive'); return value ? Number.parseInt(value,8) : 0; }
|
|
49
49
|
function archivePath(name ,prefix ) {
|
|
50
50
|
const decode=(bytes )=>new TextDecoder().decode(bytes).replace(/\0.*$/,'');
|
|
@@ -83,15 +83,15 @@ export async function extractArtifact(bytes , entry , des
|
|
|
83
83
|
const root=resolve(destination), temporary=join(tmpdir(),`urlcode-extension-${process.pid}-${Math.random().toString(16).slice(2)}`); await mkdir(temporary,{recursive:true});
|
|
84
84
|
try { for(const file of files) { const target=resolve(temporary,file.path); assert(relative(temporary,target) && !relative(temporary,target).startsWith('..'),'Unsafe extension archive path'); await mkdir(dirname(target),{recursive:true}); await writeFile(target,file.bytes,{flag:'wx'}); } await writeFile(join(temporary,'.artifact.tgz'),bytes,{flag:'wx'}); await mkdir(dirname(root),{recursive:true}); try { await rename(temporary,root); } catch { try { await validateCached(root,entry); return; } catch { throw new ConfigError(`Extension cache entry ${entry.sha256} already exists but is not identical`); } } } finally { await rm(temporary,{recursive:true,force:true}); }
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
async function readLock(project ) { let raw ; try { const path=join(project,'urlcode.extensions.lock.json'), info=await lstat(path); assert(info.isFile()&&!info.isSymbolicLink()&&info.nlink===1,'Extension artifact lockfile must be an ordinary file'); raw=JSON.parse(await readFile(path,'utf8')); } catch(error) { if(error instanceof ConfigError)throw error; throw new ConfigError('No extension artifact lockfile; install an artifact first'); } assert(record(raw)&&raw.format===1&&Array.isArray(raw.artifacts),'Invalid extension artifact lockfile'); exactKeys(raw,['format','artifacts'],'Extension artifact lockfile'); const seen=new Set (),digests=new Set (); const artifacts=raw.artifacts.map(value=>{ assert(record(value)&&record(value.catalog),'Invalid extension artifact lockfile'); exactKeys(value,['name','version','asset','sha256','kind','catalog'],'Extension artifact lock entry'); exactKeys(value.catalog,['tag','commit'],'Extension artifact lock catalog'); const item ={name:text(value.name,'lockfile artifact'),version:text(value.version,'lockfile artifact'),asset:text(value.asset,'lockfile artifact'),sha256:text(value.sha256,'lockfile artifact'),kind:value.kind==='declarative'?'declarative':(()=>{throw new ConfigError('Invalid extension artifact lockfile');})(),catalog:{tag:text(value.catalog.tag,'lockfile tag'),commit:text(value.catalog.commit,'lockfile commit')}}; assert(name.test(item.name)&&/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(item.version)&&/^[A-Za-z0-9._-]+\.tgz$/.test(item.asset)&&hex.test(item.sha256)&&tag.test(item.catalog.tag)&&/^[a-f0-9]{40}$/.test(item.catalog.commit)&&!seen.has(item.name)&&!digests.has(item.sha256),'Invalid or duplicate extension artifact lock entry'); seen.add(item.name); digests.add(item.sha256); return item; }); return {format:1,artifacts}; }
|
|
87
87
|
export async function writeLock(project , lock ) { const path=join(project,'urlcode.extensions.lock.json'), temporary=join(project,`.urlcode.extensions.lock.${process.pid}.${Math.random().toString(16).slice(2)}`); await writeLockAtomic(path,temporary,lock); }
|
|
88
88
|
export function cachePath(project , sha256 ) { assert(hex.test(sha256),'Invalid extension digest'); return join(project,'.urlcode','extensions',sha256); }
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
/** The default transport accepts only GitHub Release asset URLs and verifies every downloaded subject. */
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const githubTransport =createGithubTransport({repository:ARTIFACT_REPOSITORY,workflow:ARTIFACT_WORKFLOW,tagPattern:tag,exampleTag:'extensions@v1.0.0',maxAssetSize:MAX_ARCHIVE,itemLabel:'extension artifact'});
|
|
94
|
+
async function resolveCatalog(release , transport =githubTransport) { const assets=await transport.release(release); const bytes=await verifiedReleaseAsset(assets,'extensions-catalog.json',release,transport,'extension artifact','urlcode-attest'); return {catalog:parseCatalog(bytes,release),assets}; }
|
|
95
95
|
export async function installArtifact(project , release , artifactName , transport =githubTransport) {
|
|
96
96
|
assert(name.test(artifactName),'Invalid extension artifact name'); const {catalog,assets}=await resolveCatalog(release,transport); const entry=catalog.artifacts.find(item=>item.name===artifactName); assert(entry,`Extension artifact ${artifactName} is not in the signed catalog`); const revoked=catalog.revoked.find(item=>item.sha256===entry.sha256); assert(!revoked,`Extension artifact ${artifactName} is revoked: ${revoked?.reason ?? 'unknown reason'}`);
|
|
97
97
|
const bytes=await verifiedReleaseAsset(assets,entry.asset,release,transport,'extension artifact','urlcode-attest'); await extractArtifact(bytes,entry,cachePath(project,entry.sha256));
|
|
@@ -4,11 +4,13 @@ import { readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ConfigError, assert } from './errors.js';
|
|
7
|
+
import { isRecord } from './object-guards.js';
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
/** Shared GitHub release/cache/lockfile plumbing for extension-artifacts.ts and extension-bundles.ts. No opinion on what content is allowed or executable; that trust boundary stays local to each caller (#441). */
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
export { isRecord };
|
|
13
|
+
|
|
12
14
|
export const digestHex=(bytes ) =>createHash('sha256').update(bytes).digest('hex');
|
|
13
15
|
export function textField(value , what ) { assert(typeof value==='string' && value.length>0 && value.length<256,`Invalid ${what}`); return value; }
|
|
14
16
|
export function exactKeys(value , expected , what ) { const actual=Object.keys(value).sort(); assert(JSON.stringify(actual)===JSON.stringify([...expected].sort()),`${what} has unknown or missing fields`); }
|
|
@@ -21,8 +23,8 @@ export async function listCachedFiles(root , itemLabel , prefix='')
|
|
|
21
23
|
export async function writeLockAtomic(path , temporary , data ) { await writeFile(temporary,JSON.stringify(data,null,2)+'\n',{flag:'wx'}); try { await rename(temporary,path); } finally { await rm(temporary,{force:true}); } }
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
/** A transport that accepts only GitHub Release asset URLs and verifies every downloaded subject. */
|
|
28
30
|
export function createGithubTransport(config ) {
|
package/dist/function-worker.js
CHANGED
|
@@ -45,6 +45,7 @@ async function evaluate(entry , name , pay
|
|
|
45
45
|
run(guestBootstrap);
|
|
46
46
|
if (payload !== undefined) {
|
|
47
47
|
const value = vm.newString(payload); vm.setProp(vm.global,'__payload',value); value.dispose();
|
|
48
|
+
run("Object.defineProperty(globalThis,'__payload',{writable:false,enumerable:false,configurable:false})");
|
|
48
49
|
}
|
|
49
50
|
if (chain.length) {
|
|
50
51
|
const imports = chain.map((item,i) => `import * as mw${i} from ${JSON.stringify(item.source)};`).join('\n');
|
|
@@ -56,7 +57,7 @@ async function evaluate(entry , name , pay
|
|
|
56
57
|
} else {
|
|
57
58
|
run(`import * as entry from ${JSON.stringify(entry)};
|
|
58
59
|
if (typeof entry[${JSON.stringify(name)}] !== 'function') throw new Error('Invalid export');
|
|
59
|
-
${payload === undefined ?
|
|
60
|
+
${payload === undefined ? 'globalThis.__ready();' : `globalThis.__invoke(entry[${JSON.stringify(name)}], globalThis.__payload);`}`,
|
|
60
61
|
'/__urlcode_entry.mjs','module');
|
|
61
62
|
}
|
|
62
63
|
while (string('__state') === 'pending') {
|
|
@@ -83,7 +84,9 @@ port.on('message', async ({id,source,name,request,context,maxBytes,timeoutMs,cha
|
|
|
83
84
|
if (output === undefined || Buffer.byteLength(output) > maxBytes * 6 + 65536) throw new Error('Output limit');
|
|
84
85
|
const value = JSON.parse(output) ; // trust boundary: guest JSON, checked below
|
|
85
86
|
if (!value || !Number.isInteger(value.status) || value.status < 200 || value.status > 599 || typeof value.body !== 'string' || !Array.isArray(value.headers) || value.headers.length > 256) throw new Error('Invalid response');
|
|
86
|
-
|
|
87
|
+
// Only HEAD may state a length: it carries no body. Any other method is
|
|
88
|
+
// framed by the body bytes, so a stated length there is a shape violation.
|
|
89
|
+
if (value.contentLength !== undefined && (request.method !== 'HEAD' || !Number.isSafeInteger(value.contentLength) || value.contentLength < 0)) throw new Error('Invalid response');
|
|
87
90
|
if (value.nativeBody) {
|
|
88
91
|
if (!native || value.status !== native.status || value.body !== '') throw new Error('Invalid native response');
|
|
89
92
|
// Preserve native status and metadata (validators, ranges, redirect Location).
|
package/dist/functions.js
CHANGED
package/dist/guest-api.js
CHANGED
|
@@ -31,15 +31,25 @@ export const guestBootstrap = String.raw`
|
|
|
31
31
|
}
|
|
32
32
|
return bytes;
|
|
33
33
|
}
|
|
34
|
+
// The invocation's outcome lives in this closure. The host reads it through
|
|
35
|
+
// accessors and calls the entry points below, all fixed before guest code
|
|
36
|
+
// runs, so a guest can neither rewrite its result nor replace the code that
|
|
37
|
+
// shapes it. The first outcome settles the invocation.
|
|
38
|
+
let state = 'pending', output = '';
|
|
39
|
+
const settle = (next, text = '') => { if (state === 'pending') { state = next; output = text; } };
|
|
40
|
+
const fix = (name, value) => Object.defineProperty(globalThis, name, {value, writable:false, enumerable:false, configurable:false});
|
|
41
|
+
Object.defineProperty(globalThis, '__state', {get: () => state, enumerable:false, configurable:false});
|
|
42
|
+
Object.defineProperty(globalThis, '__output', {get: () => output, enumerable:false, configurable:false});
|
|
43
|
+
fix('__ready', () => settle('done'));
|
|
34
44
|
const timers = new Map(); let next = 1;
|
|
35
45
|
globalThis.setTimeout = (fn, delay = 0) => {
|
|
36
46
|
if (typeof fn !== 'function' || timers.size >= 128) throw new Error('Timer limit');
|
|
37
47
|
const id = next++; timers.set(id, {fn, at:now() + Math.max(0, Number(delay) || 0)}); return id;
|
|
38
48
|
};
|
|
39
49
|
globalThis.clearTimeout = id => timers.delete(id);
|
|
40
|
-
|
|
50
|
+
fix('__pump', () => {
|
|
41
51
|
for (const [id,timer] of timers) if (timer.at <= now()) { timers.delete(id); timer.fn(); }
|
|
42
|
-
};
|
|
52
|
+
});
|
|
43
53
|
globalThis.console = Object.freeze({log(){},error(){},warn(){},info(){},debug(){}});
|
|
44
54
|
class Headers {
|
|
45
55
|
constructor(init = []) {
|
|
@@ -83,7 +93,7 @@ export const guestBootstrap = String.raw`
|
|
|
83
93
|
static redirect(url, status = 302) { if (![301,302,303,307,308].includes(status)) throw new TypeError('Invalid redirect status'); return new Response(null,{status,headers:{location:String(url)}}); }
|
|
84
94
|
}
|
|
85
95
|
globalThis.Headers = Headers; globalThis.Request = Request; globalThis.Response = Response;
|
|
86
|
-
|
|
96
|
+
fix('__invokePipeline', async (middleware, handler, payload) => {
|
|
87
97
|
try {
|
|
88
98
|
const input = NativeJSON.parse(payload);
|
|
89
99
|
const request = new Request(input.request.url, input.request);
|
|
@@ -117,24 +127,21 @@ export const guestBootstrap = String.raw`
|
|
|
117
127
|
// materialized string at construction time, no stream read needed
|
|
118
128
|
// (#144, mirroring #139's fix for the trusted path). Only the
|
|
119
129
|
// transmitted bytes are suppressed for HEAD, never the length.
|
|
120
|
-
|
|
130
|
+
settle('done', stringify({status:response.status,headers:response.headers._pairs,
|
|
121
131
|
body:nativeBody || isHead ? '' : response._text,nativeBody,
|
|
122
|
-
...(isHead && !nativeBody ? {contentLength:byteLength(response._text)} : {})});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
globalThis.__state = 'pending'; globalThis.__output = '';
|
|
127
|
-
globalThis.__invoke = async (handler, payload) => {
|
|
132
|
+
...(isHead && !nativeBody ? {contentLength:byteLength(response._text)} : {})}));
|
|
133
|
+
} catch { settle('failed'); }
|
|
134
|
+
});
|
|
135
|
+
fix('__invoke', async (handler, payload) => {
|
|
128
136
|
try {
|
|
129
137
|
const input = NativeJSON.parse(payload);
|
|
130
138
|
const response = await handler(new Request(input.request.url, input.request), input.context);
|
|
131
139
|
if (!(response instanceof Response)) throw new TypeError('Return a Response');
|
|
132
140
|
const headers = response.headers._pairs;
|
|
133
141
|
const isHead = input.request.method === 'HEAD';
|
|
134
|
-
|
|
135
|
-
...(isHead ? {contentLength:byteLength(response._text)} : {})});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
};
|
|
142
|
+
settle('done', stringify({status:response.status,headers,body: isHead ? '' : response._text,
|
|
143
|
+
...(isHead ? {contentLength:byteLength(response._text)} : {})}));
|
|
144
|
+
} catch { settle('failed'); }
|
|
145
|
+
});
|
|
139
146
|
})();
|
|
140
147
|
`;
|
package/dist/http-policy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateHeaderName, validateHeaderValue } from './header-validation.js';
|
|
2
2
|
import { assert, HttpError } from './errors.js';
|
|
3
|
+
import { byteLength } from './http-response.js';
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
import { assertBodySchema, bodySchemaIssues, bodySchemaLine, bodySchemaJson, prefersJson } from './body-schema.js';
|
|
@@ -17,9 +18,6 @@ import { assertBodySchema, bodySchemaIssues, bodySchemaLine, bodySchemaJson, pre
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
const encoder = new TextEncoder();
|
|
21
|
-
const byteLength = (value ) => encoder.encode(value).length;
|
|
22
|
-
|
|
23
21
|
export const reservedResponseHeaders = new Set(['connection','keep-alive','transfer-encoding','content-length','upgrade','trailer','proxy-authenticate','proxy-authorization','te','location','allow','content-range','accept-ranges','etag','last-modified','content-encoding','x-request-id','x-content-type-options']);
|
|
24
22
|
export function compileHttp(route ) {
|
|
25
23
|
const seen = new Set (); let size = 0;
|