@pulse-compute/provider-fastly 0.0.0 → 1.0.0-beta.10
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 +53 -1
- package/dist/config-schema.json +103 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -5
- package/src/build/canonical-target.js +469 -0
- package/src/build/digest-native.js +25 -0
- package/src/build/effect-invocations.js +49 -0
- package/src/build/embedded-assets.as.ts +32 -0
- package/src/build/entities-native.js +559 -0
- package/src/build/fixed-memory-runtime.as.ts +148 -0
- package/src/build/kv-native.as.ts +331 -0
- package/src/build/kv-native.js +27 -0
- package/src/build/native-application-errors.js +159 -0
- package/src/build/native-binary-resources.js +30 -0
- package/src/build/native-body-forwarding.as.ts +341 -0
- package/src/build/native-body-forwarding.js +69 -0
- package/src/build/native-dynamic-backends.as.ts +115 -0
- package/src/build/native-dynamic-backends.js +52 -0
- package/src/build/native-fetch-deadlines.as.ts +164 -0
- package/src/build/native-fetch-deadlines.js +27 -0
- package/src/build/native-fetch-errors.js +38 -0
- package/src/build/native-handle-ownership.js +127 -0
- package/src/build/native-header-lookup.js +50 -0
- package/src/build/native-http-effects.js +994 -0
- package/src/build/native-http-shell.js +703 -0
- package/src/build/native-platform-capabilities.js +2654 -0
- package/src/build/native-request-body.js +44 -0
- package/src/build/native-request-headers.js +93 -0
- package/src/build/native-send-error-detail.as.ts +12 -0
- package/src/build/native-stdio.js +45 -0
- package/src/build/native-string-values.js +143 -0
- package/src/build/native-transport-details.as.ts +84 -0
- package/src/build/native-value-budget.js +90 -0
- package/src/build/package-native-application.js +114 -0
- package/src/build/request-budget.as.ts +50 -0
- package/src/build/request-budget.js +47 -0
- package/src/build/s3-body.as.ts +97 -0
- package/src/build/s3-native.as.ts +228 -0
- package/src/build/s3-native.js +30 -0
- package/src/build/schema-admission.js +83 -0
- package/src/build/schema-nested-json.js +93 -0
- package/src/build/schema-scalar-record.js +41 -0
- package/src/build/time-native.js +49 -0
- package/src/build/wasm-budget.js +28 -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 +69 -0
- package/src/config-schema.d.ts +13 -0
- package/src/config-schema.js +27 -0
- package/src/config-schema.json +103 -0
- package/src/index.js +3 -0
- package/src/index.ts +143 -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 +147 -0
- package/src/javascript/lifecycle.js +55 -0
- package/src/javascript/local-loader.js +138 -0
- package/src/javascript/logging.js +35 -0
- package/src/javascript/package-effects.js +71 -0
- package/src/javascript/runtime-host.js +112 -0
- package/src/javascript/sha256.js +128 -0
- package/src/javascript/source-package.js +778 -0
- package/src/javascript/support.js +142 -0
- package/src/javascript/target-support-policy.js +277 -0
- package/src/javascript/target.js +89 -0
- package/src/javascript/test-runtime.js +408 -0
- package/src/provider-contract.js +88 -0
- package/src/runtime/assets-provider.js +464 -0
- package/src/runtime/canonical-api-runtime.js +603 -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/conditional-kv-host.js +155 -0
- package/src/testing/fastly-cli.js +538 -0
- package/src/testing/native-dynamic-backends-host.js +39 -0
- package/src/testing/native-http-effects-host.js +398 -0
- package/src/testing/native-http-shell-host.js +190 -0
- package/src/testing/native-platform-capabilities-host.js +924 -0
- package/src/toolchain/body-forwarding.js +25 -0
- package/src/toolchain/config.js +144 -0
- package/src/toolchain/index.js +386 -0
- package/src/toolchain/logging.js +18 -0
- package/src/toolchain/native-execution.js +78 -0
- package/src/toolchain/s3.js +26 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { normalizeFastlyMaxWasmBytes, assertFastlyWasmBudget } = require('./wasm-budget.js');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { CANONICAL_NATIVE_PLAN_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
|
|
8
|
+
const {
|
|
9
|
+
FASTLY_NATIVE_PLATFORM_CAPABILITIES_VERSION,
|
|
10
|
+
compileFastlyNativePlatformCapabilitiesPlan,
|
|
11
|
+
resolveCapabilityBindings,
|
|
12
|
+
writeFastlyNativePlatformCapabilitiesModule
|
|
13
|
+
} = require('./native-platform-capabilities.js');
|
|
14
|
+
|
|
15
|
+
const FASTLY_CANONICAL_BUILD_VERSION = 'pulse.fastly-canonical-build.v6';
|
|
16
|
+
const FASTLY_CANONICAL_TARGET = 'fastly-compute-native';
|
|
17
|
+
|
|
18
|
+
function stableJson(value) { return `${JSON.stringify(value, null, 2)}\n`; }
|
|
19
|
+
function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
|
|
20
|
+
|
|
21
|
+
function isFastlyNativeArtifact(native) {
|
|
22
|
+
return Boolean(
|
|
23
|
+
native
|
|
24
|
+
&& native.version === FASTLY_NATIVE_PLATFORM_CAPABILITIES_VERSION
|
|
25
|
+
&& Buffer.isBuffer(native.wasm)
|
|
26
|
+
&& native.manifest
|
|
27
|
+
&& native.manifest.wasm
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function providerBuildError(code, message, detail = {}) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error.name = 'FastlyCanonicalBuildError';
|
|
34
|
+
error.code = code;
|
|
35
|
+
error.detail = Object.freeze({ ...detail });
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertFastlyProviderPlan(providerPlan) {
|
|
40
|
+
if (
|
|
41
|
+
!providerPlan
|
|
42
|
+
|| providerPlan.version !== 'pulse.canonical-provider-plan.v1'
|
|
43
|
+
|| providerPlan.provider !== 'fastly'
|
|
44
|
+
|| !Array.isArray(providerPlan.operations)
|
|
45
|
+
) {
|
|
46
|
+
throw new TypeError('Fastly canonical target requires the canonical Fastly provider plan.');
|
|
47
|
+
}
|
|
48
|
+
return providerPlan;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function metadataFromCanonicalPlans(plan, providerPlan) {
|
|
52
|
+
const lowering = assertFastlyProviderPlan(providerPlan);
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
sourceHash: plan.source.sourceHash,
|
|
55
|
+
projectSourceHash: plan.source.projectSourceHash || plan.source.sourceHash,
|
|
56
|
+
capabilities: plan.capabilities,
|
|
57
|
+
providerOperations: lowering.operations,
|
|
58
|
+
packageEffects: plan.packages && plan.packages.effects || Object.freeze([]),
|
|
59
|
+
schemaIds: plan.schemas && plan.schemas.ids || Object.freeze([]),
|
|
60
|
+
schemaReferences: plan.schemas && plan.schemas.references || Object.freeze([]),
|
|
61
|
+
schemaSourceHash: plan.schemas && plan.schemas.sourceHash
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function assertExactProviderNative(native) {
|
|
66
|
+
if (!isFastlyNativeArtifact(native)) {
|
|
67
|
+
throw providerBuildError(
|
|
68
|
+
'PULSE_FASTLY_NATIVE_ARTIFACT_INVALID',
|
|
69
|
+
'Fastly packaging requires a Fastly provider-native artifact.',
|
|
70
|
+
{ automaticFallback: false }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const observedSha256 = sha256(native.wasm);
|
|
74
|
+
if (native.manifest.wasm.sha256 !== observedSha256) {
|
|
75
|
+
throw providerBuildError(
|
|
76
|
+
'PULSE_FASTLY_NATIVE_ARTIFACT_HASH_MISMATCH',
|
|
77
|
+
'Fastly provider-native bytes do not match their compiler manifest.',
|
|
78
|
+
{
|
|
79
|
+
expectedSha256: native.manifest.wasm.sha256,
|
|
80
|
+
observedSha256,
|
|
81
|
+
automaticFallback: false
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (native.guestLink) {
|
|
86
|
+
const audit = native.guestLink.audit;
|
|
87
|
+
if (
|
|
88
|
+
!audit
|
|
89
|
+
|| audit.status !== 'passed'
|
|
90
|
+
|| audit.providerPackaging && audit.providerPackaging.authorized !== true
|
|
91
|
+
|| !audit.artifact
|
|
92
|
+
|| audit.artifact.sha256 !== observedSha256
|
|
93
|
+
|| native.guestLink.providerPackaging
|
|
94
|
+
&& native.guestLink.providerPackaging.authorized !== true
|
|
95
|
+
) {
|
|
96
|
+
throw providerBuildError(
|
|
97
|
+
'PULSE_GUEST_FINAL_AUDIT_FAILED',
|
|
98
|
+
'Fastly packaging requires exact bytes authorized by the final guest-link audit.',
|
|
99
|
+
{ observedSha256, automaticFallback: false }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return native;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateGripBindings(metadata = {}, bindings = {}) {
|
|
107
|
+
const operations = (metadata.providerOperations || []).filter((operation) => operation && operation.kind === 'grip');
|
|
108
|
+
if (operations.length === 0) return Object.freeze({ active: false, operations: Object.freeze([]) });
|
|
109
|
+
const grip = bindings.grip && typeof bindings.grip === 'object' ? bindings.grip : {};
|
|
110
|
+
const publish = operations.filter((operation) => operation.operation === 'publish');
|
|
111
|
+
const broadcast = operations.filter((operation) => operation.operation === 'broadcast');
|
|
112
|
+
const hold = operations.filter((operation) => operation.operation === 'hold');
|
|
113
|
+
if (broadcast.length > 0 && !String(grip.publishEndpoint || '').trim()) {
|
|
114
|
+
throw providerBuildError(
|
|
115
|
+
'PULSE_FASTLY_GRIP_PUBLISH_BINDING_REQUIRED',
|
|
116
|
+
'Fastly GRIP broadcast operations require provider.grip.publishEndpoint.',
|
|
117
|
+
{ operations: broadcast.map((operation) => operation.id) }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (broadcast.length > 0) {
|
|
121
|
+
try {
|
|
122
|
+
const endpoint = new URL(String(grip.publishEndpoint));
|
|
123
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) throw new TypeError('unsupported protocol');
|
|
124
|
+
} catch {
|
|
125
|
+
throw providerBuildError(
|
|
126
|
+
'PULSE_FASTLY_GRIP_PUBLISH_BINDING_REQUIRED',
|
|
127
|
+
'Fastly GRIP publishEndpoint must be an absolute HTTP or HTTPS URL.',
|
|
128
|
+
{ operations: broadcast.map((operation) => operation.id) }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (grip.authentication !== undefined) {
|
|
132
|
+
const authentication = grip.authentication && typeof grip.authentication === 'object' ? grip.authentication : {};
|
|
133
|
+
if (String(authentication.scheme || 'bearer').toLowerCase() !== 'bearer') {
|
|
134
|
+
throw providerBuildError(
|
|
135
|
+
'PULSE_FASTLY_NATIVE_GRIP_AUTH_SCHEME_UNSUPPORTED',
|
|
136
|
+
'Fastly GRIP broadcast supports only bearer authentication.',
|
|
137
|
+
{ operations: broadcast.map((operation) => operation.id) }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!String(authentication.secretRef || '').trim()) {
|
|
141
|
+
throw providerBuildError(
|
|
142
|
+
'PULSE_FASTLY_NATIVE_GRIP_SECRET_REFERENCE_MISSING',
|
|
143
|
+
'Fastly GRIP bearer authentication requires a named secret reference.',
|
|
144
|
+
{ operations: broadcast.map((operation) => operation.id) }
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (publish.length > 0 && !String(grip.publishUrl || '').trim()) {
|
|
150
|
+
throw providerBuildError(
|
|
151
|
+
'PULSE_FASTLY_GRIP_PUBLISH_BINDING_REQUIRED',
|
|
152
|
+
'Fastly GRIP publish operations require provider.grip.publishUrl.',
|
|
153
|
+
{ operations: publish.map((operation) => operation.id) }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (hold.length > 0 && grip.directHold === false && !String(grip.fanoutBackend || '').trim()) {
|
|
157
|
+
throw providerBuildError(
|
|
158
|
+
'PULSE_FASTLY_GRIP_FANOUT_BACKEND_REQUIRED',
|
|
159
|
+
'Fastly GRIP Fanout handoff requires provider.grip.fanoutBackend when directHold is disabled.',
|
|
160
|
+
{ operations: hold.map((operation) => operation.id) }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const endpoint = broadcast.length > 0 ? grip.publishEndpoint : grip.publishUrl;
|
|
164
|
+
if ((publish.length > 0 || broadcast.length > 0) && String(endpoint || '').trim() && !String(grip.publishBackend || '').trim() && bindings.dynamicBackends !== true) {
|
|
165
|
+
const origin = new URL(String(endpoint)).origin;
|
|
166
|
+
if (!(bindings.backends && bindings.backends[origin])) {
|
|
167
|
+
throw providerBuildError(
|
|
168
|
+
'PULSE_FASTLY_GRIP_PUBLISH_BACKEND_REQUIRED',
|
|
169
|
+
`No Fastly backend binding is configured for the GRIP publish origin ${origin}.`,
|
|
170
|
+
{ origin, operations: [...publish, ...broadcast].map((operation) => operation.id) }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
active: true,
|
|
176
|
+
operations: Object.freeze(operations.map((operation) => Object.freeze({ id: operation.id, operation: operation.operation, result: operation.result }))),
|
|
177
|
+
directHold: grip.directHold !== false,
|
|
178
|
+
fanoutBackend: grip.fanoutBackend === undefined ? undefined : String(grip.fanoutBackend),
|
|
179
|
+
publishEndpoint: grip.publishEndpoint === undefined ? undefined : String(grip.publishEndpoint),
|
|
180
|
+
publishUrl: grip.publishUrl === undefined ? undefined : String(grip.publishUrl),
|
|
181
|
+
publishBackend: grip.publishBackend === undefined ? undefined : String(grip.publishBackend),
|
|
182
|
+
authentication: grip.authentication === undefined ? undefined : Object.freeze({
|
|
183
|
+
scheme: String(grip.authentication.scheme || 'bearer'),
|
|
184
|
+
secretRef: String(grip.authentication.secretRef || '')
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function cleanName(value) { return String(value || 'pulse-app').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'pulse-app'; }
|
|
190
|
+
function tomlString(value) { return JSON.stringify(String(value)); }
|
|
191
|
+
|
|
192
|
+
function normalizedBuildConfig(providerConfig = {}) {
|
|
193
|
+
const input = providerConfig.build && typeof providerConfig.build === 'object' ? providerConfig.build : {};
|
|
194
|
+
return Object.freeze({
|
|
195
|
+
name: cleanName(input.name),
|
|
196
|
+
description: String(input.description || 'Pulse application'),
|
|
197
|
+
authors: Object.freeze(Array.isArray(input.authors) ? input.authors.map(String) : []),
|
|
198
|
+
language: 'other'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function nativeBuildCheckCommand() {
|
|
203
|
+
return "node -e \"const fs=require('node:fs');const f='bin/main.wasm';if(!fs.existsSync(f)){console.error('Run pulse build before Fastly packaging.');process.exit(1)}\"";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderFastlyToml(build) {
|
|
207
|
+
return [
|
|
208
|
+
'manifest_version = 3',
|
|
209
|
+
`name = ${tomlString(build.name)}`,
|
|
210
|
+
`description = ${tomlString(build.description)}`,
|
|
211
|
+
`authors = [${build.authors.map(tomlString).join(', ')}]`,
|
|
212
|
+
'language = "other"',
|
|
213
|
+
'',
|
|
214
|
+
'[scripts]',
|
|
215
|
+
`build = ${tomlString(nativeBuildCheckCommand())}`,
|
|
216
|
+
''
|
|
217
|
+
].join('\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function inspectFastlyCanonicalTarget(options = {}) {
|
|
221
|
+
const plan = options.plan;
|
|
222
|
+
if (!plan || plan.version !== CANONICAL_NATIVE_PLAN_VERSION) {
|
|
223
|
+
throw new TypeError(`Fastly canonical target inspection requires ${CANONICAL_NATIVE_PLAN_VERSION}.`);
|
|
224
|
+
}
|
|
225
|
+
const providerConfig = options.providerConfig || {};
|
|
226
|
+
const bindings = options.bindings || providerConfig.bindings || {};
|
|
227
|
+
const maxWasmBytes = normalizeFastlyMaxWasmBytes(providerConfig.build?.maxWasmBytes);
|
|
228
|
+
if (isFastlyNativeArtifact(options.native)) assertFastlyWasmBudget(options.native.wasm.length, maxWasmBytes);
|
|
229
|
+
const native = isFastlyNativeArtifact(options.native)
|
|
230
|
+
? assertExactProviderNative(options.native)
|
|
231
|
+
: compileFastlyNativePlatformCapabilitiesPlan(plan, {
|
|
232
|
+
cwd: options.cwd || options.projectRoot || process.cwd(),
|
|
233
|
+
projectRoot: options.projectRoot,
|
|
234
|
+
profile: options.profile,
|
|
235
|
+
bindings,
|
|
236
|
+
maxDurationMs: providerConfig.maxDurationMs,
|
|
237
|
+
bodyForwarding: providerConfig.bodyForwarding,
|
|
238
|
+
maxWasmBytes,
|
|
239
|
+
requirePlatformCapability: false,
|
|
240
|
+
canonicalBuild: true,
|
|
241
|
+
targetDescriptor: options.targetDescriptor,
|
|
242
|
+
synchronizedPackages: options.synchronizedPackages,
|
|
243
|
+
realizationArtifacts: options.realizationArtifacts
|
|
244
|
+
|| (options.native && options.native.realizationArtifacts),
|
|
245
|
+
guestUnits: options.guestUnits
|
|
246
|
+
|| (options.native && options.native.guestUnits),
|
|
247
|
+
nativeOptimization: options.nativeOptimization
|
|
248
|
+
|| (options.experimentalNativeSize === true ? 'experimental-native-size' : undefined),
|
|
249
|
+
elideJsonCacheArena: options.elideJsonCacheArena,
|
|
250
|
+
emitWat: options.emitWat,
|
|
251
|
+
timeoutMs: options.compileTimeoutMs
|
|
252
|
+
});
|
|
253
|
+
if (native.manifest.bindings?.logEndpoint !== require('../toolchain/logging.js').normalizeFastlyLogEndpoint(bindings.logEndpoint)) {
|
|
254
|
+
throw providerBuildError('PULSE_FASTLY_NATIVE_ARTIFACT_INVALID', 'Fastly artifact logging destination does not match the selected provider profile; rebuild the Native artifact.');
|
|
255
|
+
}
|
|
256
|
+
const duration = require('@pulse-compute/runtime/host').normalizeRequestDuration(providerConfig.maxDurationMs);
|
|
257
|
+
if (native.manifest.bindings?.maxDurationMs !== duration) {
|
|
258
|
+
throw providerBuildError('PULSE_REQUEST_DURATION_ARTIFACT_MISMATCH', 'Fastly artifact request deadline does not match the selected provider profile.');
|
|
259
|
+
}
|
|
260
|
+
const forwarding = require('../toolchain/body-forwarding.js').normalizeBodyForwarding(providerConfig.bodyForwarding, duration);
|
|
261
|
+
if (JSON.stringify(native.manifest.bindings?.bodyForwarding) !== JSON.stringify(forwarding)) {
|
|
262
|
+
throw providerBuildError('PULSE_FASTLY_BODY_FORWARDING_ARTIFACT_MISMATCH', 'Fastly artifact forwarding bounds do not match the selected provider profile.');
|
|
263
|
+
}
|
|
264
|
+
if (native.manifest.dynamicBackends || (isFastlyNativeArtifact(options.native) && bindings.dynamicBackends === true)) {
|
|
265
|
+
const dynamic = require('./native-dynamic-backends.js');
|
|
266
|
+
const expected = resolveCapabilityBindings(plan, { bindings, maxDurationMs: duration, bodyForwarding: forwarding });
|
|
267
|
+
const expectedPolicy = dynamic.active(expected) ? dynamic.describe(bindings.backends) : undefined;
|
|
268
|
+
if (JSON.stringify(native.manifest.dynamicBackends) !== JSON.stringify(expectedPolicy)
|
|
269
|
+
|| JSON.stringify(native.manifest.backends) !== JSON.stringify(expected.backends)) {
|
|
270
|
+
throw providerBuildError('PULSE_FASTLY_NATIVE_ARTIFACT_INVALID', 'Fastly artifact backend selection does not match the selected provider profile; rebuild the Native artifact.');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
target: FASTLY_CANONICAL_TARGET,
|
|
275
|
+
provider: 'fastly',
|
|
276
|
+
providerNeutralInput: true,
|
|
277
|
+
nativeWasm: true,
|
|
278
|
+
javascriptRuntime: false,
|
|
279
|
+
jsComputeRuntime: false,
|
|
280
|
+
planHash: plan.planHash,
|
|
281
|
+
compilerVersion: native.compilerVersion,
|
|
282
|
+
compiler: native.manifest.assemblyScript,
|
|
283
|
+
wasm: native.manifest.wasm,
|
|
284
|
+
wat: native.manifest.wat,
|
|
285
|
+
importModules: native.manifest.importModules,
|
|
286
|
+
imports: native.manifest.imports,
|
|
287
|
+
exports: native.manifest.exports,
|
|
288
|
+
policy: native.manifest.policy,
|
|
289
|
+
native
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function writeFastlyCanonicalTarget(options = {}) {
|
|
294
|
+
if (options.sourceOnly === true) {
|
|
295
|
+
throw providerBuildError(
|
|
296
|
+
'PULSE_SOURCE_ONLY_REMOVED',
|
|
297
|
+
'Fastly source-only JavaScript packaging was removed. pulse build emits generated native source and bin/main.wasm together.',
|
|
298
|
+
{ replacement: 'pulse build' }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const outDir = path.resolve(options.outDir || '.');
|
|
302
|
+
const plan = options.plan;
|
|
303
|
+
if (!plan || plan.version !== CANONICAL_NATIVE_PLAN_VERSION) {
|
|
304
|
+
throw new TypeError(`Fastly canonical target requires ${CANONICAL_NATIVE_PLAN_VERSION}.`);
|
|
305
|
+
}
|
|
306
|
+
const lowering = assertFastlyProviderPlan(options.providerPlan);
|
|
307
|
+
const metadata = metadataFromCanonicalPlans(plan, lowering);
|
|
308
|
+
const providerConfig = options.providerConfig || {};
|
|
309
|
+
const bindings = options.bindings || providerConfig.bindings || {};
|
|
310
|
+
const buildConfig = normalizedBuildConfig(providerConfig);
|
|
311
|
+
const grip = validateGripBindings(metadata, bindings);
|
|
312
|
+
|
|
313
|
+
const srcDir = path.join(outDir, 'src');
|
|
314
|
+
const binDir = path.join(outDir, 'bin');
|
|
315
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
316
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
317
|
+
|
|
318
|
+
let native;
|
|
319
|
+
const maxWasmBytes = normalizeFastlyMaxWasmBytes(providerConfig.build?.maxWasmBytes);
|
|
320
|
+
if (isFastlyNativeArtifact(options.native)) assertFastlyWasmBudget(options.native.wasm.length, maxWasmBytes);
|
|
321
|
+
const auditedNative = isFastlyNativeArtifact(options.native)
|
|
322
|
+
? assertExactProviderNative(options.native)
|
|
323
|
+
: null;
|
|
324
|
+
if (auditedNative) {
|
|
325
|
+
native = auditedNative;
|
|
326
|
+
} else {
|
|
327
|
+
const realization = inspectFastlyCanonicalTarget({
|
|
328
|
+
plan,
|
|
329
|
+
providerConfig,
|
|
330
|
+
bindings,
|
|
331
|
+
cwd: options.cwd || options.projectRoot || outDir,
|
|
332
|
+
projectRoot: options.projectRoot,
|
|
333
|
+
profile: options.profile,
|
|
334
|
+
targetDescriptor: options.targetDescriptor,
|
|
335
|
+
synchronizedPackages: options.synchronizedPackages,
|
|
336
|
+
realizationArtifacts: options.realizationArtifacts
|
|
337
|
+
|| (options.native && options.native.realizationArtifacts),
|
|
338
|
+
guestUnits: options.guestUnits
|
|
339
|
+
|| (options.native && options.native.guestUnits),
|
|
340
|
+
nativeOptimization: options.nativeOptimization
|
|
341
|
+
|| (options.experimentalNativeSize === true ? 'experimental-native-size' : undefined),
|
|
342
|
+
elideJsonCacheArena: options.elideJsonCacheArena,
|
|
343
|
+
emitWat: options.emitWat,
|
|
344
|
+
compileTimeoutMs: options.compileTimeoutMs
|
|
345
|
+
});
|
|
346
|
+
native = realization.native;
|
|
347
|
+
}
|
|
348
|
+
const written = writeFastlyNativePlatformCapabilitiesModule(native, outDir, {
|
|
349
|
+
sourceFile: 'src/main.as.ts',
|
|
350
|
+
wasmFile: 'bin/main.wasm',
|
|
351
|
+
watFile: 'bin/main.wat',
|
|
352
|
+
planFile: 'fastly-native-plan.json',
|
|
353
|
+
manifestFile: 'fastly-native-manifest.json'
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const files = {
|
|
357
|
+
providerFile: path.join(outDir, 'fastly-provider.json'),
|
|
358
|
+
entryFile: path.join(outDir, 'fastly-entry.cjs'),
|
|
359
|
+
buildFile: path.join(outDir, 'fastly-build.json'),
|
|
360
|
+
fastlyTomlFile: path.join(outDir, 'fastly.toml'),
|
|
361
|
+
packageFile: path.join(outDir, 'package.json'),
|
|
362
|
+
sourceEntryFile: written.sourceFile,
|
|
363
|
+
sourceHandlerFile: undefined,
|
|
364
|
+
wasmFile: written.wasmFile,
|
|
365
|
+
watFile: written.watFile,
|
|
366
|
+
nativePlanFile: written.planFile,
|
|
367
|
+
nativeManifestFile: written.manifestFile
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
fs.writeFileSync(files.providerFile, stableJson(lowering));
|
|
371
|
+
fs.writeFileSync(files.fastlyTomlFile, renderFastlyToml(buildConfig));
|
|
372
|
+
fs.writeFileSync(files.packageFile, stableJson({
|
|
373
|
+
name: buildConfig.name,
|
|
374
|
+
version: '0.0.0',
|
|
375
|
+
private: true,
|
|
376
|
+
scripts: {
|
|
377
|
+
build: nativeBuildCheckCommand(),
|
|
378
|
+
start: 'fastly compute serve'
|
|
379
|
+
}
|
|
380
|
+
}));
|
|
381
|
+
fs.writeFileSync(files.entryFile, `'use strict';\n\nconst program = require('./canonical-handler.cjs');\nconst { createCanonicalFastlyRuntime } = require('@pulse-compute/provider-fastly/runtime/canonical-api-runtime');\nconst bindings = Object.freeze(${JSON.stringify(bindings)});\nmodule.exports = Object.freeze({\n version: ${JSON.stringify(FASTLY_CANONICAL_BUILD_VERSION)},\n provider: 'fastly',\n deployable: true,\n deploymentValidated: false,\n nativeWasm: true,\n localConformanceRuntime: true,\n createRuntime(options = {}) { return createCanonicalFastlyRuntime({ ...options, bindings }); },\n execute(options = {}) { return createCanonicalFastlyRuntime({ ...options, bindings }).execute(program, options); }\n});\n`);
|
|
382
|
+
|
|
383
|
+
const build = Object.freeze({
|
|
384
|
+
version: FASTLY_CANONICAL_BUILD_VERSION,
|
|
385
|
+
status: 'built',
|
|
386
|
+
provider: 'fastly',
|
|
387
|
+
target: FASTLY_CANONICAL_TARGET,
|
|
388
|
+
sourceHash: metadata.sourceHash,
|
|
389
|
+
projectSourceHash: metadata.projectSourceHash || metadata.sourceHash,
|
|
390
|
+
handlerSourceHash: metadata.sourceHash,
|
|
391
|
+
planVersion: plan.version,
|
|
392
|
+
planHash: plan.planHash,
|
|
393
|
+
deployable: true,
|
|
394
|
+
sourcePackage: true,
|
|
395
|
+
sourceOnly: false,
|
|
396
|
+
compiledWasmPresent: true,
|
|
397
|
+
deploymentValidated: false,
|
|
398
|
+
compileCommand: 'pulse build',
|
|
399
|
+
outputWasm: 'bin/main.wasm',
|
|
400
|
+
compiler: Object.freeze({
|
|
401
|
+
package: native.manifest.assemblyScript.package,
|
|
402
|
+
version: native.manifest.assemblyScript.version,
|
|
403
|
+
target: FASTLY_NATIVE_PLATFORM_CAPABILITIES_VERSION,
|
|
404
|
+
targetCompilerVersion: native.compilerVersion,
|
|
405
|
+
invoked: auditedNative === null
|
|
406
|
+
}),
|
|
407
|
+
optimization: native.manifest.optimization,
|
|
408
|
+
wasm: Object.freeze({ file: 'bin/main.wasm', ...native.manifest.wasm }),
|
|
409
|
+
wat: Object.freeze({ file: written.watFile ? 'bin/main.wat' : null, ...native.manifest.wat }),
|
|
410
|
+
imports: native.manifest.imports,
|
|
411
|
+
importModules: native.manifest.importModules,
|
|
412
|
+
exports: native.manifest.exports,
|
|
413
|
+
crypto: native.manifest.crypto,
|
|
414
|
+
...(native.guestLink ? {
|
|
415
|
+
guestLink: Object.freeze({
|
|
416
|
+
version: native.guestLink.report.version,
|
|
417
|
+
units: native.guestLink.report.units,
|
|
418
|
+
finalArtifact: native.guestLink.report.finalArtifact,
|
|
419
|
+
fallback: false,
|
|
420
|
+
packagedBytesMatchAudit: sha256(fs.readFileSync(written.wasmFile)) === native.guestLink.audit.artifact.sha256
|
|
421
|
+
})
|
|
422
|
+
} : {}),
|
|
423
|
+
localConformanceRuntime: true,
|
|
424
|
+
providerSpecificUserland: false,
|
|
425
|
+
providerSdkUserland: false,
|
|
426
|
+
javascriptRuntime: false,
|
|
427
|
+
jsComputeRuntime: false,
|
|
428
|
+
build: buildConfig,
|
|
429
|
+
files: Object.freeze({
|
|
430
|
+
handler: 'canonical-handler.cjs',
|
|
431
|
+
program: 'canonical-program.json',
|
|
432
|
+
portableWasm: 'canonical-native.wasm',
|
|
433
|
+
portableManifest: 'canonical-native-manifest.json',
|
|
434
|
+
providerPlan: 'fastly-provider.json',
|
|
435
|
+
localEntry: 'fastly-entry.cjs',
|
|
436
|
+
sourceEntry: 'src/main.as.ts',
|
|
437
|
+
wasm: 'bin/main.wasm',
|
|
438
|
+
wat: written.watFile ? 'bin/main.wat' : null,
|
|
439
|
+
nativePlan: 'fastly-native-plan.json',
|
|
440
|
+
nativeManifest: 'fastly-native-manifest.json',
|
|
441
|
+
fastlyToml: 'fastly.toml',
|
|
442
|
+
package: 'package.json',
|
|
443
|
+
schemaRegistry: metadata.schemaIds && metadata.schemaIds.length > 0 ? 'schema-json-registry.json' : undefined,
|
|
444
|
+
schemaCodecs: metadata.schemaIds && metadata.schemaIds.length > 0 ? 'schema-json-codecs.cjs' : undefined
|
|
445
|
+
}),
|
|
446
|
+
grip,
|
|
447
|
+
schemas: Object.freeze({
|
|
448
|
+
active: Boolean(metadata.schemaIds && metadata.schemaIds.length > 0),
|
|
449
|
+
ids: Object.freeze([...(metadata.schemaIds || [])]),
|
|
450
|
+
references: Object.freeze([...(metadata.schemaReferences || [])]),
|
|
451
|
+
sourceHash: metadata.schemaSourceHash
|
|
452
|
+
}),
|
|
453
|
+
lowering
|
|
454
|
+
});
|
|
455
|
+
fs.writeFileSync(files.buildFile, stableJson(build));
|
|
456
|
+
return Object.freeze({ ...files, build, lowering, compilation: native, native });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
module.exports = Object.freeze({
|
|
460
|
+
FASTLY_CANONICAL_BUILD_VERSION,
|
|
461
|
+
FASTLY_CANONICAL_TARGET,
|
|
462
|
+
normalizedBuildConfig,
|
|
463
|
+
renderFastlyToml,
|
|
464
|
+
validateGripBindings,
|
|
465
|
+
isFastlyNativeArtifact,
|
|
466
|
+
assertExactProviderNative,
|
|
467
|
+
inspectFastlyCanonicalTarget,
|
|
468
|
+
writeFastlyCanonicalTarget
|
|
469
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
function digestNativeSource(plan) {
|
|
5
|
+
if (!plan.effects.some(effect => effect.kind === 'crypto.digestText')) return '';
|
|
6
|
+
const selected = plan.crypto?.algorithms.find(entry => entry.algorithm === 'SHA-256');
|
|
7
|
+
if (!selected || selected.realization !== 'guest-source:pulse-hmac-as' || selected.automaticFallback !== false) throw new TypeError('Digest requires the selected Crypto SHA-256 guest source.');
|
|
8
|
+
const root = path.dirname(require.resolve('@pulse-compute/crypto/pulsewasm-native'));
|
|
9
|
+
return fs.readFileSync(path.join(root, 'as/digest-text.as.ts'), 'utf8') + `
|
|
10
|
+
function __pulse_fastly_digest_begin(index: i32, outer: __PulseFastlyValue): void {
|
|
11
|
+
const payload = __pulse_fastly_value(__pulse_fastly_payload_field(outer, 'invocation'))
|
|
12
|
+
const text = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'text'))
|
|
13
|
+
const digest = __pulse_crypto_digest_text(text.kind == PULSE_VALUE_STRING ? text.text : null)
|
|
14
|
+
if (text.kind == PULSE_VALUE_STRING) __pulse_fastly_remember_secret(text.text)
|
|
15
|
+
const result = host_value_object()
|
|
16
|
+
host_value_object_set(result, __pulse_fastly_string_value('status'), __pulse_fastly_string_value(digest.status))
|
|
17
|
+
if (digest.status == 'ok') {
|
|
18
|
+
host_value_object_set(result, __pulse_fastly_string_value('sha256'), __pulse_fastly_string_value(digest.sha256))
|
|
19
|
+
host_value_object_set(result, __pulse_fastly_string_value('byteLength'), host_value_number(f64(digest.byteLength)))
|
|
20
|
+
} else host_value_object_set(result, __pulse_fastly_string_value('reason'), __pulse_fastly_string_value(digest.reason))
|
|
21
|
+
__pulse_fastly_ready_effect(index, result)
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
module.exports = { digestNativeSource };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CANONICAL_EFFECT_INVOCATION_POLICY } = require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
|
|
4
|
+
|
|
5
|
+
function runtimeSource(options = {}) {
|
|
6
|
+
return `
|
|
7
|
+
const PULSE_FASTLY_MAX_EFFECT_INVOCATIONS: i32 = ${CANONICAL_EFFECT_INVOCATION_POLICY.defaultMaxEffects}
|
|
8
|
+
let __pulse_invocation_count: i32 = 0
|
|
9
|
+
let __pulse_invocation_started: bool = false
|
|
10
|
+
let __pulse_invocation_closed: bool = false
|
|
11
|
+
let __pulse_invocation_failure: i32 = 171
|
|
12
|
+
const __pulse_invocation_tickets = new StaticArray<i32>(PULSE_FASTLY_EFFECT_COUNT)
|
|
13
|
+
function __pulse_invocation_begin(index: i32): bool {
|
|
14
|
+
if (__pulse_invocation_closed || index < 0 || index >= PULSE_FASTLY_EFFECT_COUNT
|
|
15
|
+
|| unchecked(__pulse_invocation_tickets[index]) != 0) {
|
|
16
|
+
__pulse_fastly_fail(PULSE_ERROR_STATE, __pulse_invocation_failure, index); return false
|
|
17
|
+
}
|
|
18
|
+
if (__pulse_invocation_count >= PULSE_FASTLY_MAX_EFFECT_INVOCATIONS) {
|
|
19
|
+
__pulse_invocation_closed = true
|
|
20
|
+
__pulse_invocation_failure = 170
|
|
21
|
+
__pulse_fastly_fail(PULSE_ERROR_STATE, 170, index); return false
|
|
22
|
+
}
|
|
23
|
+
unchecked(__pulse_invocation_tickets[index] = ++__pulse_invocation_count)
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
function __pulse_invocation_settle(index: i32, ticket: i32, result: i32): i32 {
|
|
27
|
+
if (__pulse_invocation_closed || index < 0 || index >= PULSE_FASTLY_EFFECT_COUNT
|
|
28
|
+
|| ticket <= 0 || unchecked(__pulse_invocation_tickets[index]) != ticket) {
|
|
29
|
+
__pulse_fastly_fail(PULSE_ERROR_STATE, __pulse_invocation_failure, index); return 0
|
|
30
|
+
}
|
|
31
|
+
unchecked(__pulse_invocation_tickets[index] = 0)
|
|
32
|
+
return ${options.packageCompletion ? "result == 0 ? pulse_package_set_effect_failure(index) : pulse_set_effect_result(index, result)" : "pulse_set_effect_result(index, result)"}
|
|
33
|
+
}
|
|
34
|
+
function __pulse_invocation_close(): void {
|
|
35
|
+
__pulse_invocation_closed = true
|
|
36
|
+
for (let i = 0; i < PULSE_FASTLY_EFFECT_COUNT; i += 1) unchecked(__pulse_invocation_tickets[i] = 0)
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function instrument(source) {
|
|
42
|
+
const signature = 'function host_effect_begin(effectIndex: i32, payload: i32): void {';
|
|
43
|
+
if (!source.includes(signature)) throw new Error('Fastly invocation admission boundary drift.');
|
|
44
|
+
// Every guest-requested slot gets a ticket, including a group member suppressed
|
|
45
|
+
// by an earlier application error. Its undefined result can then be drained.
|
|
46
|
+
return source.replace(signature, signature + '\n if (!__pulse_invocation_begin(effectIndex)) return;');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { runtimeSource, instrument, policy: CANONICAL_EFFECT_INVOCATION_POLICY };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function __pulse_embedded_request_header(name: string): string | null {
|
|
2
|
+
const values = __pulse_request_header_list(name, false);
|
|
3
|
+
return values.length ? values.join(', ') : null;
|
|
4
|
+
}
|
|
5
|
+
function __pulse_embedded_field(payload: __PulseFastlyValue, name: string): string { return __pulse_fastly_string(__pulse_fastly_payload_field(payload, name)); }
|
|
6
|
+
function __pulse_fastly_embedded_asset(payload: __PulseFastlyValue): i32 {
|
|
7
|
+
// Avoid exposing provider buffers as canonical application values.
|
|
8
|
+
const length = __pulse_fastly_value(__pulse_fastly_payload_field(payload, 'embeddedFound')).boolean != 0 ? i32(__pulse_fastly_number(__pulse_fastly_payload_field(payload, 'embeddedLength'))) : -1;
|
|
9
|
+
const etag = __pulse_embedded_field(payload, 'embeddedEtag'), method = __pulse_embedded_field(payload, 'method');
|
|
10
|
+
const selected = __pulse_embedded_selection(length, method, etag,
|
|
11
|
+
__pulse_embedded_request_header('if-none-match'), __pulse_embedded_request_header('range'), __pulse_embedded_request_header('if-range'));
|
|
12
|
+
if (__pulse_request_headers_failed) return 0;
|
|
13
|
+
const output = new __PulseFastlyValue(); output.kind = PULSE_VALUE_RESPONSE; output.status = selected.status;
|
|
14
|
+
if (selected.status != 400 && selected.status != 404) {
|
|
15
|
+
output.headers.push(new __PulseFastlyHeader('content-type', __pulse_embedded_field(payload, 'embeddedType')));
|
|
16
|
+
output.headers.push(new __PulseFastlyHeader('etag', etag)); output.headers.push(new __PulseFastlyHeader('accept-ranges', 'bytes'));
|
|
17
|
+
if (selected.contentRange.length) output.headers.push(new __PulseFastlyHeader('content-range', selected.contentRange));
|
|
18
|
+
if (selected.status == 200 || selected.status == 206) {
|
|
19
|
+
output.headers.push(new __PulseFastlyHeader('content-length', max<i32>(0, selected.end - selected.start + 1).toString()));
|
|
20
|
+
if (method != 'HEAD') output.binaryBody = __pulse_embedded_bytes(__pulse_embedded_field(payload, 'embeddedData'), selected.start, selected.end);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return __pulse_fastly_put(output);
|
|
24
|
+
}
|
|
25
|
+
function __pulse_fastly_write_binary(handle: i32, data: Uint8Array): i32 {
|
|
26
|
+
const written = new StaticArray<i32>(1); let offset = 0;
|
|
27
|
+
while (offset < data.length) {
|
|
28
|
+
if (fastly_http_body_write(handle, data.dataStart + offset, data.length - offset, 0, changetype<usize>(written)) != 0 || written[0] <= 0 || written[0] > data.length - offset) return 1;
|
|
29
|
+
offset += written[0];
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|