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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +52 -1
  2. package/dist/config-schema.json +73 -0
  3. package/dist/index.d.ts +61 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +59 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +87 -5
  8. package/src/build/canonical-target.js +436 -0
  9. package/src/build/entities-native.js +564 -0
  10. package/src/build/fixed-memory-runtime.as.ts +148 -0
  11. package/src/build/native-http-effects.js +871 -0
  12. package/src/build/native-http-shell.js +703 -0
  13. package/src/build/native-platform-capabilities.js +2109 -0
  14. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  15. package/src/compiler/assets-provider-proof.js +499 -0
  16. package/src/compiler/config-provider-resolution.js +573 -0
  17. package/src/compiler/fastly-adapter.js +564 -0
  18. package/src/compiler/fastly-command-entry.js +390 -0
  19. package/src/compiler/fastly-config-secret-serve.js +323 -0
  20. package/src/compiler/fastly-hostcall-binding.js +598 -0
  21. package/src/compiler/fastly-readiness.js +477 -0
  22. package/src/compiler/index.js +40 -0
  23. package/src/compiler/lifecycle-parity-contract.js +70 -0
  24. package/src/compiler/lifecycle-parity-proof.js +260 -0
  25. package/src/compiler/route-handler-effect-proof.js +255 -0
  26. package/src/compiler/runtime-provider-kv.js +237 -0
  27. package/src/compiler/runtime-provider-shape.js +109 -0
  28. package/src/config-api.js +63 -0
  29. package/src/config-schema.d.ts +13 -0
  30. package/src/config-schema.js +27 -0
  31. package/src/config-schema.json +73 -0
  32. package/src/index.js +3 -0
  33. package/src/index.ts +118 -0
  34. package/src/javascript/assets-adapter.js +122 -0
  35. package/src/javascript/bindings-adapter.js +237 -0
  36. package/src/javascript/errors.js +26 -0
  37. package/src/javascript/fetch-adapter.js +93 -0
  38. package/src/javascript/grip-broadcast.js +261 -0
  39. package/src/javascript/jwt-verifier.js +137 -0
  40. package/src/javascript/lifecycle.js +55 -0
  41. package/src/javascript/local-loader.js +138 -0
  42. package/src/javascript/logging.js +26 -0
  43. package/src/javascript/package-effects.js +68 -0
  44. package/src/javascript/runtime-host.js +108 -0
  45. package/src/javascript/sha256.js +128 -0
  46. package/src/javascript/source-package.js +776 -0
  47. package/src/javascript/support.js +142 -0
  48. package/src/javascript/target-support-policy.js +260 -0
  49. package/src/javascript/target.js +73 -0
  50. package/src/javascript/test-runtime.js +407 -0
  51. package/src/provider-contract.js +72 -0
  52. package/src/runtime/assets-provider.js +464 -0
  53. package/src/runtime/canonical-api-runtime.js +597 -0
  54. package/src/runtime/index.js +28 -0
  55. package/src/runtime/provider-primitives-contract.js +272 -0
  56. package/src/runtime/provider-primitives.js +82 -0
  57. package/src/runtime/route-handler-effects.js +507 -0
  58. package/src/testing/fastly-cli.js +538 -0
  59. package/src/testing/native-http-effects-host.js +390 -0
  60. package/src/testing/native-http-shell-host.js +190 -0
  61. package/src/testing/native-platform-capabilities-host.js +599 -0
  62. package/src/toolchain/config.js +128 -0
  63. package/src/toolchain/index.js +343 -0
@@ -0,0 +1,237 @@
1
+ 'use strict';
2
+
3
+ const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = loadContractsDiagnostics();
4
+ const { validateRuntimeProvider, extractProviderMap, extractKvCapabilities, normalizeRuntimeProvider } = require('./runtime-provider-shape.js');
5
+ const {
6
+ CONFIG_AUTHORING_SHAPE_VERSION,
7
+ PLATFORM_STORE_MAP_VERSION,
8
+ RUNTIME_PROVIDER_KV_PHASE,
9
+ KV_ADAPTER_INTEGRATION_PHASE,
10
+ RUNTIME_PROVIDER_CONTRACT_VERSION,
11
+ RUNTIME_PROVIDER_KV_RESOLUTION_VERSION,
12
+ KV_CAPABILITY_CONTRACT_VERSION,
13
+ KV_PROVIDER_MAP_VERSION,
14
+ RUNTIME_PROVIDER_API_SURFACE_VERSION,
15
+ RUNTIME_PROVIDER_SMOKE_VERSION,
16
+ KV_ADAPTER_INTEGRATION_VERSION,
17
+ KV_PROVIDER_RUNTIME_MAP_VERSION,
18
+ RUNTIME_PROVIDER_AUTHORING_SHAPE,
19
+ RUNTIME_PROVIDER_DEFAULT_POLICY,
20
+ RUNTIME_PROVIDER_KINDS,
21
+ KV_CAPABILITY_POLICY,
22
+ RUNTIME_PROVIDER_API_SURFACES
23
+ } = loadContractsRuntimeProviderKv();
24
+
25
+
26
+ function loadContractsDiagnostics() {
27
+ try {
28
+ return require('@pulse-compute/wasm-contracts/diagnostics');
29
+ } catch (error) {
30
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
31
+ return require('../../../../wasm/packages/contracts/src/diagnostics.js');
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ function loadContractsRuntimeProviderKv() {
38
+ try {
39
+ return require('@pulse-compute/wasm-contracts/config/runtime-provider-kv');
40
+ } catch (error) {
41
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
42
+ return require('../../../../wasm/packages/contracts/src/config/runtime-provider-kv.js');
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ function plainObject(value) {
49
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
50
+ }
51
+
52
+ function makeDiag(raw) {
53
+ return normalizeDiagnostic({
54
+ code: raw.code || 'PULSEWASM_RUNTIME_PROVIDER_INVALID',
55
+ severity: 'error',
56
+ phase: 'runtime-provider-kv',
57
+ pass: 'runtime-provider-kv',
58
+ loc: { file: '<config>' },
59
+ message: raw.message || 'Invalid runtime provider configuration.',
60
+ details: raw
61
+ });
62
+ }
63
+
64
+ function normalizeResolvedConfig(input = {}) {
65
+ return input && input.artifact ? input.artifact : input;
66
+ }
67
+
68
+ function buildRuntimeProviderKv(options = {}) {
69
+ const cwd = options.cwd || process.cwd();
70
+ const generatedBy = options.generatedBy || PACKAGE_VERSION;
71
+ const resolvedConfig = normalizeResolvedConfig(options.resolvedConfig || {});
72
+ const profileName = resolvedConfig.profile || resolvedConfig.selectedProfile || options.profile || 'edge';
73
+ const profile = { runtime: plainObject(resolvedConfig.runtime) };
74
+ const provider = normalizeRuntimeProvider(profile.runtime) || {};
75
+ const rawDiagnostics = validateRuntimeProvider(profileName, profile);
76
+ const diagnostics = rawDiagnostics.map(makeDiag);
77
+ const providerMap = extractProviderMap(profileName, profile);
78
+ const kvCapabilities = extractKvCapabilities(profileName, profile);
79
+
80
+ const contract = normalizeArtifact({
81
+ version: RUNTIME_PROVIDER_CONTRACT_VERSION,
82
+ generatedBy,
83
+ phase: RUNTIME_PROVIDER_KV_PHASE,
84
+ status: diagnostics.length ? 'error' : 'ok',
85
+ authoringShape: RUNTIME_PROVIDER_AUTHORING_SHAPE,
86
+ defaultPolicy: RUNTIME_PROVIDER_DEFAULT_POLICY,
87
+ providerKinds: RUNTIME_PROVIDER_KINDS.slice(),
88
+ provider,
89
+ summary: {
90
+ profile: profileName,
91
+ provider: provider.kind || null,
92
+ kvNamespaces: kvCapabilities.length,
93
+ configStore: provider.configStore || null,
94
+ secretStore: provider.secretStore || null
95
+ }
96
+ }, cwd);
97
+
98
+ const resolution = normalizeArtifact({
99
+ version: RUNTIME_PROVIDER_KV_RESOLUTION_VERSION,
100
+ generatedBy,
101
+ phase: RUNTIME_PROVIDER_KV_PHASE,
102
+ status: diagnostics.length ? 'error' : 'ok',
103
+ profile: profileName,
104
+ provider,
105
+ providerMap,
106
+ kvCapabilities,
107
+ summary: contract.summary
108
+ }, cwd);
109
+
110
+ const configAuthoringShape = normalizeArtifact({
111
+ version: CONFIG_AUTHORING_SHAPE_VERSION,
112
+ generatedBy,
113
+ phase: RUNTIME_PROVIDER_KV_PHASE,
114
+ canonical: RUNTIME_PROVIDER_AUTHORING_SHAPE,
115
+ rootConfigDefinesTopologyOnly: true,
116
+ profileRuntimeDefinesBehavior: true,
117
+ examples: {
118
+ provider: {
119
+ kind: 'fastly',
120
+ configStore: 'pulse_config',
121
+ secretStore: 'pulse_secrets',
122
+ kv: { sessions: 'sessions' }
123
+ }
124
+ },
125
+ summary: { canonical: RUNTIME_PROVIDER_AUTHORING_SHAPE }
126
+ }, cwd);
127
+
128
+ const platformStoreMap = normalizeArtifact({
129
+ version: PLATFORM_STORE_MAP_VERSION,
130
+ generatedBy,
131
+ phase: RUNTIME_PROVIDER_KV_PHASE,
132
+ providerKind: provider.kind || null,
133
+ stores: providerMap.stores,
134
+ kv: providerMap.kv,
135
+ generatedOutputOnly: true,
136
+ summary: { stores: Object.keys(providerMap.stores || {}).length, kvNamespaces: Object.keys(providerMap.kv || {}).length }
137
+ }, cwd);
138
+
139
+ const kvCapabilityContract = normalizeArtifact({
140
+ version: KV_CAPABILITY_CONTRACT_VERSION,
141
+ generatedBy,
142
+ phase: RUNTIME_PROVIDER_KV_PHASE,
143
+ handlerSurface: KV_CAPABILITY_POLICY.handlerSurface,
144
+ compiledHandlerLowering: KV_CAPABILITY_POLICY.compiledHandlerLowering,
145
+ hostEffectExecution: KV_CAPABILITY_POLICY.hostEffectExecution,
146
+ localAdapterProof: KV_CAPABILITY_POLICY.localAdapterProof,
147
+ namespaces: kvCapabilities,
148
+ summary: { namespaces: kvCapabilities.length, execution: 'reserved-effect' }
149
+ }, cwd);
150
+
151
+ const kvProviderMap = normalizeArtifact({
152
+ version: KV_PROVIDER_MAP_VERSION,
153
+ generatedBy,
154
+ phase: RUNTIME_PROVIDER_KV_PHASE,
155
+ provider: provider.kind || null,
156
+ kv: providerMap.kv,
157
+ summary: { namespaces: Object.keys(providerMap.kv || {}).length }
158
+ }, cwd);
159
+
160
+ const apiSurface = normalizeArtifact({
161
+ version: RUNTIME_PROVIDER_API_SURFACE_VERSION,
162
+ generatedBy,
163
+ phase: RUNTIME_PROVIDER_KV_PHASE,
164
+ surfaces: RUNTIME_PROVIDER_API_SURFACES.map((surface) => ({ ...surface })),
165
+ summary: { surfaces: RUNTIME_PROVIDER_API_SURFACES.length }
166
+ }, cwd);
167
+
168
+ const smoke = normalizeArtifact({
169
+ version: RUNTIME_PROVIDER_SMOKE_VERSION,
170
+ generatedBy,
171
+ phase: RUNTIME_PROVIDER_KV_PHASE,
172
+ status: diagnostics.length ? 'error' : 'ok',
173
+ checks: [
174
+ { name: 'provider_kind_valid', status: provider.kind ? 'ok' : 'error' },
175
+ { name: 'fastly_stores_declared', status: provider.kind === 'fastly' && provider.configStore && provider.secretStore ? 'ok' : 'skipped' },
176
+ { name: 'kv_namespaces_declared', status: kvCapabilities.length > 0 ? 'ok' : 'skipped' }
177
+ ],
178
+ summary: { failedChecks: diagnostics.length, kvNamespaces: kvCapabilities.length }
179
+ }, cwd);
180
+
181
+ const markdown = `# Runtime provider / KV contract\n\n- Canonical authoring shape: \`runtime.provider\`\n- Provider: \`${provider.kind || 'unknown'}\`\n- KV namespaces: ${kvCapabilities.map((entry) => `\`${entry.logicalName}\``).join(', ') || 'none'}\n\nKV execution and compiled-handler lowering remain reserved here; the local runtime/adapter store is validated separately.\n`;
182
+
183
+ return {
184
+ artifact: contract,
185
+ resolution,
186
+ configAuthoringShape,
187
+ platformStoreMap,
188
+ kvCapabilityContract,
189
+ kvProviderMap,
190
+ apiSurface,
191
+ smoke,
192
+ files: [{ file: 'generated/host/runtime-provider-kv-contract.md', text: markdown }],
193
+ diagnostics,
194
+ summary: contract.summary
195
+ };
196
+ }
197
+
198
+ function buildKvAdapterIntegration(options = {}) {
199
+ const cwd = options.cwd || process.cwd();
200
+ const generatedBy = options.generatedBy || PACKAGE_VERSION;
201
+ const resolvedConfig = normalizeResolvedConfig(options.resolvedConfig || {});
202
+ const profileName = resolvedConfig.profile || resolvedConfig.selectedProfile || options.profile || 'edge';
203
+ const runtime = plainObject(resolvedConfig.runtime);
204
+ const provider = normalizeRuntimeProvider(runtime) || {};
205
+ const kv = provider.kv || {};
206
+ return {
207
+ integration: normalizeArtifact({
208
+ version: KV_ADAPTER_INTEGRATION_VERSION,
209
+ generatedBy,
210
+ phase: KV_ADAPTER_INTEGRATION_PHASE,
211
+ status: 'ok',
212
+ profile: profileName,
213
+ providerKind: provider.kind || null,
214
+ hostRuntimeModule: 'src/compiled-wasm-host-runtime-kv.js',
215
+ nodeAdapterModule: 'src/compiled-wasm-node-adapter-kv.js',
216
+ kvProviderModule: 'src/kv-provider.js',
217
+ compiledHandlerLowering: KV_CAPABILITY_POLICY.compiledHandlerLowering,
218
+ fastlyKvHostcalls: 'reserved',
219
+ summary: { namespaces: Object.keys(kv).length, localAdapterProof: true }
220
+ }, cwd),
221
+ runtimeMap: normalizeArtifact({
222
+ version: KV_PROVIDER_RUNTIME_MAP_VERSION,
223
+ generatedBy,
224
+ phase: KV_ADAPTER_INTEGRATION_PHASE,
225
+ status: 'ok',
226
+ provider: provider.kind || null,
227
+ namespaces: Object.entries(kv).map(([logicalName, physicalName]) => ({ logicalName, physicalName, storage: 'in-memory-local-proof' })),
228
+ summary: { namespaces: Object.keys(kv).length }
229
+ }, cwd),
230
+ files: [{
231
+ file: 'generated/host/runtime-provider-kv-adapter-proof.md',
232
+ text: `# Runtime provider KV adapter proof\n\nThis proof wires provider-neutral KV namespaces into the local compiled-Wasm host runtime and Node adapter. It does not lower \`ctx.kv(...)\` inside compiled handlers or bind Fastly KV hostcalls.\n`
233
+ }]
234
+ };
235
+ }
236
+
237
+ module.exports = { buildRuntimeProviderKv, buildKvAdapterIntegration };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const ALLOWED_PROVIDER_KINDS = new Set(['fastly', 'local', 'node']);
4
+ const DIAGNOSTICS = {
5
+ PROFILE_RUNTIME_REQUIRED: 'PULSEWASM_PROFILE_RUNTIME_REQUIRED',
6
+ PROVIDER_REQUIRED: 'PULSEWASM_RUNTIME_PROVIDER_REQUIRED',
7
+ PROVIDER_KIND_INVALID: 'PULSEWASM_RUNTIME_PROVIDER_KIND_INVALID',
8
+ FASTLY_CONFIG_STORE_REQUIRED: 'PULSEWASM_FASTLY_CONFIG_STORE_REQUIRED',
9
+ FASTLY_SECRET_STORE_REQUIRED: 'PULSEWASM_FASTLY_SECRET_STORE_REQUIRED',
10
+ KV_LINK_INVALID: 'PULSEWASM_KV_LINK_INVALID',
11
+ KV_NAMESPACE_INVALID: 'PULSEWASM_KV_NAMESPACE_INVALID'
12
+ };
13
+
14
+ function isPlainObject(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+
18
+ function normalizeRuntimeProvider(runtime = {}) {
19
+ if (isPlainObject(runtime.provider)) return runtime.provider;
20
+ const fastly = isPlainObject(runtime.platform) && isPlainObject(runtime.platform.fastly) ? runtime.platform.fastly : undefined;
21
+ if (fastly) {
22
+ return {
23
+ kind: 'fastly',
24
+ configStore: fastly.configStore,
25
+ secretStore: fastly.secretStore,
26
+ kv: fastly.kv || {}
27
+ };
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ function validateRuntimeProvider(profileName, profile) {
33
+ const diagnostics = [];
34
+ const runtime = profile && profile.runtime;
35
+ if (!isPlainObject(runtime)) {
36
+ diagnostics.push({ code: DIAGNOSTICS.PROFILE_RUNTIME_REQUIRED, profile: profileName, message: 'Profile runtime is required.' });
37
+ return diagnostics;
38
+ }
39
+ const provider = normalizeRuntimeProvider(runtime);
40
+ if (!isPlainObject(provider)) {
41
+ diagnostics.push({ code: DIAGNOSTICS.PROVIDER_REQUIRED, profile: profileName, message: 'runtime.provider is required.' });
42
+ return diagnostics;
43
+ }
44
+ if (!ALLOWED_PROVIDER_KINDS.has(provider.kind)) {
45
+ diagnostics.push({ code: DIAGNOSTICS.PROVIDER_KIND_INVALID, profile: profileName, message: 'runtime.provider.kind must be one of fastly, local, node.' });
46
+ }
47
+ if (provider.kind === 'fastly') {
48
+ if (typeof provider.configStore !== 'string' || !provider.configStore) {
49
+ diagnostics.push({ code: DIAGNOSTICS.FASTLY_CONFIG_STORE_REQUIRED, profile: profileName, message: 'Fastly provider requires configStore.' });
50
+ }
51
+ if (typeof provider.secretStore !== 'string' || !provider.secretStore) {
52
+ diagnostics.push({ code: DIAGNOSTICS.FASTLY_SECRET_STORE_REQUIRED, profile: profileName, message: 'Fastly provider requires secretStore.' });
53
+ }
54
+ }
55
+ if (provider.kv !== undefined) {
56
+ if (!isPlainObject(provider.kv)) {
57
+ diagnostics.push({ code: DIAGNOSTICS.KV_NAMESPACE_INVALID, profile: profileName, message: 'runtime.provider.kv must be an object map.' });
58
+ } else {
59
+ for (const [logicalName, link] of Object.entries(provider.kv)) {
60
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(logicalName)) {
61
+ diagnostics.push({ code: DIAGNOSTICS.KV_NAMESPACE_INVALID, profile: profileName, logicalName, message: 'KV logical names must be alphanumeric with _ or -.' });
62
+ }
63
+ if (typeof link !== 'string' || !link) {
64
+ diagnostics.push({ code: DIAGNOSTICS.KV_LINK_INVALID, profile: profileName, logicalName, message: 'KV links must be non-empty strings.' });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return diagnostics;
70
+ }
71
+
72
+ function extractProviderMap(profileName, profile) {
73
+ const runtime = profile.runtime;
74
+ const provider = normalizeRuntimeProvider(runtime) || {};
75
+ return {
76
+ profile: profileName,
77
+ provider: provider.kind || null,
78
+ stores: provider.kind === 'fastly' ? {
79
+ configStore: provider.configStore,
80
+ secretStore: provider.secretStore
81
+ } : {},
82
+ kv: provider.kv || {},
83
+ authoringShape: 'runtime.provider',
84
+ legacyPlatformFallback: !isPlainObject(runtime.provider) && isPlainObject(runtime.platform)
85
+ };
86
+ }
87
+
88
+ function extractKvCapabilities(profileName, profile) {
89
+ const runtime = profile.runtime || {};
90
+ const provider = normalizeRuntimeProvider(runtime) || {};
91
+ const kv = provider.kv || {};
92
+ return Object.entries(kv).map(([name, link]) => ({
93
+ profile: profileName,
94
+ logicalName: name,
95
+ link,
96
+ provider: provider.kind || null,
97
+ execution: 'reserved-effect',
98
+ localAdapterProof: 'runtime-owned in-memory provider',
99
+ handlerSurface: `ctx.kv("${name}")`
100
+ }));
101
+ }
102
+
103
+ module.exports = {
104
+ DIAGNOSTICS,
105
+ validateRuntimeProvider,
106
+ extractProviderMap,
107
+ extractKvCapabilities,
108
+ normalizeRuntimeProvider
109
+ };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const { fastlyConfigDefault } = require('./config-schema.js');
4
+
5
+ const FASTLY_PROVIDER_API_VERSION = 'pulse.provider-fastly-api.v2';
6
+
7
+ function stringMap(input = {}) {
8
+ return Object.freeze(Object.fromEntries(Object.entries(input || {}).map(([key, value]) => [String(key), String(value)])));
9
+ }
10
+
11
+ function gripAuthentication(input) {
12
+ if (input === undefined) return undefined;
13
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
14
+ throw new TypeError('fastly.grip.authentication must be an object.');
15
+ }
16
+ const scheme = String(input.scheme || 'bearer').toLowerCase();
17
+ if (scheme !== 'bearer') throw new TypeError('fastly.grip.authentication.scheme must be bearer.');
18
+ return Object.freeze({
19
+ scheme: 'bearer',
20
+ secretRef: String(input.secretRef || '')
21
+ });
22
+ }
23
+
24
+ function gripBindings(input = {}) {
25
+ const value = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
26
+ return Object.freeze({
27
+ fanoutBackend: value.fanoutBackend === undefined ? undefined : String(value.fanoutBackend),
28
+ publishEndpoint: value.publishEndpoint === undefined ? undefined : String(value.publishEndpoint),
29
+ publishUrl: value.publishUrl === undefined ? undefined : String(value.publishUrl),
30
+ publishBackend: value.publishBackend === undefined ? undefined : String(value.publishBackend),
31
+ authentication: gripAuthentication(value.authentication),
32
+ directHold: value.directHold === undefined ? fastlyConfigDefault('fastly.grip.directHold') : value.directHold !== false
33
+ });
34
+ }
35
+
36
+ function fastly(options = {}) {
37
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('fastly() requires an options object.');
38
+ const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
39
+ return Object.freeze({
40
+ kind: 'fastly',
41
+ version: FASTLY_PROVIDER_API_VERSION,
42
+ bindings: Object.freeze({
43
+ configStore: String(options.configStore ?? fastlyConfigDefault('fastly.configStore')),
44
+ secretStore: String(options.secretStore ?? fastlyConfigDefault('fastly.secretStore')),
45
+ kv: stringMap(options.kv),
46
+ backends: stringMap(options.backends),
47
+ ...(normalizedGrip ? { grip: normalizedGrip } : {}),
48
+ dynamicBackends: options.dynamicBackends === undefined ? fastlyConfigDefault('fastly.dynamicBackends') : options.dynamicBackends === true
49
+ }),
50
+ build: Object.freeze({
51
+ name: String(options.name ?? fastlyConfigDefault('fastly.name')),
52
+ description: String(options.description ?? fastlyConfigDefault('fastly.description')),
53
+ authors: Object.freeze((Array.isArray(options.authors) ? options.authors : fastlyConfigDefault('fastly.authors')).map(String)),
54
+ language: 'other'
55
+ }),
56
+ local: Object.freeze({
57
+ networkFetch: Boolean(options.local?.networkFetch ?? fastlyConfigDefault('fastly.local.networkFetch'))
58
+ }),
59
+ providerSpecificUserland: false
60
+ });
61
+ }
62
+
63
+ module.exports = Object.freeze({ FASTLY_PROVIDER_API_VERSION, fastly });
@@ -0,0 +1,13 @@
1
+ export interface PulseFastlyConfigSchemaRule {
2
+ readonly kind: string;
3
+ readonly default?: unknown;
4
+ }
5
+
6
+ export interface PulseFastlyConfigSchema {
7
+ readonly schemaVersion: 'pulse.provider-fastly-config-schema.v1';
8
+ readonly fields: Readonly<Record<string, PulseFastlyConfigSchemaRule>>;
9
+ }
10
+
11
+ export const FASTLY_CONFIG_SCHEMA: PulseFastlyConfigSchema;
12
+ export function fastlyConfigRule(path: string): PulseFastlyConfigSchemaRule;
13
+ export function fastlyConfigDefault(path: string): unknown;
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const FASTLY_CONFIG_SCHEMA = require('./config-schema.json');
4
+
5
+ function clone(value) {
6
+ if (Array.isArray(value)) return value.map(clone);
7
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clone(entry)]));
8
+ return value;
9
+ }
10
+
11
+ function fastlyConfigRule(path) {
12
+ const rule = FASTLY_CONFIG_SCHEMA.fields[path];
13
+ if (!rule) throw new Error(`Unknown Fastly config schema field ${path}`);
14
+ return rule;
15
+ }
16
+
17
+ function fastlyConfigDefault(path) {
18
+ const rule = fastlyConfigRule(path);
19
+ if (!Object.prototype.hasOwnProperty.call(rule, 'default')) throw new Error(`Fastly config schema field ${path} has no default`);
20
+ return clone(rule.default);
21
+ }
22
+
23
+ module.exports = Object.freeze({
24
+ FASTLY_CONFIG_SCHEMA,
25
+ fastlyConfigRule,
26
+ fastlyConfigDefault
27
+ });
@@ -0,0 +1,73 @@
1
+ {
2
+ "schemaVersion": "pulse.provider-fastly-config-schema.v1",
3
+ "fields": {
4
+ "fastly.configStore": {
5
+ "kind": "string",
6
+ "default": "pulse_config"
7
+ },
8
+ "fastly.secretStore": {
9
+ "kind": "string",
10
+ "default": "pulse_secrets"
11
+ },
12
+ "fastly.kv": {
13
+ "kind": "string-map",
14
+ "default": {}
15
+ },
16
+ "fastly.backends": {
17
+ "kind": "string-map",
18
+ "default": {}
19
+ },
20
+ "fastly.dynamicBackends": {
21
+ "kind": "boolean",
22
+ "default": false
23
+ },
24
+ "fastly.grip": {
25
+ "kind": "object"
26
+ },
27
+ "fastly.grip.fanoutBackend": {
28
+ "kind": "optional-string"
29
+ },
30
+ "fastly.grip.publishEndpoint": {
31
+ "kind": "optional-string"
32
+ },
33
+ "fastly.grip.publishUrl": {
34
+ "kind": "optional-string"
35
+ },
36
+ "fastly.grip.publishBackend": {
37
+ "kind": "optional-string"
38
+ },
39
+ "fastly.grip.authentication": {
40
+ "kind": "object"
41
+ },
42
+ "fastly.grip.authentication.scheme": {
43
+ "kind": "optional-string"
44
+ },
45
+ "fastly.grip.authentication.secretRef": {
46
+ "kind": "optional-string"
47
+ },
48
+ "fastly.grip.directHold": {
49
+ "kind": "boolean",
50
+ "default": true
51
+ },
52
+ "fastly.name": {
53
+ "kind": "string",
54
+ "default": "pulse-app"
55
+ },
56
+ "fastly.description": {
57
+ "kind": "string",
58
+ "default": "Pulse application"
59
+ },
60
+ "fastly.authors": {
61
+ "kind": "string-array",
62
+ "default": []
63
+ },
64
+ "fastly.local": {
65
+ "kind": "object",
66
+ "default": {}
67
+ },
68
+ "fastly.local.networkFetch": {
69
+ "kind": "boolean",
70
+ "default": false
71
+ }
72
+ }
73
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./config-api.js');
package/src/index.ts ADDED
@@ -0,0 +1,118 @@
1
+ import FASTLY_CONFIG_SCHEMA from './config-schema.json';
2
+
3
+ type FastlySchemaRule = Readonly<{ kind: string; default?: unknown }>;
4
+ const fastlySchemaFields = FASTLY_CONFIG_SCHEMA.fields as Readonly<Record<string, FastlySchemaRule>>;
5
+ const fastlyDefaults = Object.freeze(Object.fromEntries(Object.entries(fastlySchemaFields).filter(([, rule]) => Object.prototype.hasOwnProperty.call(rule, 'default')).map(([key, rule]) => [key, rule.default]))) as Readonly<Record<string, unknown>>;
6
+
7
+ export const FASTLY_PROVIDER_API_VERSION = 'pulse.provider-fastly-api.v2' as const;
8
+
9
+ export interface PulseFastlyGripOptions {
10
+ readonly fanoutBackend?: string;
11
+ readonly publishEndpoint?: string;
12
+ readonly publishUrl?: string;
13
+ readonly publishBackend?: string;
14
+ readonly authentication?: {
15
+ readonly scheme?: 'bearer';
16
+ readonly secretRef: string;
17
+ };
18
+ readonly directHold?: boolean;
19
+ }
20
+
21
+ export interface PulseFastlyLocalOptions {
22
+ readonly networkFetch?: boolean;
23
+ }
24
+
25
+ export interface PulseFastlyProviderOptions {
26
+ readonly configStore?: string;
27
+ readonly secretStore?: string;
28
+ readonly kv?: Readonly<Record<string, string>>;
29
+ readonly backends?: Readonly<Record<string, string>>;
30
+ readonly dynamicBackends?: boolean;
31
+ readonly grip?: PulseFastlyGripOptions;
32
+ readonly name?: string;
33
+ readonly description?: string;
34
+ readonly authors?: readonly string[];
35
+ readonly local?: PulseFastlyLocalOptions;
36
+ }
37
+
38
+ export interface PulseFastlyProviderConfig {
39
+ readonly kind: 'fastly';
40
+ readonly version: typeof FASTLY_PROVIDER_API_VERSION;
41
+ readonly bindings: {
42
+ readonly configStore: string;
43
+ readonly secretStore: string;
44
+ readonly kv: Readonly<Record<string, string>>;
45
+ readonly backends: Readonly<Record<string, string>>;
46
+ readonly grip?: {
47
+ readonly fanoutBackend?: string;
48
+ readonly publishEndpoint?: string;
49
+ readonly publishUrl?: string;
50
+ readonly publishBackend?: string;
51
+ readonly authentication?: {
52
+ readonly scheme: 'bearer';
53
+ readonly secretRef: string;
54
+ };
55
+ readonly directHold: boolean;
56
+ };
57
+ readonly dynamicBackends: boolean;
58
+ };
59
+ readonly build: {
60
+ readonly name: string;
61
+ readonly description: string;
62
+ readonly authors: readonly string[];
63
+ readonly language: 'other';
64
+ };
65
+ readonly local: {
66
+ readonly networkFetch: boolean;
67
+ };
68
+ readonly providerSpecificUserland: false;
69
+ }
70
+
71
+ function stringMap(input: Readonly<Record<string, string>> | undefined): Readonly<Record<string, string>> {
72
+ return Object.freeze(Object.fromEntries(Object.entries(input ?? {}).map(([key, value]) => [String(key), String(value)])));
73
+ }
74
+
75
+ function gripAuthentication(input: PulseFastlyGripOptions['authentication'] | undefined): NonNullable<PulseFastlyProviderConfig['bindings']['grip']>['authentication'] {
76
+ if (input === undefined) return undefined;
77
+ const scheme = String(input.scheme ?? 'bearer').toLowerCase();
78
+ if (scheme !== 'bearer') throw new TypeError('fastly.grip.authentication.scheme must be bearer.');
79
+ return Object.freeze({
80
+ scheme: 'bearer' as const,
81
+ secretRef: String(input.secretRef),
82
+ });
83
+ }
84
+
85
+ function gripBindings(input: PulseFastlyGripOptions | undefined): NonNullable<PulseFastlyProviderConfig['bindings']['grip']> {
86
+ return Object.freeze({
87
+ fanoutBackend: input?.fanoutBackend === undefined ? undefined : String(input.fanoutBackend),
88
+ publishEndpoint: input?.publishEndpoint === undefined ? undefined : String(input.publishEndpoint),
89
+ publishUrl: input?.publishUrl === undefined ? undefined : String(input.publishUrl),
90
+ publishBackend: input?.publishBackend === undefined ? undefined : String(input.publishBackend),
91
+ authentication: gripAuthentication(input?.authentication),
92
+ directHold: input?.directHold === undefined ? Boolean(fastlyDefaults['fastly.grip.directHold']) : input.directHold !== false
93
+ });
94
+ }
95
+
96
+ export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyProviderConfig {
97
+ const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
98
+ return Object.freeze({
99
+ kind: 'fastly',
100
+ version: FASTLY_PROVIDER_API_VERSION,
101
+ bindings: Object.freeze({
102
+ configStore: String(options.configStore ?? fastlyDefaults['fastly.configStore']),
103
+ secretStore: String(options.secretStore ?? fastlyDefaults['fastly.secretStore']),
104
+ kv: stringMap(options.kv),
105
+ backends: stringMap(options.backends),
106
+ ...(normalizedGrip ? { grip: normalizedGrip } : {}),
107
+ dynamicBackends: options.dynamicBackends === undefined ? Boolean(fastlyDefaults['fastly.dynamicBackends']) : options.dynamicBackends === true
108
+ }),
109
+ build: Object.freeze({
110
+ name: String(options.name ?? fastlyDefaults['fastly.name']),
111
+ description: String(options.description ?? fastlyDefaults['fastly.description']),
112
+ authors: Object.freeze((options.authors ?? (Array.isArray(fastlyDefaults['fastly.authors']) ? fastlyDefaults['fastly.authors'] : [])).map(String)),
113
+ language: 'other'
114
+ }),
115
+ local: Object.freeze({ networkFetch: Boolean(options.local?.networkFetch ?? fastlyDefaults['fastly.local.networkFetch']) }),
116
+ providerSpecificUserland: false
117
+ });
118
+ }