@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,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { fastlyJavascriptProviderError, isPulseRuntimeError } = require('./errors.js');
|
|
4
|
+
|
|
5
|
+
const FASTLY_JAVASCRIPT_GRIP_BROADCAST_VERSION = 'pulse.fastly-javascript-grip-broadcast.v1';
|
|
6
|
+
const DEFAULT_MAX_REQUEST_BYTES = 65536;
|
|
7
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
8
|
+
|
|
9
|
+
function positiveSafeInteger(value, fallback, field) {
|
|
10
|
+
if (value === undefined) return fallback;
|
|
11
|
+
const number = Number(value);
|
|
12
|
+
if (!Number.isSafeInteger(number) || number <= 0) {
|
|
13
|
+
throw new TypeError(`Pulse Fastly JavaScript GRIP ${field} must be a positive safe integer.`);
|
|
14
|
+
}
|
|
15
|
+
return number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeEndpoint(input) {
|
|
19
|
+
let endpoint;
|
|
20
|
+
try { endpoint = new URL(String(input || '')); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw fastlyJavascriptProviderError(
|
|
23
|
+
'PULSE_GRIP_BROADCAST_ENDPOINT_INVALID',
|
|
24
|
+
'Fastly JavaScript GRIP publish endpoint must be an absolute HTTP or HTTPS URL.',
|
|
25
|
+
{ operation: 'broadcast', configuration: 'bindings.grip.publishEndpoint' },
|
|
26
|
+
error
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) {
|
|
30
|
+
throw fastlyJavascriptProviderError(
|
|
31
|
+
'PULSE_GRIP_BROADCAST_ENDPOINT_INVALID',
|
|
32
|
+
'Fastly JavaScript GRIP publish endpoint must use HTTP or HTTPS.',
|
|
33
|
+
{ operation: 'broadcast', protocol: endpoint.protocol }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return endpoint;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeAuthentication(input) {
|
|
40
|
+
if (input === undefined) return undefined;
|
|
41
|
+
const scheme = input && String(input.scheme || 'bearer').toLowerCase();
|
|
42
|
+
const secretRef = input && typeof input.secretRef === 'string' ? input.secretRef.trim() : '';
|
|
43
|
+
if (scheme !== 'bearer') {
|
|
44
|
+
throw fastlyJavascriptProviderError(
|
|
45
|
+
'PULSE_GRIP_BROADCAST_AUTH_SCHEME_UNSUPPORTED',
|
|
46
|
+
'Fastly JavaScript GRIP supports bearer authentication only.',
|
|
47
|
+
{ operation: 'broadcast', scheme }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (!secretRef) {
|
|
51
|
+
throw fastlyJavascriptProviderError(
|
|
52
|
+
'PULSE_GRIP_BROADCAST_SECRET_REFERENCE_REQUIRED',
|
|
53
|
+
'Fastly JavaScript GRIP bearer authentication requires a secret reference.',
|
|
54
|
+
{ operation: 'broadcast', configuration: 'bindings.grip.authentication.secretRef' }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({ scheme, secretRef });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requestBody(payload) {
|
|
61
|
+
const channel = payload && typeof payload.channel === 'string' ? payload.channel.trim() : '';
|
|
62
|
+
if (!channel || /[,\r\n]/.test(channel)) {
|
|
63
|
+
throw fastlyJavascriptProviderError(
|
|
64
|
+
'PULSE_GRIP_BROADCAST_CHANNEL_INVALID',
|
|
65
|
+
'GRIP broadcast channel must be non-empty and contain no commas or line breaks.',
|
|
66
|
+
{ operation: 'broadcast' }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!payload || !Object.prototype.hasOwnProperty.call(payload, 'data') || payload.data === undefined) {
|
|
70
|
+
throw fastlyJavascriptProviderError(
|
|
71
|
+
'PULSE_GRIP_BROADCAST_MESSAGE_INVALID',
|
|
72
|
+
'GRIP broadcast requires a JSON-compatible data value.',
|
|
73
|
+
{ operation: 'broadcast' }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const body = JSON.stringify({
|
|
78
|
+
channel,
|
|
79
|
+
data: payload.data,
|
|
80
|
+
...(payload.event === undefined ? {} : { event: String(payload.event) }),
|
|
81
|
+
...(payload.id === undefined ? {} : { id: String(payload.id) })
|
|
82
|
+
});
|
|
83
|
+
if (typeof body !== 'string') throw new TypeError('not serializable');
|
|
84
|
+
return body;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw fastlyJavascriptProviderError(
|
|
87
|
+
'PULSE_GRIP_BROADCAST_MESSAGE_INVALID',
|
|
88
|
+
'GRIP broadcast message must be JSON-compatible.',
|
|
89
|
+
{ operation: 'broadcast' },
|
|
90
|
+
error
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readBoundedResponseText(response, maxBytes) {
|
|
96
|
+
const length = Number(response.headers && response.headers.get && response.headers.get('content-length'));
|
|
97
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
98
|
+
throw fastlyJavascriptProviderError(
|
|
99
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_TOO_LARGE',
|
|
100
|
+
'GRIP broadcast provider response exceeded the configured byte limit.',
|
|
101
|
+
{ operation: 'broadcast', maxResponseBytes: maxBytes }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (!response.body) return '';
|
|
105
|
+
const reader = response.body.getReader();
|
|
106
|
+
const decoder = new TextDecoder();
|
|
107
|
+
let bytes = 0;
|
|
108
|
+
let text = '';
|
|
109
|
+
try {
|
|
110
|
+
for (;;) {
|
|
111
|
+
const part = await reader.read();
|
|
112
|
+
if (part.done) break;
|
|
113
|
+
bytes += part.value.byteLength;
|
|
114
|
+
if (bytes > maxBytes) {
|
|
115
|
+
try { await reader.cancel(); } catch (_) { /* Best-effort host stream cleanup. */ }
|
|
116
|
+
throw fastlyJavascriptProviderError(
|
|
117
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_TOO_LARGE',
|
|
118
|
+
'GRIP broadcast provider response exceeded the configured byte limit.',
|
|
119
|
+
{ operation: 'broadcast', maxResponseBytes: maxBytes }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
text += decoder.decode(part.value, { stream: true });
|
|
123
|
+
}
|
|
124
|
+
return text + decoder.decode();
|
|
125
|
+
} finally {
|
|
126
|
+
reader.releaseLock();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeAck(status, text) {
|
|
131
|
+
if (!text.trim()) return Object.freeze({ accepted: true, status });
|
|
132
|
+
let parsed;
|
|
133
|
+
try { parsed = JSON.parse(text); }
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw fastlyJavascriptProviderError(
|
|
136
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_INVALID',
|
|
137
|
+
'GRIP broadcast provider returned an invalid acknowledgement.',
|
|
138
|
+
{ operation: 'broadcast', status },
|
|
139
|
+
error
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
143
|
+
throw fastlyJavascriptProviderError(
|
|
144
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_INVALID',
|
|
145
|
+
'GRIP broadcast provider acknowledgement must be an object.',
|
|
146
|
+
{ operation: 'broadcast', status }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
accepted: parsed.accepted !== false,
|
|
151
|
+
status,
|
|
152
|
+
...(parsed.messageId === undefined ? {} : { messageId: String(parsed.messageId) })
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createFastlyJavascriptGripBroadcast(options = {}) {
|
|
157
|
+
const grip = options.grip && typeof options.grip === 'object' ? options.grip : {};
|
|
158
|
+
const endpoint = normalizeEndpoint(grip.publishEndpoint || grip.publishUrl);
|
|
159
|
+
const authentication = normalizeAuthentication(grip.authentication);
|
|
160
|
+
const bindings = options.bindings && typeof options.bindings === 'object' ? options.bindings : {};
|
|
161
|
+
const backends = bindings.backends && typeof bindings.backends === 'object' ? bindings.backends : {};
|
|
162
|
+
const backend = typeof grip.publishBackend === 'string' && grip.publishBackend.trim()
|
|
163
|
+
? grip.publishBackend.trim()
|
|
164
|
+
: (typeof backends[endpoint.origin] === 'string' ? backends[endpoint.origin].trim() : '');
|
|
165
|
+
if (!backend && bindings.dynamicBackends !== true) {
|
|
166
|
+
throw fastlyJavascriptProviderError(
|
|
167
|
+
'PULSE_GRIP_BROADCAST_BACKEND_REQUIRED',
|
|
168
|
+
'Fastly JavaScript GRIP requires a named publish backend unless dynamic backends are enabled.',
|
|
169
|
+
{ operation: 'broadcast', origin: endpoint.origin, configuration: 'bindings.grip.publishBackend' }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (typeof options.fetchImplementation !== 'function') {
|
|
173
|
+
throw fastlyJavascriptProviderError(
|
|
174
|
+
'PULSE_GRIP_BROADCAST_CAPABILITY_REQUIRED',
|
|
175
|
+
'Fastly JavaScript GRIP requires the provider fetch implementation.',
|
|
176
|
+
{ operation: 'broadcast' }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const maxRequestBytes = positiveSafeInteger(grip.maxRequestBytes, DEFAULT_MAX_REQUEST_BYTES, 'maxRequestBytes');
|
|
180
|
+
const maxResponseBytes = positiveSafeInteger(grip.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES, 'maxResponseBytes');
|
|
181
|
+
|
|
182
|
+
return async function fastlyJavascriptGripBroadcast(payload, execution = {}) {
|
|
183
|
+
const body = requestBody(payload);
|
|
184
|
+
if (new TextEncoder().encode(body).byteLength > maxRequestBytes) {
|
|
185
|
+
throw fastlyJavascriptProviderError(
|
|
186
|
+
'PULSE_GRIP_BROADCAST_REQUEST_TOO_LARGE',
|
|
187
|
+
'GRIP broadcast request exceeded the configured byte limit.',
|
|
188
|
+
{ operation: 'broadcast', maxRequestBytes }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const headers = new Headers({
|
|
192
|
+
accept: 'application/json',
|
|
193
|
+
'content-type': 'application/json; charset=utf-8'
|
|
194
|
+
});
|
|
195
|
+
if (authentication) {
|
|
196
|
+
const secret = typeof options.secretLookup === 'function'
|
|
197
|
+
? await options.secretLookup(authentication.secretRef)
|
|
198
|
+
: undefined;
|
|
199
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
200
|
+
throw fastlyJavascriptProviderError(
|
|
201
|
+
'PULSE_GRIP_BROADCAST_SECRET_REQUIRED',
|
|
202
|
+
'GRIP broadcast authentication secret is unavailable.',
|
|
203
|
+
{ operation: 'broadcast', secretRef: authentication.secretRef }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (typeof execution.registerRedactionValue === 'function') execution.registerRedactionValue(secret);
|
|
207
|
+
headers.set('authorization', `Bearer ${secret}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let response;
|
|
211
|
+
let text;
|
|
212
|
+
try {
|
|
213
|
+
response = await options.fetchImplementation(endpoint.href, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers,
|
|
216
|
+
body,
|
|
217
|
+
signal: execution.signal,
|
|
218
|
+
redirect: 'error',
|
|
219
|
+
...(backend ? { backend } : {})
|
|
220
|
+
});
|
|
221
|
+
if (!(response instanceof Response)) {
|
|
222
|
+
throw fastlyJavascriptProviderError(
|
|
223
|
+
'PULSE_GRIP_BROADCAST_RESPONSE_INVALID',
|
|
224
|
+
'GRIP broadcast provider must return a Web Response.',
|
|
225
|
+
{ operation: 'broadcast' }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
text = await readBoundedResponseText(response, maxResponseBytes);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (isPulseRuntimeError(error)) throw error;
|
|
231
|
+
if (execution.signal && execution.signal.aborted) {
|
|
232
|
+
throw fastlyJavascriptProviderError(
|
|
233
|
+
'PULSE_GRIP_BROADCAST_ABORTED',
|
|
234
|
+
'GRIP broadcast was cancelled with its owning request.',
|
|
235
|
+
{ operation: 'broadcast' }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw fastlyJavascriptProviderError(
|
|
239
|
+
'PULSE_GRIP_BROADCAST_NETWORK',
|
|
240
|
+
'GRIP broadcast provider request failed.',
|
|
241
|
+
{ operation: 'broadcast' },
|
|
242
|
+
error
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (response.status < 200 || response.status > 299) {
|
|
246
|
+
throw fastlyJavascriptProviderError(
|
|
247
|
+
'PULSE_GRIP_BROADCAST_REJECTED',
|
|
248
|
+
'GRIP broadcast provider rejected the publication.',
|
|
249
|
+
{ operation: 'broadcast', status: response.status }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return normalizeAck(response.status, text);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = Object.freeze({
|
|
257
|
+
FASTLY_JAVASCRIPT_GRIP_BROADCAST_VERSION,
|
|
258
|
+
DEFAULT_MAX_REQUEST_BYTES,
|
|
259
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
260
|
+
createFastlyJavascriptGripBroadcast
|
|
261
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
normalizeJwtVerifyEffect
|
|
5
|
+
} = require('@pulse-compute/wasm-contracts/jwt/contracts');
|
|
6
|
+
const {
|
|
7
|
+
CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
8
|
+
CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
|
|
9
|
+
} = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
10
|
+
|
|
11
|
+
const FASTLY_JAVASCRIPT_JWT_VERIFIER_VERSION = 'pulse.fastly-javascript-jwt-verifier.v2';
|
|
12
|
+
const FASTLY_JAVASCRIPT_JWT_REALIZATIONS = Object.freeze({
|
|
13
|
+
HS256: Object.freeze({
|
|
14
|
+
algorithm: 'HS256',
|
|
15
|
+
realization: 'runtime-builtin',
|
|
16
|
+
implementation: CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
17
|
+
automaticFallback: false
|
|
18
|
+
}),
|
|
19
|
+
ES256: Object.freeze({
|
|
20
|
+
algorithm: 'ES256',
|
|
21
|
+
realization: 'runtime-builtin',
|
|
22
|
+
implementation: CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
23
|
+
automaticFallback: false
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
const FASTLY_JAVASCRIPT_JWT_REALIZATION =
|
|
27
|
+
FASTLY_JAVASCRIPT_JWT_REALIZATIONS.HS256;
|
|
28
|
+
|
|
29
|
+
let runtimeModules;
|
|
30
|
+
|
|
31
|
+
function loadRuntimeModules() {
|
|
32
|
+
if (!runtimeModules) {
|
|
33
|
+
runtimeModules = Promise.all([
|
|
34
|
+
import('@pulse-compute/jwt/provider'),
|
|
35
|
+
import('@pulse-compute/crypto')
|
|
36
|
+
]).then(([jwtProvider, cryptoProvider]) => Object.freeze({
|
|
37
|
+
jwtProvider,
|
|
38
|
+
cryptoProvider
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
return runtimeModules;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fastlyWallClock() {
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
unixEpochSeconds: Date.now() / 1000,
|
|
47
|
+
trusted: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertRequestActive(execution, jwtProvider) {
|
|
52
|
+
if (execution && execution.signal && execution.signal.aborted) {
|
|
53
|
+
throw jwtProvider.jwtError('PULSE_JWT_OPERATION_FAILED', {
|
|
54
|
+
category: 'request-cancelled',
|
|
55
|
+
automaticFallback: false
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertRuntimeBuiltin(cryptoProvider, jwtProvider) {
|
|
61
|
+
if (
|
|
62
|
+
!cryptoProvider
|
|
63
|
+
|| cryptoProvider.CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION !== CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION
|
|
64
|
+
|| !cryptoProvider.crypto
|
|
65
|
+
|| !cryptoProvider.crypto.mac
|
|
66
|
+
|| typeof cryptoProvider.crypto.mac.verify !== 'function'
|
|
67
|
+
|| !cryptoProvider.crypto.signature
|
|
68
|
+
|| typeof cryptoProvider.crypto.signature.verify !== 'function'
|
|
69
|
+
) {
|
|
70
|
+
throw jwtProvider.jwtError('PULSE_JWT_TARGET_UNSUPPORTED', {
|
|
71
|
+
category: 'crypto-realization',
|
|
72
|
+
realization: FASTLY_JAVASCRIPT_JWT_REALIZATION.realization,
|
|
73
|
+
implementation: FASTLY_JAVASCRIPT_JWT_REALIZATION.implementation,
|
|
74
|
+
automaticFallback: false
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return cryptoProvider.crypto;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createFastlyJavascriptJwtVerify(options = {}) {
|
|
81
|
+
const captureWallClock = options.captureWallClock || fastlyWallClock;
|
|
82
|
+
const secretLookup = options.secretLookup;
|
|
83
|
+
return async function verifyFastlyJavascriptJwt(effect, execution) {
|
|
84
|
+
const { jwtProvider, cryptoProvider } = await loadRuntimeModules();
|
|
85
|
+
assertRequestActive(execution, jwtProvider);
|
|
86
|
+
const input = normalizeJwtVerifyEffect(effect);
|
|
87
|
+
if (execution && typeof execution.registerRedactionValue === 'function') {
|
|
88
|
+
execution.registerRedactionValue(input.token);
|
|
89
|
+
}
|
|
90
|
+
assertRequestActive(execution, jwtProvider);
|
|
91
|
+
const selectedCrypto = assertRuntimeBuiltin(cryptoProvider, jwtProvider);
|
|
92
|
+
const host = {
|
|
93
|
+
async captureWallClock() {
|
|
94
|
+
assertRequestActive(execution, jwtProvider);
|
|
95
|
+
const captured = await captureWallClock();
|
|
96
|
+
assertRequestActive(execution, jwtProvider);
|
|
97
|
+
return captured;
|
|
98
|
+
},
|
|
99
|
+
resolveSecret: typeof secretLookup === 'function'
|
|
100
|
+
? async (binding) => {
|
|
101
|
+
assertRequestActive(execution, jwtProvider);
|
|
102
|
+
const secret = await secretLookup(binding, execution);
|
|
103
|
+
assertRequestActive(execution, jwtProvider);
|
|
104
|
+
return secret;
|
|
105
|
+
}
|
|
106
|
+
: undefined,
|
|
107
|
+
registerSensitiveValue(value) {
|
|
108
|
+
if (execution && typeof execution.registerRedactionValue === 'function') {
|
|
109
|
+
execution.registerRedactionValue(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (execution && typeof execution.validateSchemaValue === 'function') {
|
|
114
|
+
host.validateClaims = async (schemaId, claims, context) => {
|
|
115
|
+
assertRequestActive(execution, jwtProvider);
|
|
116
|
+
const result = await execution.validateSchemaValue(schemaId, claims, context);
|
|
117
|
+
assertRequestActive(execution, jwtProvider);
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const result = await jwtProvider.verifyJwtWithCrypto(
|
|
122
|
+
input,
|
|
123
|
+
Object.freeze(host),
|
|
124
|
+
selectedCrypto
|
|
125
|
+
);
|
|
126
|
+
assertRequestActive(execution, jwtProvider);
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = Object.freeze({
|
|
132
|
+
FASTLY_JAVASCRIPT_JWT_VERIFIER_VERSION,
|
|
133
|
+
FASTLY_JAVASCRIPT_JWT_REALIZATION,
|
|
134
|
+
FASTLY_JAVASCRIPT_JWT_REALIZATIONS,
|
|
135
|
+
createFastlyJavascriptJwtVerify,
|
|
136
|
+
fastlyWallClock
|
|
137
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
const { executeFastlyJavascriptApplication } = require('./runtime-host.js');
|
|
5
|
+
|
|
6
|
+
const FASTLY_JAVASCRIPT_LIFECYCLE_VERSION = 'pulse.fastly-javascript-lifecycle.v1';
|
|
7
|
+
|
|
8
|
+
function requestForInput(input) {
|
|
9
|
+
if (input instanceof Request) return input;
|
|
10
|
+
if (input && input.request instanceof Request) return input.request;
|
|
11
|
+
throw new TypeError('Pulse Fastly JavaScript lifecycle requires a FetchEvent or Web Request.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function safeFailureResponse() {
|
|
15
|
+
return new Response('Internal Server Error', {
|
|
16
|
+
status: 500,
|
|
17
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createFastlyJavascriptHandler(application, options = {}) {
|
|
22
|
+
const normalizedApplication = runtimeHost.normalizeApplication(application);
|
|
23
|
+
return async function pulseFastlyJavascriptHandler(input) {
|
|
24
|
+
try {
|
|
25
|
+
const request = requestForInput(input);
|
|
26
|
+
return await executeFastlyJavascriptApplication(normalizedApplication, request, {
|
|
27
|
+
...options,
|
|
28
|
+
signal: options.signal || request.signal
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (typeof options.onError === 'function') {
|
|
32
|
+
try { options.onError(error); } catch (_) { /* Lifecycle evidence is best effort. */ }
|
|
33
|
+
}
|
|
34
|
+
return safeFailureResponse();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function installFastlyJavascriptApplication(application, options = {}) {
|
|
40
|
+
const handler = createFastlyJavascriptHandler(application, options);
|
|
41
|
+
const addEventListener = options.addEventListener || globalThis.addEventListener;
|
|
42
|
+
if (typeof addEventListener !== 'function') {
|
|
43
|
+
throw new TypeError('Pulse Fastly JavaScript lifecycle requires addEventListener.');
|
|
44
|
+
}
|
|
45
|
+
addEventListener('fetch', (event) => event.respondWith(handler(event)));
|
|
46
|
+
return handler;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = Object.freeze({
|
|
50
|
+
FASTLY_JAVASCRIPT_LIFECYCLE_VERSION,
|
|
51
|
+
createFastlyJavascriptHandler,
|
|
52
|
+
installFastlyJavascriptApplication,
|
|
53
|
+
requestForInput,
|
|
54
|
+
safeFailureResponse
|
|
55
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const vm = require('node:vm');
|
|
5
|
+
const {
|
|
6
|
+
bundleFastlyJavascriptApplication
|
|
7
|
+
} = require('./source-package.js');
|
|
8
|
+
const {
|
|
9
|
+
assertFastlyJavascriptApplication
|
|
10
|
+
} = require('./runtime-host.js');
|
|
11
|
+
|
|
12
|
+
const FASTLY_JAVASCRIPT_LOCAL_LOADER_VERSION = 'pulse.fastly-javascript-local-loader.v1';
|
|
13
|
+
const LOCAL_EXTERNALS = Object.freeze({
|
|
14
|
+
'@pulse-compute/runtime': require('@pulse-compute/runtime'),
|
|
15
|
+
'@pulse-compute/runtime/host': require('@pulse-compute/runtime/host'),
|
|
16
|
+
'@pulse-compute/runtime/package': require('@pulse-compute/runtime/package')
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
class PulseFastlyJavascriptLocalLoaderError extends Error {
|
|
20
|
+
constructor(code, message, detail = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'PulseFastlyJavascriptLocalLoaderError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.detail = Object.freeze({ ...detail });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function moduleDefault(exports) {
|
|
29
|
+
return exports && Object.prototype.hasOwnProperty.call(exports, 'default')
|
|
30
|
+
? exports.default
|
|
31
|
+
: exports;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadFastlyJavascriptLocalApplication(plan, options = {}) {
|
|
35
|
+
if (!plan || plan.provider !== 'fastly' || plan.target !== 'javascript' || plan.loadable !== true) {
|
|
36
|
+
throw new PulseFastlyJavascriptLocalLoaderError(
|
|
37
|
+
'PULSE_FASTLY_JAVASCRIPT_LOCAL_PLAN_UNAVAILABLE',
|
|
38
|
+
'Fastly JavaScript provider emulation requires a loadable Fastly JavaScript application plan.',
|
|
39
|
+
{
|
|
40
|
+
provider: plan && plan.provider,
|
|
41
|
+
target: plan && plan.target,
|
|
42
|
+
blockers: plan && plan.blockers || [],
|
|
43
|
+
automaticFallback: false
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const projectRoot = path.resolve(options.projectRoot || options.workspaceRoot || '.');
|
|
49
|
+
const closure = bundleFastlyJavascriptApplication(
|
|
50
|
+
plan,
|
|
51
|
+
projectRoot,
|
|
52
|
+
options.schemaBundle,
|
|
53
|
+
{ format: 'cjs', externalRuntime: true }
|
|
54
|
+
);
|
|
55
|
+
const module = { exports: {} };
|
|
56
|
+
const externalRequire = (request) => {
|
|
57
|
+
if (Object.prototype.hasOwnProperty.call(LOCAL_EXTERNALS, request)) {
|
|
58
|
+
return LOCAL_EXTERNALS[request];
|
|
59
|
+
}
|
|
60
|
+
throw new PulseFastlyJavascriptLocalLoaderError(
|
|
61
|
+
'PULSE_FASTLY_JAVASCRIPT_LOCAL_EXTERNAL_UNAVAILABLE',
|
|
62
|
+
`Bundled Fastly JavaScript provider emulation cannot load external module ${String(request)}.`,
|
|
63
|
+
{ request: String(request), automaticFallback: false }
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const wrapper = new vm.Script(
|
|
69
|
+
`(function(module,exports,require,__filename,__dirname){\n${closure.source}\n})`,
|
|
70
|
+
{ filename: 'pulse-fastly-javascript-local-application.cjs' }
|
|
71
|
+
).runInThisContext();
|
|
72
|
+
wrapper(
|
|
73
|
+
module,
|
|
74
|
+
module.exports,
|
|
75
|
+
externalRequire,
|
|
76
|
+
path.join(projectRoot, 'pulse-fastly-javascript-local-application.cjs'),
|
|
77
|
+
projectRoot
|
|
78
|
+
);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof PulseFastlyJavascriptLocalLoaderError) throw error;
|
|
81
|
+
throw new PulseFastlyJavascriptLocalLoaderError(
|
|
82
|
+
error && error.code || 'PULSE_FASTLY_JAVASCRIPT_LOCAL_BUNDLE_LOAD_FAILED',
|
|
83
|
+
'The bundled Fastly JavaScript application could not be loaded for provider emulation.',
|
|
84
|
+
{
|
|
85
|
+
causeName: error && error.name,
|
|
86
|
+
causeCode: error && error.code,
|
|
87
|
+
planHash: plan.planHash,
|
|
88
|
+
graphHash: plan.graph && plan.graph.graphHash,
|
|
89
|
+
automaticFallback: false
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let application;
|
|
95
|
+
try {
|
|
96
|
+
application = assertFastlyJavascriptApplication(moduleDefault(module.exports));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new PulseFastlyJavascriptLocalLoaderError(
|
|
99
|
+
error && error.code || 'PULSE_FASTLY_JAVASCRIPT_APPLICATION_EXPORT_INVALID',
|
|
100
|
+
error && error.message || 'The bundled Fastly JavaScript application export is invalid.',
|
|
101
|
+
{
|
|
102
|
+
planHash: plan.planHash,
|
|
103
|
+
graphHash: plan.graph && plan.graph.graphHash,
|
|
104
|
+
automaticFallback: false
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
version: FASTLY_JAVASCRIPT_LOCAL_LOADER_VERSION,
|
|
111
|
+
mode: 'provider-emulation',
|
|
112
|
+
provider: 'fastly',
|
|
113
|
+
target: 'javascript',
|
|
114
|
+
providerReality: false,
|
|
115
|
+
realityRunner: 'fastly compute serve',
|
|
116
|
+
automaticFallback: false,
|
|
117
|
+
plan,
|
|
118
|
+
application,
|
|
119
|
+
schemaCodecs: module.exports && module.exports.schemaCodecs || null,
|
|
120
|
+
entry: plan.workspace.entry,
|
|
121
|
+
modulesLoaded: plan.graph.modules.length,
|
|
122
|
+
packagesLoaded: Object.freeze(plan.packages
|
|
123
|
+
.filter((entry) => entry.status !== 'unavailable')
|
|
124
|
+
.map((entry) => entry.packageName)
|
|
125
|
+
.sort()),
|
|
126
|
+
closure: Object.freeze({
|
|
127
|
+
format: closure.format,
|
|
128
|
+
sourceHash: closure.sourceHash,
|
|
129
|
+
inputs: closure.inputs
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = Object.freeze({
|
|
135
|
+
FASTLY_JAVASCRIPT_LOCAL_LOADER_VERSION,
|
|
136
|
+
PulseFastlyJavascriptLocalLoaderError,
|
|
137
|
+
loadFastlyJavascriptLocalApplication
|
|
138
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FASTLY_JAVASCRIPT_LOGGING_VERSION = 'pulse.fastly-javascript-logging.v1';
|
|
4
|
+
|
|
5
|
+
function createFastlyJavascriptLogEmitter(options = {}) {
|
|
6
|
+
const output = options.console || globalThis.console;
|
|
7
|
+
const methods = Object.freeze({
|
|
8
|
+
1: 'error',
|
|
9
|
+
2: 'warn',
|
|
10
|
+
3: 'info',
|
|
11
|
+
4: 'debug'
|
|
12
|
+
});
|
|
13
|
+
return function fastlyJavascriptLog(level, message, event) {
|
|
14
|
+
if (!output) return;
|
|
15
|
+
const method = methods[Number(level)] || 'log';
|
|
16
|
+
const write = typeof output[method] === 'function'
|
|
17
|
+
? output[method]
|
|
18
|
+
: (typeof output.log === 'function' ? output.log : undefined);
|
|
19
|
+
if (write) write.call(output, `[pulse][${event && event.name || method}] ${String(message)}`);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = Object.freeze({
|
|
24
|
+
FASTLY_JAVASCRIPT_LOGGING_VERSION,
|
|
25
|
+
createFastlyJavascriptLogEmitter
|
|
26
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { fastlyJavascriptProviderError } = require('./errors.js');
|
|
4
|
+
|
|
5
|
+
const FASTLY_JAVASCRIPT_PACKAGE_EFFECTS_VERSION = 'pulse.fastly-javascript-package-effects.v1';
|
|
6
|
+
|
|
7
|
+
function unavailable(effect = {}) {
|
|
8
|
+
throw fastlyJavascriptProviderError(
|
|
9
|
+
'PULSE_RUNTIME_CAPABILITY_UNAVAILABLE',
|
|
10
|
+
`Pulse capability ${effect.capability || effect.kind || 'package-effect'} is unavailable in the Fastly JavaScript realization.`,
|
|
11
|
+
{ package: effect.package, contractId: effect.contractId, operation: effect.operation }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createFastlyJavascriptPackageEffectCapabilities(options = {}) {
|
|
16
|
+
const assetsLookup = options.assetsLookup;
|
|
17
|
+
const gripBroadcast = options.gripBroadcast;
|
|
18
|
+
const jwtVerify = options.jwtVerify;
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
version: FASTLY_JAVASCRIPT_PACKAGE_EFFECTS_VERSION,
|
|
21
|
+
effect(effect, execution) {
|
|
22
|
+
if (effect && effect.contractId === 'pulse.assets' && effect.operation === 'lookup') {
|
|
23
|
+
if (typeof assetsLookup !== 'function') return unavailable(effect);
|
|
24
|
+
return assetsLookup(effect.payload, execution);
|
|
25
|
+
}
|
|
26
|
+
if (effect && effect.contractId === 'pulse.grip' && effect.operation === 'broadcast') {
|
|
27
|
+
if (typeof gripBroadcast !== 'function') {
|
|
28
|
+
throw fastlyJavascriptProviderError(
|
|
29
|
+
'PULSE_GRIP_BROADCAST_CAPABILITY_REQUIRED',
|
|
30
|
+
'GRIP broadcast requires a configured Fastly publish capability.',
|
|
31
|
+
{ package: effect.package, operation: 'broadcast', configuration: 'bindings.grip.publishEndpoint' }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return gripBroadcast(effect.payload, execution);
|
|
35
|
+
}
|
|
36
|
+
if (effect && effect.contractId === 'pulse.jwt' && effect.operation === 'verify') {
|
|
37
|
+
if (typeof jwtVerify !== 'function') return unavailable(effect);
|
|
38
|
+
return jwtVerify(effect, execution);
|
|
39
|
+
}
|
|
40
|
+
return unavailable(effect);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function withFastlyPackageEffectCapabilities(capabilities, options = {}) {
|
|
46
|
+
const base = capabilities && typeof capabilities === 'object' ? capabilities : {};
|
|
47
|
+
const configured = createFastlyJavascriptPackageEffectCapabilities(options);
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
...base,
|
|
50
|
+
effect(effect, execution) {
|
|
51
|
+
if (effect && (
|
|
52
|
+
effect.contractId === 'pulse.assets'
|
|
53
|
+
|| effect.contractId === 'pulse.grip'
|
|
54
|
+
|| effect.contractId === 'pulse.jwt'
|
|
55
|
+
)) {
|
|
56
|
+
return configured.effect(effect, execution);
|
|
57
|
+
}
|
|
58
|
+
if (typeof base.effect === 'function') return base.effect(effect, execution);
|
|
59
|
+
return unavailable(effect);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = Object.freeze({
|
|
65
|
+
FASTLY_JAVASCRIPT_PACKAGE_EFFECTS_VERSION,
|
|
66
|
+
createFastlyJavascriptPackageEffectCapabilities,
|
|
67
|
+
withFastlyPackageEffectCapabilities
|
|
68
|
+
});
|