@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.
Files changed (103) hide show
  1. package/README.md +53 -1
  2. package/dist/config-schema.json +103 -0
  3. package/dist/index.d.ts +77 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +67 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +88 -5
  8. package/src/build/canonical-target.js +469 -0
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +49 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +559 -0
  13. package/src/build/fixed-memory-runtime.as.ts +148 -0
  14. package/src/build/kv-native.as.ts +331 -0
  15. package/src/build/kv-native.js +27 -0
  16. package/src/build/native-application-errors.js +159 -0
  17. package/src/build/native-binary-resources.js +30 -0
  18. package/src/build/native-body-forwarding.as.ts +341 -0
  19. package/src/build/native-body-forwarding.js +69 -0
  20. package/src/build/native-dynamic-backends.as.ts +115 -0
  21. package/src/build/native-dynamic-backends.js +52 -0
  22. package/src/build/native-fetch-deadlines.as.ts +164 -0
  23. package/src/build/native-fetch-deadlines.js +27 -0
  24. package/src/build/native-fetch-errors.js +38 -0
  25. package/src/build/native-handle-ownership.js +127 -0
  26. package/src/build/native-header-lookup.js +50 -0
  27. package/src/build/native-http-effects.js +994 -0
  28. package/src/build/native-http-shell.js +703 -0
  29. package/src/build/native-platform-capabilities.js +2654 -0
  30. package/src/build/native-request-body.js +44 -0
  31. package/src/build/native-request-headers.js +93 -0
  32. package/src/build/native-send-error-detail.as.ts +12 -0
  33. package/src/build/native-stdio.js +45 -0
  34. package/src/build/native-string-values.js +143 -0
  35. package/src/build/native-transport-details.as.ts +84 -0
  36. package/src/build/native-value-budget.js +90 -0
  37. package/src/build/package-native-application.js +114 -0
  38. package/src/build/request-budget.as.ts +50 -0
  39. package/src/build/request-budget.js +47 -0
  40. package/src/build/s3-body.as.ts +97 -0
  41. package/src/build/s3-native.as.ts +228 -0
  42. package/src/build/s3-native.js +30 -0
  43. package/src/build/schema-admission.js +83 -0
  44. package/src/build/schema-nested-json.js +93 -0
  45. package/src/build/schema-scalar-record.js +41 -0
  46. package/src/build/time-native.js +49 -0
  47. package/src/build/wasm-budget.js +28 -0
  48. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  49. package/src/compiler/assets-provider-proof.js +499 -0
  50. package/src/compiler/config-provider-resolution.js +573 -0
  51. package/src/compiler/fastly-adapter.js +564 -0
  52. package/src/compiler/fastly-command-entry.js +390 -0
  53. package/src/compiler/fastly-config-secret-serve.js +323 -0
  54. package/src/compiler/fastly-hostcall-binding.js +598 -0
  55. package/src/compiler/fastly-readiness.js +477 -0
  56. package/src/compiler/index.js +40 -0
  57. package/src/compiler/lifecycle-parity-contract.js +70 -0
  58. package/src/compiler/lifecycle-parity-proof.js +260 -0
  59. package/src/compiler/route-handler-effect-proof.js +255 -0
  60. package/src/compiler/runtime-provider-kv.js +237 -0
  61. package/src/compiler/runtime-provider-shape.js +109 -0
  62. package/src/config-api.js +69 -0
  63. package/src/config-schema.d.ts +13 -0
  64. package/src/config-schema.js +27 -0
  65. package/src/config-schema.json +103 -0
  66. package/src/index.js +3 -0
  67. package/src/index.ts +143 -0
  68. package/src/javascript/assets-adapter.js +122 -0
  69. package/src/javascript/bindings-adapter.js +237 -0
  70. package/src/javascript/errors.js +26 -0
  71. package/src/javascript/fetch-adapter.js +93 -0
  72. package/src/javascript/grip-broadcast.js +261 -0
  73. package/src/javascript/jwt-verifier.js +147 -0
  74. package/src/javascript/lifecycle.js +55 -0
  75. package/src/javascript/local-loader.js +138 -0
  76. package/src/javascript/logging.js +35 -0
  77. package/src/javascript/package-effects.js +71 -0
  78. package/src/javascript/runtime-host.js +112 -0
  79. package/src/javascript/sha256.js +128 -0
  80. package/src/javascript/source-package.js +778 -0
  81. package/src/javascript/support.js +142 -0
  82. package/src/javascript/target-support-policy.js +277 -0
  83. package/src/javascript/target.js +89 -0
  84. package/src/javascript/test-runtime.js +408 -0
  85. package/src/provider-contract.js +88 -0
  86. package/src/runtime/assets-provider.js +464 -0
  87. package/src/runtime/canonical-api-runtime.js +603 -0
  88. package/src/runtime/index.js +28 -0
  89. package/src/runtime/provider-primitives-contract.js +272 -0
  90. package/src/runtime/provider-primitives.js +82 -0
  91. package/src/runtime/route-handler-effects.js +507 -0
  92. package/src/testing/conditional-kv-host.js +155 -0
  93. package/src/testing/fastly-cli.js +538 -0
  94. package/src/testing/native-dynamic-backends-host.js +39 -0
  95. package/src/testing/native-http-effects-host.js +398 -0
  96. package/src/testing/native-http-shell-host.js +190 -0
  97. package/src/testing/native-platform-capabilities-host.js +924 -0
  98. package/src/toolchain/body-forwarding.js +25 -0
  99. package/src/toolchain/config.js +144 -0
  100. package/src/toolchain/index.js +386 -0
  101. package/src/toolchain/logging.js +18 -0
  102. package/src/toolchain/native-execution.js +78 -0
  103. 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
+ }