@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.1
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/README.md +52 -1
- package/dist/config-schema.json +73 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -5
- package/src/build/canonical-target.js +436 -0
- package/src/build/entities-native.js +564 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/native-http-effects.js +871 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2109 -0
- package/src/compiler/assets-package-out-parity-proof.js +338 -0
- package/src/compiler/assets-provider-proof.js +499 -0
- package/src/compiler/config-provider-resolution.js +573 -0
- package/src/compiler/fastly-adapter.js +564 -0
- package/src/compiler/fastly-command-entry.js +390 -0
- package/src/compiler/fastly-config-secret-serve.js +323 -0
- package/src/compiler/fastly-hostcall-binding.js +598 -0
- package/src/compiler/fastly-readiness.js +477 -0
- package/src/compiler/index.js +40 -0
- package/src/compiler/lifecycle-parity-contract.js +70 -0
- package/src/compiler/lifecycle-parity-proof.js +260 -0
- package/src/compiler/route-handler-effect-proof.js +255 -0
- package/src/compiler/runtime-provider-kv.js +237 -0
- package/src/compiler/runtime-provider-shape.js +109 -0
- package/src/config-api.js +63 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +73 -0
- package/src/index.js +3 -0
- package/src/index.ts +118 -0
- package/src/javascript/assets-adapter.js +122 -0
- package/src/javascript/bindings-adapter.js +237 -0
- package/src/javascript/errors.js +26 -0
- package/src/javascript/fetch-adapter.js +93 -0
- package/src/javascript/grip-broadcast.js +261 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +26 -0
- package/src/javascript/package-effects.js +68 -0
- package/src/javascript/runtime-host.js +108 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +776 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +260 -0
- package/src/javascript/target.js +73 -0
- package/src/javascript/test-runtime.js +407 -0
- package/src/provider-contract.js +72 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +597 -0
- package/src/runtime/index.js +28 -0
- package/src/runtime/provider-primitives-contract.js +272 -0
- package/src/runtime/provider-primitives.js +82 -0
- package/src/runtime/route-handler-effects.js +507 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-http-effects-host.js +390 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +599 -0
- package/src/toolchain/config.js +128 -0
- package/src/toolchain/index.js +343 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const { CANONICAL_NATIVE_PLAN_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
|
|
9
|
+
|
|
10
|
+
function loadBuildSupport() {
|
|
11
|
+
try { return require('@pulse-compute/wasm-build-support/assemblyscript-compile'); }
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
14
|
+
return require('../../../../wasm/packages/build-support/src/assemblyscript-compile.js');
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { resolveAsc } = loadBuildSupport();
|
|
21
|
+
|
|
22
|
+
const FASTLY_NATIVE_HTTP_SHELL_VERSION = 'pulse.fastly-native-http-shell.v1';
|
|
23
|
+
const FASTLY_NATIVE_HTTP_SHELL_GENERATOR_VERSION = 'pulse.fastly-native-http-shell-generator.v1';
|
|
24
|
+
const FASTLY_NATIVE_HTTP_SHELL_COMPILER_VERSION = 'pulse.fastly-native-http-shell-compiler.v1';
|
|
25
|
+
const FASTLY_NATIVE_HTTP_SHELL_ABI_VERSION = 1;
|
|
26
|
+
const FASTLY_NATIVE_HTTP_BUFFER_BYTES = 65536;
|
|
27
|
+
const FASTLY_NATIVE_HTTP_MAX_WASM_BYTES = 512 * 1024;
|
|
28
|
+
|
|
29
|
+
const FASTLY_NATIVE_HTTP_IMPORTS = Object.freeze([
|
|
30
|
+
Object.freeze(['fastly_abi', 'init']),
|
|
31
|
+
Object.freeze(['fastly_http_req', 'body_downstream_get']),
|
|
32
|
+
Object.freeze(['fastly_http_req', 'method_get']),
|
|
33
|
+
Object.freeze(['fastly_http_req', 'uri_get']),
|
|
34
|
+
Object.freeze(['fastly_http_req', 'header_value_get']),
|
|
35
|
+
Object.freeze(['fastly_http_resp', 'new']),
|
|
36
|
+
Object.freeze(['fastly_http_resp', 'header_append']),
|
|
37
|
+
Object.freeze(['fastly_http_resp', 'status_set']),
|
|
38
|
+
Object.freeze(['fastly_http_resp', 'send_downstream']),
|
|
39
|
+
Object.freeze(['fastly_http_body', 'new']),
|
|
40
|
+
Object.freeze(['fastly_http_body', 'write'])
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const FASTLY_NATIVE_HTTP_CORE_IMPORTS = Object.freeze(FASTLY_NATIVE_HTTP_IMPORTS.filter(([module, name]) => !(module === 'fastly_http_req' && name === 'header_value_get')));
|
|
44
|
+
|
|
45
|
+
const FASTLY_NATIVE_HTTP_ALLOWED_IMPORTS = new Set([
|
|
46
|
+
...FASTLY_NATIVE_HTTP_IMPORTS.map(([module, name]) => `${module}:${name}`)
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
class FastlyNativeHttpShellError extends Error {
|
|
50
|
+
constructor(message, code = 'PULSE_FASTLY_NATIVE_HTTP_SHELL_FAILED', detail = {}) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'FastlyNativeHttpShellError';
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.detail = Object.freeze({ ...detail });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sha256(value) {
|
|
59
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stableObject(value) {
|
|
63
|
+
if (Array.isArray(value)) return value.map(stableObject);
|
|
64
|
+
if (!value || typeof value !== 'object') return value;
|
|
65
|
+
const output = {};
|
|
66
|
+
for (const key of Object.keys(value).sort()) if (value[key] !== undefined) output[key] = stableObject(value[key]);
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stableStringify(value, space = 0) {
|
|
71
|
+
return JSON.stringify(stableObject(value), null, space);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function quote(value) {
|
|
75
|
+
return JSON.stringify(String(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fail(message, code, detail = {}) {
|
|
79
|
+
throw new FastlyNativeHttpShellError(message, code, detail);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function literalValue(expression, label) {
|
|
83
|
+
if (!expression || expression.kind !== 'literal') {
|
|
84
|
+
fail(`${label} must be a static literal in the Pass96 Fastly HTTP shell.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_VALUE_REQUIRED', { label, expression });
|
|
85
|
+
}
|
|
86
|
+
return expression.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function objectEntries(expression, label) {
|
|
90
|
+
if (!expression || expression.kind !== 'object') {
|
|
91
|
+
fail(`${label} must be a static object in the Pass96 Fastly HTTP shell.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_OBJECT_REQUIRED', { label, expression });
|
|
92
|
+
}
|
|
93
|
+
const entries = new Map();
|
|
94
|
+
for (const entry of expression.entries || []) {
|
|
95
|
+
if (entry.kind !== 'property' || !entry.key || entry.key.kind !== 'literal' || typeof entry.key.value !== 'string') {
|
|
96
|
+
fail(`${label} only accepts literal property names.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_OBJECT_REQUIRED', { label, entry });
|
|
97
|
+
}
|
|
98
|
+
entries.set(entry.key.value, entry.value);
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseStaticHeaders(expression, label) {
|
|
104
|
+
if (!expression) return [];
|
|
105
|
+
const headers = [];
|
|
106
|
+
if (expression.kind === 'array') {
|
|
107
|
+
for (const [index, item] of (expression.items || []).entries()) {
|
|
108
|
+
if (!item || item.kind !== 'array' || (item.items || []).length !== 2) {
|
|
109
|
+
fail(`${label}[${index}] must be a literal [name, value] pair.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_HEADERS_REQUIRED', { label, index, item });
|
|
110
|
+
}
|
|
111
|
+
const name = literalValue(item.items[0], `${label}[${index}][0]`);
|
|
112
|
+
const value = literalValue(item.items[1], `${label}[${index}][1]`);
|
|
113
|
+
if (typeof name !== 'string' || typeof value !== 'string') {
|
|
114
|
+
fail(`${label}[${index}] must contain two strings.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_HEADERS_REQUIRED', { label, index, name, value });
|
|
115
|
+
}
|
|
116
|
+
headers.push([name, value]);
|
|
117
|
+
}
|
|
118
|
+
return headers;
|
|
119
|
+
}
|
|
120
|
+
if (expression.kind === 'object') {
|
|
121
|
+
for (const entry of expression.entries || []) {
|
|
122
|
+
if (entry.kind !== 'property' || !entry.key || entry.key.kind !== 'literal' || typeof entry.key.value !== 'string') {
|
|
123
|
+
fail(`${label} only accepts literal header names.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_HEADERS_REQUIRED', { label, entry });
|
|
124
|
+
}
|
|
125
|
+
const name = entry.key.value;
|
|
126
|
+
if (entry.value && entry.value.kind === 'array') {
|
|
127
|
+
for (const [index, item] of (entry.value.items || []).entries()) {
|
|
128
|
+
const value = literalValue(item, `${label}.${name}[${index}]`);
|
|
129
|
+
if (typeof value !== 'string') fail(`${label}.${name}[${index}] must be a string.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_HEADERS_REQUIRED', { label, name, index, value });
|
|
130
|
+
headers.push([name, value]);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
const value = literalValue(entry.value, `${label}.${name}`);
|
|
134
|
+
if (typeof value !== 'string') fail(`${label}.${name} must be a string.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_HEADERS_REQUIRED', { label, name, value });
|
|
135
|
+
headers.push([name, value]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return headers;
|
|
139
|
+
}
|
|
140
|
+
fail(`${label} must be a static header object or array of pairs.`, 'PULSE_FASTLY_NATIVE_HTTP_STATIC_HEADERS_REQUIRED', { label, expression });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function responseOptions(expression, kind) {
|
|
144
|
+
const defaultContentType = kind === 'json' ? 'application/json; charset=utf-8' : 'text/plain; charset=utf-8';
|
|
145
|
+
const defaults = { status: 200, headers: [['content-type', defaultContentType]] };
|
|
146
|
+
if (!expression || expression.kind === 'undefined') return defaults;
|
|
147
|
+
const entries = objectEntries(expression, `${kind} response options`);
|
|
148
|
+
const allowed = new Set(['status', 'headers']);
|
|
149
|
+
for (const key of entries.keys()) {
|
|
150
|
+
if (!allowed.has(key)) {
|
|
151
|
+
fail(`Pass96 ${kind} responses do not support option ${key}.`, 'PULSE_FASTLY_NATIVE_HTTP_RESPONSE_OPTION_UNSUPPORTED', { kind, option: key });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let status = 200;
|
|
155
|
+
if (entries.has('status')) {
|
|
156
|
+
status = literalValue(entries.get('status'), `${kind} response status`);
|
|
157
|
+
if (!Number.isInteger(status) || status < 100 || status > 999) {
|
|
158
|
+
fail(`${kind} response status must be a static integer between 100 and 999.`, 'PULSE_FASTLY_NATIVE_HTTP_STATUS_INVALID', { kind, status });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const headers = entries.has('headers') ? parseStaticHeaders(entries.get('headers'), `${kind} response headers`) : [];
|
|
162
|
+
const contentType = headers.some(([name]) => name.toLowerCase() === 'content-type');
|
|
163
|
+
if (!contentType) headers.push(['content-type', defaultContentType]);
|
|
164
|
+
return { status, headers };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validatePlanBoundary(plan) {
|
|
168
|
+
if (!plan || typeof plan !== 'object') throw new TypeError('Fastly native HTTP realization requires a canonical native plan.');
|
|
169
|
+
if (plan.version !== CANONICAL_NATIVE_PLAN_VERSION) {
|
|
170
|
+
fail(`Fastly native HTTP realization requires ${CANONICAL_NATIVE_PLAN_VERSION}.`, 'PULSE_FASTLY_NATIVE_HTTP_PLAN_VERSION_UNSUPPORTED', { version: plan.version });
|
|
171
|
+
}
|
|
172
|
+
if (!plan.ownership || plan.ownership.providerNeutral !== true || plan.ownership.javascriptRuntime !== false) {
|
|
173
|
+
fail('Fastly native HTTP realization requires a provider-neutral plan with no JavaScript runtime ownership.', 'PULSE_FASTLY_NATIVE_HTTP_PLAN_OWNERSHIP_INVALID', { ownership: plan.ownership });
|
|
174
|
+
}
|
|
175
|
+
if ((plan.effects || []).length > 0 || (plan.continuations || []).length > 0 || ((plan.packages && plan.packages.effects) || []).length > 0) {
|
|
176
|
+
fail('Pass96 realizes only effect-free HTTP handlers; effects and continuations are introduced in Pass97.', 'PULSE_FASTLY_NATIVE_HTTP_EFFECTS_DEFERRED', {
|
|
177
|
+
effects: (plan.effects || []).map((entry) => entry.id),
|
|
178
|
+
continuations: (plan.continuations || []).map((entry) => entry.id),
|
|
179
|
+
packageEffects: ((plan.packages && plan.packages.effects) || []).map((entry) => entry.id)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if ((plan.locals || []).length > 0) {
|
|
183
|
+
fail('Pass96 realizes effect-free handlers without local bindings; local/effect execution is introduced in Pass97.', 'PULSE_FASTLY_NATIVE_HTTP_LOCALS_DEFERRED', { locals: (plan.locals || []).map((entry) => entry.id) });
|
|
184
|
+
}
|
|
185
|
+
if (!plan.entry || !Array.isArray(plan.entry.body)) {
|
|
186
|
+
fail('Fastly native HTTP realization requires a canonical handler entry body.', 'PULSE_FASTLY_NATIVE_HTTP_ENTRY_INVALID');
|
|
187
|
+
}
|
|
188
|
+
return plan;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createRenderer(plan) {
|
|
192
|
+
const responseFactories = [];
|
|
193
|
+
let factoryIndex = 0;
|
|
194
|
+
|
|
195
|
+
function renderString(expression) {
|
|
196
|
+
if (!expression || typeof expression !== 'object') fail('String expression is missing.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
197
|
+
if (expression.kind === 'literal') {
|
|
198
|
+
if (typeof expression.value !== 'string') fail('String expression requires a string literal.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
199
|
+
return quote(expression.value);
|
|
200
|
+
}
|
|
201
|
+
if (expression.kind === 'context-read') {
|
|
202
|
+
const key = (expression.path || []).join('.');
|
|
203
|
+
const mapping = {
|
|
204
|
+
'req.method': '__pulse_request_method',
|
|
205
|
+
'req.url': '__pulse_request_url',
|
|
206
|
+
'req.path': '__pulse_request_path'
|
|
207
|
+
};
|
|
208
|
+
if (!mapping[key]) fail(`Pass96 does not support context read ${key || '<root>'}.`, 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
209
|
+
return mapping[key];
|
|
210
|
+
}
|
|
211
|
+
if (expression.kind === 'intrinsic' && expression.name === 'request.header') {
|
|
212
|
+
const args = expression.arguments || [];
|
|
213
|
+
if (args.length !== 1 || args[0].kind !== 'literal' || typeof args[0].value !== 'string') {
|
|
214
|
+
fail('Pass96 request.header() requires one static string name.', 'PULSE_FASTLY_NATIVE_HTTP_HEADER_NAME_STATIC', { expression });
|
|
215
|
+
}
|
|
216
|
+
return `__pulse_request_header(${quote(args[0].value)})`;
|
|
217
|
+
}
|
|
218
|
+
if (expression.kind === 'template') {
|
|
219
|
+
const parts = (expression.parts || []).map((part) => part.kind === 'text' ? quote(part.value || '') : renderString(part.value));
|
|
220
|
+
return parts.length > 0 ? `(${parts.join(' + ')})` : '""';
|
|
221
|
+
}
|
|
222
|
+
if (expression.kind === 'binary' && expression.operator === '+') {
|
|
223
|
+
return `(${renderString(expression.left)} + ${renderString(expression.right)})`;
|
|
224
|
+
}
|
|
225
|
+
if (expression.kind === 'conditional') {
|
|
226
|
+
return `(${renderBoolean(expression.test)} ? ${renderString(expression.whenTrue)} : ${renderString(expression.whenFalse)})`;
|
|
227
|
+
}
|
|
228
|
+
fail(`Pass96 cannot lower string expression kind ${String(expression.kind)}.`, 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderNumber(expression) {
|
|
232
|
+
if (expression && expression.kind === 'literal' && typeof expression.value === 'number' && Number.isFinite(expression.value)) return String(expression.value);
|
|
233
|
+
fail('Pass96 numeric expressions must be finite static literals.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function renderScalar(expression) {
|
|
237
|
+
const kind = expression && expression.valueKind;
|
|
238
|
+
if (kind === 'boolean') return { type: 'boolean', source: renderBoolean(expression) };
|
|
239
|
+
if (kind === 'number') return { type: 'number', source: renderNumber(expression) };
|
|
240
|
+
return { type: 'string', source: renderString(expression) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderBoolean(expression) {
|
|
244
|
+
if (!expression || typeof expression !== 'object') fail('Boolean expression is missing.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
245
|
+
if (expression.kind === 'literal' && typeof expression.value === 'boolean') return expression.value ? 'true' : 'false';
|
|
246
|
+
if (expression.kind === 'unary' && expression.operator === '!') return `(!${renderBoolean(expression.value)})`;
|
|
247
|
+
if (expression.kind === 'binary') {
|
|
248
|
+
if (expression.operator === '&&' || expression.operator === '||') {
|
|
249
|
+
return `(${renderBoolean(expression.left)} ${expression.operator} ${renderBoolean(expression.right)})`;
|
|
250
|
+
}
|
|
251
|
+
if (['===', '!==', '==', '!=', '<', '<=', '>', '>='].includes(expression.operator)) {
|
|
252
|
+
const left = renderScalar(expression.left);
|
|
253
|
+
const right = renderScalar(expression.right);
|
|
254
|
+
if (left.type !== right.type) {
|
|
255
|
+
fail('Pass96 comparison operands must have the same static scalar kind.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression, left: left.type, right: right.type });
|
|
256
|
+
}
|
|
257
|
+
const operator = expression.operator === '===' ? '==' : expression.operator === '!==' ? '!=' : expression.operator;
|
|
258
|
+
return `(${left.source} ${operator} ${right.source})`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (expression.kind === 'conditional') {
|
|
262
|
+
return `(${renderBoolean(expression.test)} ? ${renderBoolean(expression.whenTrue)} : ${renderBoolean(expression.whenFalse)})`;
|
|
263
|
+
}
|
|
264
|
+
fail(`Pass96 cannot lower boolean expression kind ${String(expression.kind)}.`, 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function renderJson(expression) {
|
|
268
|
+
if (!expression || typeof expression !== 'object') fail('JSON expression is missing.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
269
|
+
if (expression.kind === 'literal') {
|
|
270
|
+
if (typeof expression.value === 'string') return `__pulse_json_string(${quote(expression.value)})`;
|
|
271
|
+
if (expression.value === null || typeof expression.value === 'boolean' || typeof expression.value === 'number') return quote(JSON.stringify(expression.value));
|
|
272
|
+
fail('Pass96 JSON literals must be JSON primitives.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
273
|
+
}
|
|
274
|
+
if (expression.kind === 'undefined') return '"null"';
|
|
275
|
+
if (expression.kind === 'context-read' || (expression.kind === 'intrinsic' && expression.name === 'request.header') || expression.kind === 'template') {
|
|
276
|
+
return `__pulse_json_string(${renderString(expression)})`;
|
|
277
|
+
}
|
|
278
|
+
if (expression.kind === 'object') {
|
|
279
|
+
const parts = [];
|
|
280
|
+
for (const entry of expression.entries || []) {
|
|
281
|
+
if (entry.kind !== 'property' || !entry.key || entry.key.kind !== 'literal' || typeof entry.key.value !== 'string') {
|
|
282
|
+
fail('Pass96 JSON objects require static property names and do not support spreads.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { entry });
|
|
283
|
+
}
|
|
284
|
+
parts.push(`${quote(JSON.stringify(entry.key.value))} + ":" + ${renderJson(entry.value)}`);
|
|
285
|
+
}
|
|
286
|
+
if (parts.length === 0) return '"{}"';
|
|
287
|
+
return `("{" + ${parts.join(' + "," + ')} + "}")`;
|
|
288
|
+
}
|
|
289
|
+
if (expression.kind === 'array') {
|
|
290
|
+
const items = (expression.items || []).map((item) => {
|
|
291
|
+
if (item && item.kind === 'spread') fail('Pass96 JSON arrays do not support spreads.', 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { item });
|
|
292
|
+
return renderJson(item);
|
|
293
|
+
});
|
|
294
|
+
if (items.length === 0) return '"[]"';
|
|
295
|
+
return `("[" + ${items.join(' + "," + ')} + "]")`;
|
|
296
|
+
}
|
|
297
|
+
if (expression.kind === 'conditional') return `(${renderBoolean(expression.test)} ? ${renderJson(expression.whenTrue)} : ${renderJson(expression.whenFalse)})`;
|
|
298
|
+
if (expression.kind === 'binary' && expression.valueKind === 'string') return `__pulse_json_string(${renderString(expression)})`;
|
|
299
|
+
fail(`Pass96 cannot lower JSON expression kind ${String(expression.kind)}.`, 'PULSE_FASTLY_NATIVE_HTTP_EXPRESSION_UNSUPPORTED', { expression });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function registerResponse(expression) {
|
|
303
|
+
if (!expression || expression.kind !== 'intrinsic') {
|
|
304
|
+
fail('Pass96 handler returns must use ctx.json() or ctx.text().', 'PULSE_FASTLY_NATIVE_HTTP_RETURN_UNSUPPORTED', { expression });
|
|
305
|
+
}
|
|
306
|
+
const args = expression.arguments || [];
|
|
307
|
+
let kind;
|
|
308
|
+
let body;
|
|
309
|
+
if (expression.name === 'response.json') {
|
|
310
|
+
kind = 'json';
|
|
311
|
+
if (args.length < 1 || args.length > 2) fail('ctx.json() must have one value and optional response options.', 'PULSE_FASTLY_NATIVE_HTTP_RETURN_UNSUPPORTED', { expression });
|
|
312
|
+
body = renderJson(args[0]);
|
|
313
|
+
} else if (expression.name === 'response.text') {
|
|
314
|
+
kind = 'text';
|
|
315
|
+
if (args.length < 1 || args.length > 2) fail('ctx.text() must have one value and optional response options.', 'PULSE_FASTLY_NATIVE_HTTP_RETURN_UNSUPPORTED', { expression });
|
|
316
|
+
body = renderString(args[0]);
|
|
317
|
+
} else {
|
|
318
|
+
fail(`Pass96 does not realize response intrinsic ${String(expression.name)}.`, 'PULSE_FASTLY_NATIVE_HTTP_RETURN_UNSUPPORTED', { expression });
|
|
319
|
+
}
|
|
320
|
+
const options = responseOptions(args[1], kind);
|
|
321
|
+
const index = factoryIndex++;
|
|
322
|
+
const name = `__pulse_response_${index}`;
|
|
323
|
+
const lines = [
|
|
324
|
+
`function ${name}(): __PulseResponse {`,
|
|
325
|
+
' const headers = new Array<__PulseHeader>()',
|
|
326
|
+
...options.headers.map(([headerName, value]) => ` headers.push(new __PulseHeader(${quote(headerName)}, ${quote(value)}))`),
|
|
327
|
+
` return new __PulseResponse(${options.status}, ${body}, headers)`,
|
|
328
|
+
'}'
|
|
329
|
+
];
|
|
330
|
+
responseFactories.push(lines.join('\n'));
|
|
331
|
+
return `${name}()`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function renderSequence(statements, depth = 1) {
|
|
335
|
+
const indent = ' '.repeat(depth);
|
|
336
|
+
const lines = [];
|
|
337
|
+
for (const statement of statements || []) {
|
|
338
|
+
if (statement.kind === 'return') {
|
|
339
|
+
lines.push(`${indent}return ${registerResponse(statement.value)}`);
|
|
340
|
+
} else if (statement.kind === 'if') {
|
|
341
|
+
lines.push(`${indent}if (${renderBoolean(statement.test)}) {`);
|
|
342
|
+
lines.push(...renderSequence(statement.then || [], depth + 1));
|
|
343
|
+
lines.push(`${indent}}`);
|
|
344
|
+
if ((statement.else || []).length > 0) {
|
|
345
|
+
lines.push(`${indent}else {`);
|
|
346
|
+
lines.push(...renderSequence(statement.else || [], depth + 1));
|
|
347
|
+
lines.push(`${indent}}`);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
fail(`Pass96 does not realize statement kind ${String(statement.kind)}.`, 'PULSE_FASTLY_NATIVE_HTTP_STATEMENT_UNSUPPORTED', { statement });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return lines;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const handlerLines = [
|
|
357
|
+
'function __pulse_handler(): __PulseResponse {',
|
|
358
|
+
...renderSequence(plan.entry.body || []),
|
|
359
|
+
' return __pulse_internal_error_response()',
|
|
360
|
+
'}'
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
return Object.freeze({ responseFactories: Object.freeze(responseFactories), handlerSource: handlerLines.join('\n') });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function generateFastlyNativeHttpAssemblyScript(plan, options = {}) {
|
|
367
|
+
validatePlanBoundary(plan);
|
|
368
|
+
const rendered = createRenderer(plan);
|
|
369
|
+
const source = [
|
|
370
|
+
'/* Generated by Pulse Pass96 Fastly native HTTP shell. */',
|
|
371
|
+
'export function __pulse_fastly_abort(message: string | null, fileName: string | null, line: u32, column: u32): void { unreachable() }',
|
|
372
|
+
'',
|
|
373
|
+
'@external("fastly_abi", "init") declare function fastly_abi_init(version: i64): i32',
|
|
374
|
+
'@external("fastly_http_req", "body_downstream_get") declare function fastly_http_req_body_downstream_get(requestOut: usize, bodyOut: usize): i32',
|
|
375
|
+
'@external("fastly_http_req", "method_get") declare function fastly_http_req_method_get(handle: i32, buffer: usize, bufferLength: i32, writtenOut: usize): i32',
|
|
376
|
+
'@external("fastly_http_req", "uri_get") declare function fastly_http_req_uri_get(handle: i32, buffer: usize, bufferLength: i32, writtenOut: usize): i32',
|
|
377
|
+
'@external("fastly_http_req", "header_value_get") declare function fastly_http_req_header_value_get(handle: i32, name: usize, nameLength: i32, value: usize, valueLength: i32, writtenOut: usize): i32',
|
|
378
|
+
'@external("fastly_http_resp", "new") declare function fastly_http_resp_new(handleOut: usize): i32',
|
|
379
|
+
'@external("fastly_http_resp", "header_append") declare function fastly_http_resp_header_append(handle: i32, name: usize, nameLength: i32, value: usize, valueLength: i32): i32',
|
|
380
|
+
'@external("fastly_http_resp", "status_set") declare function fastly_http_resp_status_set(handle: i32, status: i32): i32',
|
|
381
|
+
'@external("fastly_http_resp", "send_downstream") declare function fastly_http_resp_send_downstream(handle: i32, body: i32, streaming: i32): i32',
|
|
382
|
+
'@external("fastly_http_body", "new") declare function fastly_http_body_new(handleOut: usize): i32',
|
|
383
|
+
'@external("fastly_http_body", "write") declare function fastly_http_body_write(handle: i32, buffer: usize, bufferLength: i32, end: i32, writtenOut: usize): i32',
|
|
384
|
+
'',
|
|
385
|
+
`const PULSE_FASTLY_ABI_VERSION: i64 = ${FASTLY_NATIVE_HTTP_SHELL_ABI_VERSION}`,
|
|
386
|
+
`const PULSE_FASTLY_BUFFER_BYTES: i32 = ${FASTLY_NATIVE_HTTP_BUFFER_BYTES}`,
|
|
387
|
+
'const FASTLY_STATUS_OK: i32 = 0',
|
|
388
|
+
'const FASTLY_STATUS_NONE: i32 = 10',
|
|
389
|
+
`const PULSE_PLAN_HASH: string = ${quote(plan.planHash)}`,
|
|
390
|
+
'let __pulse_request_handle: i32 = 0',
|
|
391
|
+
'let __pulse_request_body_handle: i32 = 0',
|
|
392
|
+
'let __pulse_request_method: string = ""',
|
|
393
|
+
'let __pulse_request_url: string = ""',
|
|
394
|
+
'let __pulse_request_path: string = ""',
|
|
395
|
+
'let __pulse_last_error: i32 = 0',
|
|
396
|
+
'',
|
|
397
|
+
'class __PulseHeader {',
|
|
398
|
+
' constructor(public name: string, public value: string) {}',
|
|
399
|
+
'}',
|
|
400
|
+
'class __PulseResponse {',
|
|
401
|
+
' constructor(public status: i32, public body: string, public headers: Array<__PulseHeader>) {}',
|
|
402
|
+
'}',
|
|
403
|
+
'',
|
|
404
|
+
'function __pulse_out_i32(): StaticArray<i32> { return new StaticArray<i32>(1) }',
|
|
405
|
+
'function __pulse_out_value(out: StaticArray<i32>): i32 { return load<i32>(changetype<usize>(out)) }',
|
|
406
|
+
'function __pulse_utf8(value: string): ArrayBuffer { return String.UTF8.encode(value, false) }',
|
|
407
|
+
'function __pulse_decode(buffer: Uint8Array, length: i32): string { return String.UTF8.decodeUnsafe(buffer.dataStart, length, false) }',
|
|
408
|
+
'',
|
|
409
|
+
'function __pulse_read_request_string(kind: i32): string {',
|
|
410
|
+
' const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES)',
|
|
411
|
+
' const written = __pulse_out_i32()',
|
|
412
|
+
' let status = kind == 0',
|
|
413
|
+
' ? fastly_http_req_method_get(__pulse_request_handle, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written))',
|
|
414
|
+
' : fastly_http_req_uri_get(__pulse_request_handle, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written))',
|
|
415
|
+
' if (status != FASTLY_STATUS_OK) { __pulse_last_error = status; return "" }',
|
|
416
|
+
' return __pulse_decode(buffer, __pulse_out_value(written))',
|
|
417
|
+
'}',
|
|
418
|
+
'',
|
|
419
|
+
'function __pulse_request_header(name: string): string {',
|
|
420
|
+
' const nameBytes = __pulse_utf8(name)',
|
|
421
|
+
' const buffer = new Uint8Array(PULSE_FASTLY_BUFFER_BYTES)',
|
|
422
|
+
' const written = __pulse_out_i32()',
|
|
423
|
+
' const status = fastly_http_req_header_value_get(__pulse_request_handle, changetype<usize>(nameBytes), nameBytes.byteLength, buffer.dataStart, PULSE_FASTLY_BUFFER_BYTES, changetype<usize>(written))',
|
|
424
|
+
' if (status == FASTLY_STATUS_NONE) return ""',
|
|
425
|
+
' if (status != FASTLY_STATUS_OK) { __pulse_last_error = status; return "" }',
|
|
426
|
+
' return __pulse_decode(buffer, __pulse_out_value(written))',
|
|
427
|
+
'}',
|
|
428
|
+
'',
|
|
429
|
+
'function __pulse_path_from_uri(uri: string): string {',
|
|
430
|
+
' let start = 0',
|
|
431
|
+
' const scheme = uri.indexOf("://")',
|
|
432
|
+
' if (scheme >= 0) {',
|
|
433
|
+
' const slash = uri.indexOf("/", scheme + 3)',
|
|
434
|
+
' start = slash >= 0 ? slash : uri.length',
|
|
435
|
+
' }',
|
|
436
|
+
' let end = uri.length',
|
|
437
|
+
' const query = uri.indexOf("?", start)',
|
|
438
|
+
' if (query >= 0 && query < end) end = query',
|
|
439
|
+
' const fragment = uri.indexOf("#", start)',
|
|
440
|
+
' if (fragment >= 0 && fragment < end) end = fragment',
|
|
441
|
+
' if (start >= end) return "/"',
|
|
442
|
+
' return uri.substring(start, end)',
|
|
443
|
+
'}',
|
|
444
|
+
'',
|
|
445
|
+
'function __pulse_hex_digit(value: i32): string {',
|
|
446
|
+
' return String.fromCharCode(value < 10 ? 48 + value : 87 + value)',
|
|
447
|
+
'}',
|
|
448
|
+
'function __pulse_json_string(value: string): string {',
|
|
449
|
+
' let output = "\\\""',
|
|
450
|
+
' for (let index = 0; index < value.length; index += 1) {',
|
|
451
|
+
' const code = value.charCodeAt(index)',
|
|
452
|
+
' if (code == 34) output += "\\\\\\\""',
|
|
453
|
+
' else if (code == 92) output += "\\\\\\\\"',
|
|
454
|
+
' else if (code == 8) output += "\\\\b"',
|
|
455
|
+
' else if (code == 9) output += "\\\\t"',
|
|
456
|
+
' else if (code == 10) output += "\\\\n"',
|
|
457
|
+
' else if (code == 12) output += "\\\\f"',
|
|
458
|
+
' else if (code == 13) output += "\\\\r"',
|
|
459
|
+
' else if (code < 32) output += "\\\\u00" + __pulse_hex_digit((code >> 4) & 15) + __pulse_hex_digit(code & 15)',
|
|
460
|
+
' else output += String.fromCharCode(code)',
|
|
461
|
+
' }',
|
|
462
|
+
' return output + "\\\""',
|
|
463
|
+
'}',
|
|
464
|
+
'',
|
|
465
|
+
'function __pulse_internal_error_response(): __PulseResponse {',
|
|
466
|
+
' const headers = new Array<__PulseHeader>()',
|
|
467
|
+
' headers.push(new __PulseHeader("content-type", "text/plain; charset=utf-8"))',
|
|
468
|
+
' return new __PulseResponse(500, "Pulse handler did not return a response", headers)',
|
|
469
|
+
'}',
|
|
470
|
+
...rendered.responseFactories,
|
|
471
|
+
rendered.handlerSource,
|
|
472
|
+
'',
|
|
473
|
+
'function __pulse_append_header(responseHandle: i32, header: __PulseHeader): i32 {',
|
|
474
|
+
' const name = __pulse_utf8(header.name)',
|
|
475
|
+
' const value = __pulse_utf8(header.value)',
|
|
476
|
+
' return fastly_http_resp_header_append(responseHandle, changetype<usize>(name), name.byteLength, changetype<usize>(value), value.byteLength)',
|
|
477
|
+
'}',
|
|
478
|
+
'',
|
|
479
|
+
'function __pulse_write_body(bodyHandle: i32, value: string): i32 {',
|
|
480
|
+
' const bytes = __pulse_utf8(value)',
|
|
481
|
+
' let offset = 0',
|
|
482
|
+
' while (offset < bytes.byteLength) {',
|
|
483
|
+
' const written = __pulse_out_i32()',
|
|
484
|
+
' const status = fastly_http_body_write(bodyHandle, changetype<usize>(bytes) + offset, bytes.byteLength - offset, 0, changetype<usize>(written))',
|
|
485
|
+
' if (status != FASTLY_STATUS_OK) return status',
|
|
486
|
+
' const count = __pulse_out_value(written)',
|
|
487
|
+
' if (count <= 0) return 1',
|
|
488
|
+
' offset += count',
|
|
489
|
+
' }',
|
|
490
|
+
' return FASTLY_STATUS_OK',
|
|
491
|
+
'}',
|
|
492
|
+
'',
|
|
493
|
+
'function __pulse_send(response: __PulseResponse): i32 {',
|
|
494
|
+
' const responseOut = __pulse_out_i32()',
|
|
495
|
+
' let status = fastly_http_resp_new(changetype<usize>(responseOut))',
|
|
496
|
+
' if (status != FASTLY_STATUS_OK) return status',
|
|
497
|
+
' const responseHandle = __pulse_out_value(responseOut)',
|
|
498
|
+
' const bodyOut = __pulse_out_i32()',
|
|
499
|
+
' status = fastly_http_body_new(changetype<usize>(bodyOut))',
|
|
500
|
+
' if (status != FASTLY_STATUS_OK) return status',
|
|
501
|
+
' const bodyHandle = __pulse_out_value(bodyOut)',
|
|
502
|
+
' status = fastly_http_resp_status_set(responseHandle, response.status)',
|
|
503
|
+
' if (status != FASTLY_STATUS_OK) return status',
|
|
504
|
+
' for (let index = 0; index < response.headers.length; index += 1) {',
|
|
505
|
+
' status = __pulse_append_header(responseHandle, unchecked(response.headers[index]))',
|
|
506
|
+
' if (status != FASTLY_STATUS_OK) return status',
|
|
507
|
+
' }',
|
|
508
|
+
' status = __pulse_write_body(bodyHandle, response.body)',
|
|
509
|
+
' if (status != FASTLY_STATUS_OK) return status',
|
|
510
|
+
' return fastly_http_resp_send_downstream(responseHandle, bodyHandle, 0)',
|
|
511
|
+
'}',
|
|
512
|
+
'',
|
|
513
|
+
'export function pulse_fastly_plan_hash_ptr(): i32 { return changetype<i32>(PULSE_PLAN_HASH) }',
|
|
514
|
+
'export function pulse_fastly_plan_hash_length(): i32 { return PULSE_PLAN_HASH.length }',
|
|
515
|
+
'export function pulse_fastly_last_error(): i32 { return __pulse_last_error }',
|
|
516
|
+
'export function _start(): void {',
|
|
517
|
+
' __pulse_last_error = fastly_abi_init(PULSE_FASTLY_ABI_VERSION)',
|
|
518
|
+
' if (__pulse_last_error != FASTLY_STATUS_OK) return',
|
|
519
|
+
' const handles = new StaticArray<i32>(2)',
|
|
520
|
+
' __pulse_last_error = fastly_http_req_body_downstream_get(changetype<usize>(handles), changetype<usize>(handles) + 4)',
|
|
521
|
+
' if (__pulse_last_error != FASTLY_STATUS_OK) return',
|
|
522
|
+
' __pulse_request_handle = load<i32>(changetype<usize>(handles))',
|
|
523
|
+
' __pulse_request_body_handle = load<i32>(changetype<usize>(handles) + 4)',
|
|
524
|
+
' __pulse_request_method = __pulse_read_request_string(0)',
|
|
525
|
+
' if (__pulse_last_error != FASTLY_STATUS_OK) return',
|
|
526
|
+
' __pulse_request_url = __pulse_read_request_string(1)',
|
|
527
|
+
' if (__pulse_last_error != FASTLY_STATUS_OK) return',
|
|
528
|
+
' __pulse_request_path = __pulse_path_from_uri(__pulse_request_url)',
|
|
529
|
+
' __pulse_last_error = __pulse_send(__pulse_handler())',
|
|
530
|
+
'}',
|
|
531
|
+
''
|
|
532
|
+
].join('\n');
|
|
533
|
+
|
|
534
|
+
return Object.freeze({
|
|
535
|
+
version: FASTLY_NATIVE_HTTP_SHELL_GENERATOR_VERSION,
|
|
536
|
+
source,
|
|
537
|
+
sourceHash: sha256(source),
|
|
538
|
+
manifest: Object.freeze({
|
|
539
|
+
version: FASTLY_NATIVE_HTTP_SHELL_VERSION,
|
|
540
|
+
generatorVersion: FASTLY_NATIVE_HTTP_SHELL_GENERATOR_VERSION,
|
|
541
|
+
abiVersion: FASTLY_NATIVE_HTTP_SHELL_ABI_VERSION,
|
|
542
|
+
planVersion: plan.version,
|
|
543
|
+
planHash: plan.planHash,
|
|
544
|
+
sourceHash: sha256(source),
|
|
545
|
+
bufferBytes: FASTLY_NATIVE_HTTP_BUFFER_BYTES,
|
|
546
|
+
effectFree: true,
|
|
547
|
+
allowedImports: FASTLY_NATIVE_HTTP_IMPORTS,
|
|
548
|
+
requiredExports: Object.freeze(['_start', 'pulse_fastly_plan_hash_ptr', 'pulse_fastly_plan_hash_length', 'pulse_fastly_last_error']),
|
|
549
|
+
policy: Object.freeze({
|
|
550
|
+
nativeFastly: true,
|
|
551
|
+
provider: 'fastly',
|
|
552
|
+
providerNeutralInput: true,
|
|
553
|
+
javascriptRuntime: false,
|
|
554
|
+
jsComputeRuntime: false,
|
|
555
|
+
wasi: false,
|
|
556
|
+
effects: false,
|
|
557
|
+
continuations: false,
|
|
558
|
+
requestBody: false,
|
|
559
|
+
requestHeaders: 'named-read',
|
|
560
|
+
repeatedResponseHeaders: true
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function inspectFastlyNativeHttpWasm(input) {
|
|
567
|
+
const bytes = Buffer.isBuffer(input) ? input : fs.readFileSync(path.resolve(input));
|
|
568
|
+
if (!WebAssembly.validate(bytes)) fail('Generated Fastly native HTTP shell is not valid WebAssembly.', 'PULSE_FASTLY_NATIVE_HTTP_WASM_INVALID');
|
|
569
|
+
const module = new WebAssembly.Module(bytes);
|
|
570
|
+
const imports = WebAssembly.Module.imports(module).map((entry) => Object.freeze({ module: entry.module, name: entry.name, kind: entry.kind }));
|
|
571
|
+
const exports = WebAssembly.Module.exports(module).map((entry) => Object.freeze({ name: entry.name, kind: entry.kind }));
|
|
572
|
+
const unexpected = imports.filter((entry) => !FASTLY_NATIVE_HTTP_ALLOWED_IMPORTS.has(`${entry.module}:${entry.name}`));
|
|
573
|
+
if (unexpected.length > 0) fail('Generated Fastly native HTTP shell imports functions outside the Pass96 ABI.', 'PULSE_FASTLY_NATIVE_HTTP_IMPORT_UNSUPPORTED', { unexpected, imports });
|
|
574
|
+
const forbidden = imports.filter((entry) => /pulse_host|wasi|js[_-]?compute/i.test(`${entry.module}:${entry.name}`));
|
|
575
|
+
if (forbidden.length > 0) fail('Generated Fastly native HTTP shell imports a forbidden runtime.', 'PULSE_FASTLY_NATIVE_HTTP_RUNTIME_FORBIDDEN', { forbidden });
|
|
576
|
+
const imported = new Set(imports.map((entry) => `${entry.module}:${entry.name}`));
|
|
577
|
+
const missingImports = FASTLY_NATIVE_HTTP_CORE_IMPORTS.map(([moduleName, name]) => `${moduleName}:${name}`).filter((key) => !imported.has(key));
|
|
578
|
+
if (missingImports.length > 0) fail('Generated Fastly native HTTP shell is missing required host imports.', 'PULSE_FASTLY_NATIVE_HTTP_IMPORT_MISSING', { missingImports, imports });
|
|
579
|
+
const exportNames = new Set(exports.map((entry) => entry.name));
|
|
580
|
+
const requiredExports = ['_start', 'memory', 'pulse_fastly_plan_hash_ptr', 'pulse_fastly_plan_hash_length', 'pulse_fastly_last_error'];
|
|
581
|
+
const missingExports = requiredExports.filter((name) => !exportNames.has(name));
|
|
582
|
+
if (missingExports.length > 0) fail('Generated Fastly native HTTP shell is missing required exports.', 'PULSE_FASTLY_NATIVE_HTTP_EXPORT_MISSING', { missingExports, exports });
|
|
583
|
+
if (bytes.length > FASTLY_NATIVE_HTTP_MAX_WASM_BYTES) fail('Generated Fastly native HTTP shell exceeds the Pass96 size ceiling.', 'PULSE_FASTLY_NATIVE_HTTP_WASM_TOO_LARGE', { bytes: bytes.length, maxBytes: FASTLY_NATIVE_HTTP_MAX_WASM_BYTES });
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
valid: true,
|
|
586
|
+
bytes: bytes.length,
|
|
587
|
+
sha256: sha256(bytes),
|
|
588
|
+
magic: bytes.subarray(0, 8).toString('hex'),
|
|
589
|
+
imports: Object.freeze(imports),
|
|
590
|
+
exports: Object.freeze(exports),
|
|
591
|
+
importModules: Object.freeze([...new Set(imports.map((entry) => entry.module))].sort())
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function compileFastlyNativeHttpPlan(plan, options = {}) {
|
|
596
|
+
const generated = generateFastlyNativeHttpAssemblyScript(plan, options);
|
|
597
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
598
|
+
const compilerFallback = path.resolve(__dirname, '..', '..', '..', '..', 'wasm', 'packages', 'compiler');
|
|
599
|
+
const asc = resolveAsc(cwd) || resolveAsc(compilerFallback);
|
|
600
|
+
if (!asc) fail('AssemblyScript compiler dependency was not found for Fastly native HTTP realization.', 'PULSE_FASTLY_NATIVE_HTTP_ASC_MISSING', { cwd, compilerFallback });
|
|
601
|
+
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-fastly-native-http-'));
|
|
602
|
+
const requestedOutDir = options.outDir ? path.resolve(options.outDir) : undefined;
|
|
603
|
+
const outputDir = requestedOutDir || stagingDir;
|
|
604
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
605
|
+
const sourceFile = path.join(stagingDir, 'fastly-native-http.as.ts');
|
|
606
|
+
const wasmFile = path.join(outputDir, options.wasmFile || 'fastly-native-http.wasm');
|
|
607
|
+
const watFile = path.join(outputDir, options.watFile || 'fastly-native-http.wat');
|
|
608
|
+
try {
|
|
609
|
+
fs.writeFileSync(sourceFile, generated.source, 'utf8');
|
|
610
|
+
const args = [
|
|
611
|
+
asc.script,
|
|
612
|
+
path.basename(sourceFile),
|
|
613
|
+
'--outFile', wasmFile,
|
|
614
|
+
'--textFile', watFile,
|
|
615
|
+
'--runtime', 'stub',
|
|
616
|
+
'--noAssert',
|
|
617
|
+
'--optimize',
|
|
618
|
+
'--use', 'abort=fastly-native-http.as/__pulse_fastly_abort'
|
|
619
|
+
];
|
|
620
|
+
const startedAt = Date.now();
|
|
621
|
+
const result = spawnSync(asc.executable, args, {
|
|
622
|
+
cwd: stagingDir,
|
|
623
|
+
encoding: 'utf8',
|
|
624
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
625
|
+
timeout: Number(options.timeoutMs || 120000)
|
|
626
|
+
});
|
|
627
|
+
const durationMs = Date.now() - startedAt;
|
|
628
|
+
if (result.error || result.status !== 0 || !fs.existsSync(wasmFile)) {
|
|
629
|
+
fail('AssemblyScript failed to compile the Fastly native HTTP shell.', 'PULSE_FASTLY_NATIVE_HTTP_ASC_FAILED', {
|
|
630
|
+
status: result.status,
|
|
631
|
+
signal: result.signal,
|
|
632
|
+
error: result.error && result.error.message,
|
|
633
|
+
stdout: String(result.stdout || '').slice(-16000),
|
|
634
|
+
stderr: String(result.stderr || '').slice(-16000),
|
|
635
|
+
source: generated.source,
|
|
636
|
+
durationMs
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const wasm = fs.readFileSync(wasmFile);
|
|
640
|
+
const wat = fs.existsSync(watFile) ? fs.readFileSync(watFile, 'utf8') : '';
|
|
641
|
+
const inspection = inspectFastlyNativeHttpWasm(wasm);
|
|
642
|
+
const manifest = Object.freeze({
|
|
643
|
+
...generated.manifest,
|
|
644
|
+
compilerVersion: FASTLY_NATIVE_HTTP_SHELL_COMPILER_VERSION,
|
|
645
|
+
assemblyScript: Object.freeze({ package: 'assemblyscript', version: require(path.join(asc.packageRoot, 'package.json')).version }),
|
|
646
|
+
wasm: Object.freeze({ bytes: inspection.bytes, sha256: inspection.sha256, magic: inspection.magic }),
|
|
647
|
+
wat: Object.freeze({ bytes: Buffer.byteLength(wat), sha256: sha256(wat) }),
|
|
648
|
+
importModules: inspection.importModules,
|
|
649
|
+
imports: inspection.imports,
|
|
650
|
+
exports: inspection.exports
|
|
651
|
+
});
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
version: FASTLY_NATIVE_HTTP_SHELL_VERSION,
|
|
654
|
+
compilerVersion: FASTLY_NATIVE_HTTP_SHELL_COMPILER_VERSION,
|
|
655
|
+
plan,
|
|
656
|
+
generated,
|
|
657
|
+
source: generated.source,
|
|
658
|
+
sourceHash: generated.sourceHash,
|
|
659
|
+
wasm,
|
|
660
|
+
wat,
|
|
661
|
+
inspection,
|
|
662
|
+
manifest,
|
|
663
|
+
durationMs,
|
|
664
|
+
output: requestedOutDir ? Object.freeze({ outDir: outputDir, wasmFile, watFile }) : undefined
|
|
665
|
+
});
|
|
666
|
+
} finally {
|
|
667
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function writeFastlyNativeHttpModule(compiled, outDir, options = {}) {
|
|
672
|
+
if (!compiled || !Buffer.isBuffer(compiled.wasm) || !compiled.manifest || !compiled.plan) throw new TypeError('writeFastlyNativeHttpModule requires a compiled Pass96 module.');
|
|
673
|
+
const target = path.resolve(outDir);
|
|
674
|
+
fs.mkdirSync(target, { recursive: true });
|
|
675
|
+
const sourceFile = path.join(target, options.sourceFile || 'fastly-native-http.as.ts');
|
|
676
|
+
const wasmFile = path.join(target, options.wasmFile || 'fastly-native-http.wasm');
|
|
677
|
+
const watFile = path.join(target, options.watFile || 'fastly-native-http.wat');
|
|
678
|
+
const planFile = path.join(target, options.planFile || 'canonical-native-plan.json');
|
|
679
|
+
const manifestFile = path.join(target, options.manifestFile || 'fastly-native-http-manifest.json');
|
|
680
|
+
fs.writeFileSync(sourceFile, compiled.source, 'utf8');
|
|
681
|
+
fs.writeFileSync(wasmFile, compiled.wasm);
|
|
682
|
+
fs.writeFileSync(watFile, compiled.wat, 'utf8');
|
|
683
|
+
fs.writeFileSync(planFile, `${stableStringify(compiled.plan, 2)}\n`, 'utf8');
|
|
684
|
+
fs.writeFileSync(manifestFile, `${stableStringify(compiled.manifest, 2)}\n`, 'utf8');
|
|
685
|
+
return Object.freeze({ target, sourceFile, wasmFile, watFile, planFile, manifestFile, manifest: compiled.manifest });
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
module.exports = Object.freeze({
|
|
689
|
+
FASTLY_NATIVE_HTTP_SHELL_VERSION,
|
|
690
|
+
FASTLY_NATIVE_HTTP_SHELL_GENERATOR_VERSION,
|
|
691
|
+
FASTLY_NATIVE_HTTP_SHELL_COMPILER_VERSION,
|
|
692
|
+
FASTLY_NATIVE_HTTP_SHELL_ABI_VERSION,
|
|
693
|
+
FASTLY_NATIVE_HTTP_BUFFER_BYTES,
|
|
694
|
+
FASTLY_NATIVE_HTTP_MAX_WASM_BYTES,
|
|
695
|
+
FASTLY_NATIVE_HTTP_IMPORTS,
|
|
696
|
+
FASTLY_NATIVE_HTTP_CORE_IMPORTS,
|
|
697
|
+
FastlyNativeHttpShellError,
|
|
698
|
+
generateFastlyNativeHttpAssemblyScript,
|
|
699
|
+
compileFastlyNativeHttpPlan,
|
|
700
|
+
inspectFastlyNativeHttpWasm,
|
|
701
|
+
writeFastlyNativeHttpModule,
|
|
702
|
+
stableStringify
|
|
703
|
+
});
|