@pulse-compute/provider-fastly 1.0.0-beta.1 → 1.0.0-beta.11

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 (83) hide show
  1. package/README.md +7 -6
  2. package/dist/config-schema.json +30 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +11 -10
  8. package/src/build/canonical-target.js +41 -2
  9. package/src/build/digest-native.js +25 -0
  10. package/src/build/effect-invocations.js +51 -0
  11. package/src/build/embedded-assets.as.ts +32 -0
  12. package/src/build/entities-native.js +6 -11
  13. package/src/build/host-extensions.js +254 -0
  14. package/src/build/kv-native.as.ts +331 -0
  15. package/src/build/kv-native.js +27 -0
  16. package/src/build/lifecycle.js +50 -0
  17. package/src/build/native-application-errors.js +159 -0
  18. package/src/build/native-binary-resources.js +30 -0
  19. package/src/build/native-body-forwarding.as.ts +325 -0
  20. package/src/build/native-body-forwarding.js +73 -0
  21. package/src/build/native-dynamic-backends.as.ts +115 -0
  22. package/src/build/native-dynamic-backends.js +52 -0
  23. package/src/build/native-fetch-accounting.as.ts +133 -0
  24. package/src/build/native-fetch-accounting.js +84 -0
  25. package/src/build/native-fetch-bounds.js +47 -0
  26. package/src/build/native-fetch-cohorts.as.ts +126 -0
  27. package/src/build/native-fetch-cohorts.js +65 -0
  28. package/src/build/native-fetch-deadlines.as.ts +169 -0
  29. package/src/build/native-fetch-deadlines.js +36 -0
  30. package/src/build/native-fetch-errors.js +38 -0
  31. package/src/build/native-fetch-headers.js +52 -0
  32. package/src/build/native-fetch-outcomes.js +87 -0
  33. package/src/build/native-fetch-timing.as.ts +74 -0
  34. package/src/build/native-fetch-timing.js +50 -0
  35. package/src/build/native-forwarding-accounting.as.ts +153 -0
  36. package/src/build/native-forwarding-accounting.js +47 -0
  37. package/src/build/native-handle-ownership.js +127 -0
  38. package/src/build/native-header-lookup.js +50 -0
  39. package/src/build/native-http-effects.js +164 -35
  40. package/src/build/native-platform-capabilities.js +815 -220
  41. package/src/build/native-request-body.js +45 -0
  42. package/src/build/native-request-headers.js +93 -0
  43. package/src/build/native-send-error-detail.as.ts +12 -0
  44. package/src/build/native-stdio.js +45 -0
  45. package/src/build/native-string-values.js +143 -0
  46. package/src/build/native-transport-details.as.ts +84 -0
  47. package/src/build/native-value-budget.js +90 -0
  48. package/src/build/package-native-application.js +114 -0
  49. package/src/build/request-budget-source.js +58 -0
  50. package/src/build/request-budget.js +46 -0
  51. package/src/build/response-authority.js +56 -0
  52. package/src/build/s3-body.as.ts +97 -0
  53. package/src/build/s3-native.as.ts +228 -0
  54. package/src/build/s3-native.js +30 -0
  55. package/src/build/schema-admission.js +83 -0
  56. package/src/build/schema-nested-json.js +93 -0
  57. package/src/build/schema-scalar-record.js +41 -0
  58. package/src/build/time-native.js +49 -0
  59. package/src/build/wasm-budget.js +28 -0
  60. package/src/config-api.js +6 -0
  61. package/src/config-schema.json +30 -0
  62. package/src/index.ts +25 -0
  63. package/src/javascript/fetch-adapter.js +3 -0
  64. package/src/javascript/jwt-verifier.js +14 -4
  65. package/src/javascript/logging.js +11 -2
  66. package/src/javascript/package-effects.js +5 -2
  67. package/src/javascript/runtime-host.js +8 -3
  68. package/src/javascript/source-package.js +4 -2
  69. package/src/javascript/target-support-policy.js +20 -3
  70. package/src/javascript/target.js +17 -1
  71. package/src/javascript/test-runtime.js +1 -0
  72. package/src/provider-contract.js +23 -6
  73. package/src/runtime/canonical-api-runtime.js +6 -0
  74. package/src/testing/conditional-kv-host.js +155 -0
  75. package/src/testing/native-dynamic-backends-host.js +39 -0
  76. package/src/testing/native-http-effects-host.js +18 -1
  77. package/src/testing/native-platform-capabilities-host.js +359 -24
  78. package/src/toolchain/body-forwarding.js +25 -0
  79. package/src/toolchain/config.js +16 -0
  80. package/src/toolchain/index.js +49 -5
  81. package/src/toolchain/logging.js +18 -0
  82. package/src/toolchain/native-execution.js +83 -0
  83. package/src/toolchain/s3.js +26 -0
@@ -1,4 +1,6 @@
1
1
  'use strict';
2
+ const { normalizeFastlyMaxWasmBytes } = require('../build/wasm-budget.js');
3
+ const { normalizeFastlyS3 } = require('./s3.js');
2
4
 
3
5
  function providerConfigError(code, message, detail) {
4
6
  const error = new TypeError(message);
@@ -32,11 +34,15 @@ function normalizeFastlyProviderConfig(value) {
32
34
  : {};
33
35
  return Object.freeze({
34
36
  kind: 'fastly',
37
+ bodyForwarding: require('./body-forwarding.js').normalizeBodyForwarding(input.bodyForwarding, input.maxDurationMs),
38
+ maxDurationMs: require('@pulse-compute/runtime/host').normalizeRequestDuration(input.maxDurationMs),
35
39
  version: input.version ? String(input.version) : undefined,
36
40
  bindings: Object.freeze({
41
+ ...(bindings.logEndpoint === undefined ? {} : { logEndpoint: require('./logging.js').normalizeFastlyLogEndpoint(bindings.logEndpoint) }),
37
42
  configStore: String(bindings.configStore || 'pulse_config'),
38
43
  secretStore: String(bindings.secretStore || 'pulse_secrets'),
39
44
  kv: normalizeStringMap(bindings.kv, 'PULSE_FASTLY_KV_BINDINGS_INVALID'),
45
+ s3: normalizeFastlyS3(bindings.s3),
40
46
  backends: normalizeStringMap(bindings.backends, 'PULSE_FASTLY_BACKEND_BINDINGS_INVALID'),
41
47
  grip: Object.freeze({
42
48
  fanoutBackend: gripInput.fanoutBackend === undefined ? undefined : String(gripInput.fanoutBackend),
@@ -52,6 +58,7 @@ function normalizeFastlyProviderConfig(value) {
52
58
  dynamicBackends: bindings.dynamicBackends === true
53
59
  }),
54
60
  build: Object.freeze({
61
+ maxWasmBytes: normalizeFastlyMaxWasmBytes(input.build?.maxWasmBytes),
55
62
  name: String(input.build && input.build.name || 'pulse-app'),
56
63
  description: String(input.build && input.build.description || 'Pulse application'),
57
64
  authors: Object.freeze(Array.isArray(input.build && input.build.authors) ? input.build.authors.map(String) : [])
@@ -65,6 +72,8 @@ function fastlyProjectConfigDocument(config) {
65
72
  return Object.freeze({
66
73
  kind: 'fastly',
67
74
  bindings: config.bindings,
75
+ maxDurationMs: config.maxDurationMs,
76
+ bodyForwarding: config.bodyForwarding,
68
77
  build: config.build,
69
78
  local: config.local,
70
79
  providerSpecificUserland: false
@@ -98,6 +107,13 @@ const FASTLY_PROJECT_CONFIG_REFERENCE = Object.freeze({
98
107
  })
99
108
  ]),
100
109
  fields: Object.freeze([
110
+ Object.freeze({ section: 'fastly', path: 'fastly.logEndpoint', type: 'string', default: 'omitted (stdout)', scope: 'fastly() facade; conventional profiles use fastly.bindings.logEndpoint', description: 'One case-sensitive named production log stream, 1–255 UTF-8 bytes, excluding stdout, stderr, fst_managed_ names and control characters. Records above 65536 UTF-8 bytes are dropped. One lookup and at most one write per record; errors and zero/short writes are dropped without retry or stdout fallback. Host acceptance does not prove downstream delivery.', diagnostics: Object.freeze(['PULSE_FASTLY_LOG_ENDPOINT_INVALID']) }),
111
+ Object.freeze({section:'fastly',path:'fastly.bodyForwarding',type:'object',default:'omitted',scope:'experimental Fastly Native forwarding',description:'Opt in to one terminal opaque POST with inline req.body() and a static backend. Requires maxDurationMs and both byte limits. Final close hands queued delivery to Fastly; it is not a client receipt.',diagnostics:Object.freeze(['PULSE_FASTLY_BODY_FORWARDING_INVALID'])}),
112
+ ...['requestMaxBytes','responseMaxBytes'].map(name => Object.freeze({section:'fastly',path:`fastly.bodyForwarding.${name}`,type:'integer',allowed:'1–1048576',default:'required with bodyForwarding',scope:'experimental Fastly Native forwarding',description:'Independent actual-byte bound, including EOF verification; no unbounded default. Fixed 16 KiB raw scratch is reused across the sequential upload and response pumps.'})),
113
+ Object.freeze({section:'fastly',path:'fastly.maxWasmBytes',type:'integer',allowed:'positive safe integer',default:'4194304 (4 MiB)',scope:'fastly() facade',description:'Facade option projected to build.maxWasmBytes. Conventional profiles configure fastly.build.maxWasmBytes.'}),
114
+ Object.freeze({section:'fastly',path:'fastly.build.maxWasmBytes',type:'integer',allowed:'positive safe integer',default:'4194304 (4 MiB)',scope:'Native final Wasm artifact',description:'Inclusive byte budget checked for newly compiled and reused artifacts. Raising it does not increase runtime memory or change Fastly service limits. The fastly() facade accepts maxWasmBytes.'}),
115
+ Object.freeze({section:'fastly',path:'fastly.maxDurationMs',type:'integer',allowed:'1–30000',default:'omitted',scope:'HTTP request execution',description:'One provider-owned monotonic budget through buffered response handoff; expiry attempts 504 before headers commit and never asserts rollback of dispatched writes. No CPU preemption or post-handoff delivery guarantee.'}),
116
+ Object.freeze({ section: 'fastly', path: 'fastly.bindings.s3', type: 'Readonly<Record<string, S3Binding & { backend: string }>>', default: '`{}`', scope: 'Fastly Native S3', description: 'Maps logical names to fixed HTTPS endpoint, bucket, region, named static backend, accessKeyIdSecret, secretAccessKeySecret, optional sessionTokenSecret, maxTextBytes (1–2097152, default 32768) and timeoutMs (1–30000, default 10000).', security: 'Credentials resolve through the configured Secret Store at execution. Dynamic backend authority and guest endpoint overrides are forbidden.' }),
101
117
  Object.freeze({ section: 'fastly', path: 'fastly.configStore', type: 'string', default: '`pulse_config`', scope: 'Fastly config capability', description: 'Fastly Config Store resource name.' }),
102
118
  Object.freeze({ section: 'fastly', path: 'fastly.secretStore', type: 'string', default: '`pulse_secrets`', scope: 'Fastly secret capability', description: 'Fastly Secret Store resource name.', security: 'The name is build metadata; secret values are never embedded by this option.' }),
103
119
  Object.freeze({ section: 'fastly', path: 'fastly.kv', type: 'Readonly<Record<string, string>>', default: '`{}`', scope: 'Fastly KV capability', description: 'Maps logical ctx.kv names to Fastly KV Store resource names.', diagnostics: Object.freeze(['PULSE_FASTLY_KV_BINDINGS_INVALID']) }),
@@ -38,7 +38,8 @@ const packageManifest = require('../../package.json');
38
38
  const reportingTargetCapability = Object.freeze({
39
39
  supported: true,
40
40
  default: 'info',
41
- levels: Object.freeze(['off', 'error', 'warn', 'info', 'debug'])
41
+ levels: Object.freeze(['off', 'error', 'warn', 'info', 'debug']),
42
+ destinations: Object.freeze(['stdout', 'configured-named-endpoint'])
42
43
  });
43
44
 
44
45
  const fastlyFinalWasmPolicy = defineFinalWasmPolicy({
@@ -124,6 +125,7 @@ function createDriver() {
124
125
  projectRoot: invocation.project.root,
125
126
  outDir: invocation.project.outDir,
126
127
  schemaBundle: invocation.javascript.schemaBundle,
128
+ binaryResources: invocation.javascript.binaryResources,
127
129
  providerConfig: invocation.providerConfig,
128
130
  reporting: invocation.javascript.reporting,
129
131
  strict: invocation.javascript.strict,
@@ -166,17 +168,36 @@ function createDriver() {
166
168
  version: 'pulse.provider-target-descriptor.v1',
167
169
  target: 'native',
168
170
  targetId: 'fastly-compute-native',
171
+ hostExtensions: require('../build/host-extensions.js').ADMISSION,
169
172
  runtimeClass: 'native',
170
173
  status: 'supported',
171
174
  capabilities: Object.freeze([
175
+ 's3.head', 's3.getText', 's3.getBody',
176
+ 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap', 'kv.list', 'kv.delete',
172
177
  'jwt.verify',
178
+ 'jwt.sign',
173
179
  'jwt.verify.hs256',
174
180
  'jwt.verify.es256',
181
+ 'jwt.verify.rs256',
175
182
  'secret.get',
176
183
  'time.wall-clock'
177
184
  ]),
178
185
  keyTypes: Object.freeze(['secret', 'jwk', 'jwks']),
179
186
  realizations: Object.freeze([
187
+ Object.freeze({
188
+ kind: 'package-native-application',
189
+ contractId: 'pulse.entities',
190
+ implementation: 'pulse.package-native-application.v1',
191
+ implemented: true,
192
+ automaticFallback: false
193
+ }),
194
+ Object.freeze({
195
+ kind: 'package-native-application',
196
+ contractId: 'pulse.mcp',
197
+ implementation: 'pulse.package-native-application.v2',
198
+ implemented: true,
199
+ automaticFallback: false
200
+ }),
180
201
  Object.freeze({
181
202
  kind: 'crypto-composed',
182
203
  realization: 'guest-source:pulse-hmac-as',
@@ -202,6 +223,19 @@ function createDriver() {
202
223
  guestUnitRequired: true,
203
224
  portable: true,
204
225
  automaticFallback: false
226
+ }),
227
+ Object.freeze({
228
+ kind: 'crypto-composed',
229
+ realization: 'guest-linked:pulse-rs256-bearssl-i31',
230
+ implementation: 'bearssl.0.6.rsa-i31.sha256.v1',
231
+ algorithms: Object.freeze(['RS256']),
232
+ keyTypes: Object.freeze(['jwk', 'jwks']),
233
+ implemented: true,
234
+ status: 'implemented-g4',
235
+ semanticOwner: '@pulse-compute/crypto',
236
+ guestUnitRequired: true,
237
+ portable: true,
238
+ automaticFallback: false
205
239
  })
206
240
  ]),
207
241
  jwt: Object.freeze({
@@ -221,7 +255,7 @@ function createDriver() {
221
255
  automaticFallback: false,
222
256
  crypto: defineCryptoTargetCapabilities({
223
257
  target: 'native',
224
- algorithms: [{
258
+ algorithms: [...['SHA-256', 'HMAC-SHA256'].map((algorithm) => ({ algorithm, realization: 'guest-source:pulse-hmac-as', implemented: true, status: 'implemented-o2' })), {
225
259
  algorithm: 'HS256',
226
260
  realization: 'guest-source:pulse-hmac-as',
227
261
  implemented: true,
@@ -231,6 +265,11 @@ function createDriver() {
231
265
  realization: 'guest-linked:pulse-es256-rustcrypto-p256',
232
266
  implemented: true,
233
267
  status: 'implemented-g4'
268
+ }, {
269
+ algorithm: 'RS256',
270
+ realization: 'guest-linked:pulse-rs256-bearssl-i31',
271
+ implemented: true,
272
+ status: 'implemented-g4'
234
273
  }]
235
274
  }),
236
275
  finalWasmPolicy: fastlyFinalWasmPolicy,
@@ -240,8 +279,9 @@ function createDriver() {
240
279
  javascript: FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR
241
280
  }),
242
281
  execute: fastlyRuntime.executeCanonicalProgram,
282
+ prepareNativeExecution: require('./native-execution.js').prepareFastlyNativeExecution,
243
283
  createLoweringPlan(metadata, providerConfig = {}) {
244
- return createFastlyLoweringPlan(metadata, providerConfig.bindings || {});
284
+ return createFastlyLoweringPlan(metadata, providerConfig.bindings || {}, providerConfig);
245
285
  },
246
286
  inspectRealization(invocation) {
247
287
  const realization = inspectFastlyCanonicalTarget({
@@ -255,6 +295,8 @@ function createDriver() {
255
295
  realizationArtifacts: invocation.nativeArtifact.realizationArtifacts,
256
296
  guestUnits: invocation.nativeArtifact.guestUnits,
257
297
  nativeOptimization: invocation.optimization,
298
+ elideJsonCacheArena: invocation.nativeArtifact.manifest.elideJsonCacheArena === true,
299
+ emitWat: invocation.nativeArtifact.manifest.wat?.emitted === true,
258
300
  compileTimeoutMs: invocation.timeoutMs
259
301
  });
260
302
  const { native, ...publicRealization } = realization;
@@ -283,6 +325,8 @@ function createDriver() {
283
325
  realizationArtifacts: invocation.nativeArtifact.realizationArtifacts,
284
326
  guestUnits: invocation.nativeArtifact.guestUnits,
285
327
  nativeOptimization: invocation.optimization,
328
+ elideJsonCacheArena: invocation.nativeArtifact.manifest.elideJsonCacheArena === true,
329
+ emitWat: invocation.nativeArtifact.manifest.wat?.emitted === true,
286
330
  compileTimeoutMs: invocation.timeoutMs
287
331
  });
288
332
  const providerBuild = writeFastlyCanonicalTarget({
@@ -318,7 +362,7 @@ function createDriver() {
318
362
  providerPackage: providerBuild.packageFile,
319
363
  providerSourceEntry: providerBuild.sourceEntryFile
320
364
  }),
321
- build: providerBuild.build,
365
+ build: { ...providerBuild.build, reportAttribution: native.reportAttribution, hostExtensions: native.manifest.hostExtensions, buildObservation: native.buildObservation },
322
366
  realization: publicRealization,
323
367
  packaging: audit ? Object.freeze({
324
368
  version: PROVIDER_PACKAGING_AUDIT_VERSION,
@@ -329,7 +373,7 @@ function createDriver() {
329
373
  });
330
374
  },
331
375
  executionOptions(providerConfig = {}, values = {}) {
332
- return { ...values, bindings: providerConfig.bindings };
376
+ return { ...values, maxDurationMs: providerConfig.maxDurationMs, bindings: providerConfig.bindings };
333
377
  }
334
378
  });
335
379
  }
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const MAX_LOG_BYTES = 65536;
4
+
5
+ function normalizeFastlyLogEndpoint(value) {
6
+ if (value === undefined) return undefined;
7
+ if (typeof value !== 'string' || !value || value.length > 255 || value.trim() !== value
8
+ || new TextEncoder().encode(value).byteLength > 255 || /[\u0000-\u001f\u007f]/u.test(value)
9
+ || /[\ud800-\udfff]/u.test(value.replace(/[\ud800-\udbff][\udc00-\udfff]/g, ''))
10
+ || /^(?:stdout$|stderr$|fst_managed_)/i.test(value)) {
11
+ const error = new TypeError('Fastly logEndpoint must be a non-reserved endpoint name of 1–255 UTF-8 bytes without control characters or surrounding whitespace.');
12
+ error.code = 'PULSE_FASTLY_LOG_ENDPOINT_INVALID';
13
+ throw error;
14
+ }
15
+ return value;
16
+ }
17
+
18
+ module.exports = { normalizeFastlyLogEndpoint, MAX_LOG_BYTES };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const { inspectFastlyCanonicalTarget } = require('../build/canonical-target.js');
4
+ const { executeFastlyNativePlatformCapabilities } = require('../testing/native-platform-capabilities-host.js');
5
+ const { createConditionalKvAuthority } = require('../testing/conditional-kv-host.js');
6
+
7
+ // Execute the provider artifact against the existing fixture ABI. This is local
8
+ // conformance evidence, never Viceroy or deployed Fastly acceptance.
9
+ function prepareFastlyNativeExecution(invocation) {
10
+ const bindings = invocation.providerConfig.bindings;
11
+ const { native } = inspectFastlyCanonicalTarget({
12
+ plan: invocation.applicationPlan,
13
+ providerConfig: invocation.providerConfig,
14
+ projectRoot: invocation.project.root,
15
+ profile: invocation.project.profile,
16
+ targetDescriptor: invocation.selectedTarget,
17
+ synchronizedPackages: invocation.synchronizedPackages,
18
+ realizationArtifacts: invocation.nativeArtifact.realizationArtifacts,
19
+ guestUnits: invocation.nativeArtifact.guestUnits,
20
+ nativeOptimization: invocation.optimization,
21
+ elideJsonCacheArena: invocation.nativeArtifact.manifest.elideJsonCacheArena === true,
22
+ emitWat: invocation.nativeArtifact.manifest.wat?.emitted === true,
23
+ compileTimeoutMs: invocation.timeoutMs || undefined
24
+ });
25
+ const evidence = Object.freeze({
26
+ kind: 'fastly-native-fixture-abi',
27
+ provider: 'fastly',
28
+ target: 'fastly-compute-native',
29
+ wasmSha256: native.manifest.wasm.sha256,
30
+ providerRealityValidated: false,
31
+ effectObservations: 'unavailable',
32
+ kvFixture: Object.freeze({ lifetime: 'invocation', initialization: 'supplied-fixtures', crossRequestPersistence: false }),
33
+ notice: 'KV fixture state is seeded from supplied fixtures for each invocation; writes do not persist across requests. Use the Fastly CLI/Viceroy workflow for local provider persistence and contention checks; deployed-service acceptance is separate.'
34
+ });
35
+ const execute = (options = {}) => {
36
+ if (options.liveFetch === true) throw new TypeError('Fastly Native fixture execution cannot perform live fetches.');
37
+ const kvStores = {};
38
+ for (const [logical, physical] of Object.entries(bindings.kv || {})) {
39
+ kvStores[physical] = { ...kvStores[physical], ...(options.kv && options.kv[logical] || {}) };
40
+ }
41
+ const authority = createConditionalKvAuthority();
42
+ for (const [store, values] of Object.entries(kvStores)) {
43
+ authority.stores.set(store, new Map());
44
+ for (const [key, value] of Object.entries(values)) {
45
+ authority.seed(store, key, Buffer.from(JSON.stringify({ __pulseKv: 1, value })));
46
+ }
47
+ }
48
+ const execution = executeFastlyNativePlatformCapabilities(native, {
49
+ request: options.request,
50
+ fixtures: options.fetches,
51
+ configStore: bindings.configStore,
52
+ config: options.config,
53
+ secretStore: bindings.secretStore,
54
+ secrets: options.secrets,
55
+ kvStores,
56
+ conditionalKv: { authority }
57
+ });
58
+ const contentType = execution.response.headers.find(([name]) => name.toLowerCase() === 'content-type');
59
+ const type = String(contentType && contentType[1] || '').toLowerCase();
60
+ return Object.freeze({
61
+ response: Object.freeze({
62
+ ...execution.response,
63
+ kind: type.includes('application/json') || type.includes('+json') ? 'json' : type.startsWith('text/') ? 'text' : 'response',
64
+ // Forwarding filters origin metadata into a new response handle, so
65
+ // its streamed payload remains opaque even though origin is false.
66
+ // Pre-commit errors use ordinary non-streaming structured responses.
67
+ bodyClass: execution.response.origin === true
68
+ || (native.plan.capabilities.includes('request.body.forward') && execution.response.streaming === 1)
69
+ ? 'opaque' : 'structured'
70
+ }),
71
+ // The fixture ABI has hostcall traces, not canonical continuation telemetry.
72
+ effectCount: null,
73
+ continuations: Object.freeze([]),
74
+ resolutionOrder: Object.freeze([]),
75
+ evidence
76
+ });
77
+ };
78
+ // Public observation already returned with execution results, available before
79
+ // the first request without running a probe or acquiring additional authority.
80
+ return Object.freeze(Object.assign(execute, { evidence }));
81
+ }
82
+
83
+ module.exports = { prepareFastlyNativeExecution };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+ const { normalizeBinding, namePattern } = require('@pulse-compute/s3/provider');
3
+ function normalizeFastlyS3(input = {}) {
4
+ if (!input || typeof input !== 'object' || Array.isArray(input) || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) throw new TypeError('Fastly S3 bindings must be a map.');
5
+ for (const key of Reflect.ownKeys(input)) {
6
+ const field = Object.getOwnPropertyDescriptor(input, key);
7
+ if (typeof key !== 'string' || !field.enumerable || !Object.hasOwn(field, 'value')) throw new TypeError('Fastly S3 bindings require data properties.');
8
+ }
9
+ return Object.freeze(Object.fromEntries(Object.entries(input).map(([name, value]) => {
10
+ if (!namePattern.test(name) || !value || typeof value !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
11
+ || Reflect.ownKeys(value).some((key) => { const field = Object.getOwnPropertyDescriptor(value, key); return typeof key !== 'string' || !field.enumerable || !Object.hasOwn(field, 'value'); })) throw new TypeError('Fastly S3 binding is invalid.');
12
+ const { backend, ...binding } = value;
13
+ if (typeof backend !== 'string' || !namePattern.test(backend)) throw new TypeError('Fastly S3 requires a named static backend.');
14
+ return [name, Object.freeze({ ...normalizeBinding(binding), backend })];
15
+ })));
16
+ }
17
+ function resolveFastlyS3(effects, input) {
18
+ const configured = normalizeFastlyS3(input);
19
+ return Object.freeze(effects.flatMap((effect, index) => {
20
+ if (!['s3.head', 's3.getText', 's3.putText', 's3.getBody'].includes(effect.kind)) return [];
21
+ const name = effect.resource && effect.resource.binding;
22
+ if (!Object.hasOwn(configured, name)) throw new TypeError('The required Fastly S3 binding is missing.');
23
+ return [Object.freeze({ index, ...configured[name], ...(effect.operation === 'getBody' ? require('@pulse-compute/s3/provider').normalizeBodyOptions(Object.fromEntries(['method', 'range', 'ifNoneMatch'].filter(key => effect.payload?.[key] !== undefined).map(key => [key, effect.payload[key]]))) : {}), contentType: effect.operation === 'putText' ? require('@pulse-compute/s3/provider').normalizePutOptions({ contentType: effect.payload && effect.payload.contentType }).contentType : '' })];
24
+ }));
25
+ }
26
+ module.exports = { normalizeFastlyS3, resolveFastlyS3 };