@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const { normalizeFastlyLogEndpoint } = require('../src/toolchain/logging.js') as { normalizeFastlyLogEndpoint(value?: string): string | undefined };
|
|
2
|
+
const { normalizeBodyForwarding } = require('../src/toolchain/body-forwarding.js') as { normalizeBodyForwarding(value: PulseFastlyBodyForwardingOptions | undefined, maxDurationMs?: number): PulseFastlyBodyForwardingOptions | undefined };
|
|
3
|
+
const { normalizeFastlyMaxWasmBytes } = require('../src/build/wasm-budget.js') as { normalizeFastlyMaxWasmBytes(value?: number): number };
|
|
4
|
+
const { normalizeRequestDuration } = require('@pulse-compute/runtime/host') as { normalizeRequestDuration(value?: number): number | undefined };
|
|
5
|
+
import FASTLY_CONFIG_SCHEMA from './config-schema.json';
|
|
6
|
+
|
|
7
|
+
type FastlySchemaRule = Readonly<{ kind: string; default?: unknown }>;
|
|
8
|
+
const fastlySchemaFields = FASTLY_CONFIG_SCHEMA.fields as Readonly<Record<string, FastlySchemaRule>>;
|
|
9
|
+
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>>;
|
|
10
|
+
|
|
11
|
+
export const FASTLY_PROVIDER_API_VERSION = 'pulse.provider-fastly-api.v2' as const;
|
|
12
|
+
|
|
13
|
+
export interface PulseFastlyGripOptions {
|
|
14
|
+
readonly fanoutBackend?: string;
|
|
15
|
+
readonly publishEndpoint?: string;
|
|
16
|
+
readonly publishUrl?: string;
|
|
17
|
+
readonly publishBackend?: string;
|
|
18
|
+
readonly authentication?: {
|
|
19
|
+
readonly scheme?: 'bearer';
|
|
20
|
+
readonly secretRef: string;
|
|
21
|
+
};
|
|
22
|
+
readonly directHold?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PulseFastlyLocalOptions {
|
|
26
|
+
readonly networkFetch?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PulseFastlyBodyForwardingOptions {
|
|
30
|
+
readonly requestMaxBytes: number;
|
|
31
|
+
readonly responseMaxBytes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PulseFastlyProviderOptions {
|
|
35
|
+
/** Inclusive final Native Wasm byte budget; default 4 MiB. */
|
|
36
|
+
readonly maxWasmBytes?: number;
|
|
37
|
+
readonly maxDurationMs?: number;
|
|
38
|
+
/** Experimental Native metered pump; final close hands delivery to Fastly. */
|
|
39
|
+
readonly bodyForwarding?: PulseFastlyBodyForwardingOptions;
|
|
40
|
+
/** Named production log stream; omitted preserves stdout. */
|
|
41
|
+
readonly logEndpoint?: string;
|
|
42
|
+
readonly configStore?: string;
|
|
43
|
+
readonly secretStore?: string;
|
|
44
|
+
readonly kv?: Readonly<Record<string, string>>;
|
|
45
|
+
readonly backends?: Readonly<Record<string, string>>;
|
|
46
|
+
readonly dynamicBackends?: boolean;
|
|
47
|
+
readonly grip?: PulseFastlyGripOptions;
|
|
48
|
+
readonly name?: string;
|
|
49
|
+
readonly description?: string;
|
|
50
|
+
readonly authors?: readonly string[];
|
|
51
|
+
readonly local?: PulseFastlyLocalOptions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PulseFastlyProviderConfig {
|
|
55
|
+
readonly maxDurationMs?: number;
|
|
56
|
+
/** Experimental Native metered pump; final close hands delivery to Fastly. */
|
|
57
|
+
readonly bodyForwarding?: PulseFastlyBodyForwardingOptions;
|
|
58
|
+
readonly kind: 'fastly';
|
|
59
|
+
readonly version: typeof FASTLY_PROVIDER_API_VERSION;
|
|
60
|
+
readonly bindings: {
|
|
61
|
+
readonly logEndpoint?: string;
|
|
62
|
+
readonly configStore: string;
|
|
63
|
+
readonly secretStore: string;
|
|
64
|
+
readonly kv: Readonly<Record<string, string>>;
|
|
65
|
+
readonly backends: Readonly<Record<string, string>>;
|
|
66
|
+
readonly grip?: {
|
|
67
|
+
readonly fanoutBackend?: string;
|
|
68
|
+
readonly publishEndpoint?: string;
|
|
69
|
+
readonly publishUrl?: string;
|
|
70
|
+
readonly publishBackend?: string;
|
|
71
|
+
readonly authentication?: {
|
|
72
|
+
readonly scheme: 'bearer';
|
|
73
|
+
readonly secretRef: string;
|
|
74
|
+
};
|
|
75
|
+
readonly directHold: boolean;
|
|
76
|
+
};
|
|
77
|
+
readonly dynamicBackends: boolean;
|
|
78
|
+
};
|
|
79
|
+
readonly build: {
|
|
80
|
+
readonly maxWasmBytes: number;
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly description: string;
|
|
83
|
+
readonly authors: readonly string[];
|
|
84
|
+
readonly language: 'other';
|
|
85
|
+
};
|
|
86
|
+
readonly local: {
|
|
87
|
+
readonly networkFetch: boolean;
|
|
88
|
+
};
|
|
89
|
+
readonly providerSpecificUserland: false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stringMap(input: Readonly<Record<string, string>> | undefined): Readonly<Record<string, string>> {
|
|
93
|
+
return Object.freeze(Object.fromEntries(Object.entries(input ?? {}).map(([key, value]) => [String(key), String(value)])));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function gripAuthentication(input: PulseFastlyGripOptions['authentication'] | undefined): NonNullable<PulseFastlyProviderConfig['bindings']['grip']>['authentication'] {
|
|
97
|
+
if (input === undefined) return undefined;
|
|
98
|
+
const scheme = String(input.scheme ?? 'bearer').toLowerCase();
|
|
99
|
+
if (scheme !== 'bearer') throw new TypeError('fastly.grip.authentication.scheme must be bearer.');
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
scheme: 'bearer' as const,
|
|
102
|
+
secretRef: String(input.secretRef),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function gripBindings(input: PulseFastlyGripOptions | undefined): NonNullable<PulseFastlyProviderConfig['bindings']['grip']> {
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
fanoutBackend: input?.fanoutBackend === undefined ? undefined : String(input.fanoutBackend),
|
|
109
|
+
publishEndpoint: input?.publishEndpoint === undefined ? undefined : String(input.publishEndpoint),
|
|
110
|
+
publishUrl: input?.publishUrl === undefined ? undefined : String(input.publishUrl),
|
|
111
|
+
publishBackend: input?.publishBackend === undefined ? undefined : String(input.publishBackend),
|
|
112
|
+
authentication: gripAuthentication(input?.authentication),
|
|
113
|
+
directHold: input?.directHold === undefined ? Boolean(fastlyDefaults['fastly.grip.directHold']) : input.directHold !== false
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function fastly(options: PulseFastlyProviderOptions = {}): PulseFastlyProviderConfig {
|
|
118
|
+
const normalizedGrip = options.grip === undefined ? undefined : gripBindings(options.grip);
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
kind: 'fastly',
|
|
121
|
+
maxDurationMs: normalizeRequestDuration(options.maxDurationMs),
|
|
122
|
+
bodyForwarding: normalizeBodyForwarding(options.bodyForwarding, options.maxDurationMs),
|
|
123
|
+
version: FASTLY_PROVIDER_API_VERSION,
|
|
124
|
+
bindings: Object.freeze({
|
|
125
|
+
...(options.logEndpoint === undefined ? {} : { logEndpoint: normalizeFastlyLogEndpoint(options.logEndpoint) }),
|
|
126
|
+
configStore: String(options.configStore ?? fastlyDefaults['fastly.configStore']),
|
|
127
|
+
secretStore: String(options.secretStore ?? fastlyDefaults['fastly.secretStore']),
|
|
128
|
+
kv: stringMap(options.kv),
|
|
129
|
+
backends: stringMap(options.backends),
|
|
130
|
+
...(normalizedGrip ? { grip: normalizedGrip } : {}),
|
|
131
|
+
dynamicBackends: options.dynamicBackends === undefined ? Boolean(fastlyDefaults['fastly.dynamicBackends']) : options.dynamicBackends === true
|
|
132
|
+
}),
|
|
133
|
+
build: Object.freeze({
|
|
134
|
+
maxWasmBytes: normalizeFastlyMaxWasmBytes(options.maxWasmBytes),
|
|
135
|
+
name: String(options.name ?? fastlyDefaults['fastly.name']),
|
|
136
|
+
description: String(options.description ?? fastlyDefaults['fastly.description']),
|
|
137
|
+
authors: Object.freeze((options.authors ?? (Array.isArray(fastlyDefaults['fastly.authors']) ? fastlyDefaults['fastly.authors'] : [])).map(String)),
|
|
138
|
+
language: 'other'
|
|
139
|
+
}),
|
|
140
|
+
local: Object.freeze({ networkFetch: Boolean(options.local?.networkFetch ?? fastlyDefaults['fastly.local.networkFetch']) }),
|
|
141
|
+
providerSpecificUserland: false
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
const { fastlyJavascriptProviderError, isPulseRuntimeError } = require('./errors.js');
|
|
5
|
+
|
|
6
|
+
const FASTLY_JAVASCRIPT_ASSETS_ADAPTER_VERSION = 'pulse.fastly-javascript-assets-adapter.v1';
|
|
7
|
+
|
|
8
|
+
function contentTypeForKey(key) {
|
|
9
|
+
const extension = String(key).toLowerCase().split('.').pop();
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
css: 'text/css; charset=utf-8',
|
|
12
|
+
html: 'text/html; charset=utf-8',
|
|
13
|
+
js: 'text/javascript; charset=utf-8',
|
|
14
|
+
json: 'application/json; charset=utf-8',
|
|
15
|
+
mjs: 'text/javascript; charset=utf-8',
|
|
16
|
+
svg: 'image/svg+xml',
|
|
17
|
+
txt: 'text/plain; charset=utf-8',
|
|
18
|
+
webp: 'image/webp',
|
|
19
|
+
png: 'image/png',
|
|
20
|
+
jpg: 'image/jpeg',
|
|
21
|
+
jpeg: 'image/jpeg',
|
|
22
|
+
gif: 'image/gif',
|
|
23
|
+
ico: 'image/x-icon'
|
|
24
|
+
})[extension] || 'application/octet-stream';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function responseHeaders(payload) {
|
|
28
|
+
const headers = new Headers(payload && payload.headers);
|
|
29
|
+
if (!headers.has('content-type')) headers.set('content-type', contentTypeForKey(payload && payload.key));
|
|
30
|
+
if (payload && payload.cacheControl !== undefined) headers.set('cache-control', String(payload.cacheControl));
|
|
31
|
+
return headers;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createFastlyJavascriptAssetsLookup(options = {}) {
|
|
35
|
+
const apis = options.apis || {};
|
|
36
|
+
const bindings = options.bindings && typeof options.bindings === 'object' ? options.bindings : {};
|
|
37
|
+
const kvBindings = bindings.kv && typeof bindings.kv === 'object' ? bindings.kv : {};
|
|
38
|
+
const stores = new Map();
|
|
39
|
+
|
|
40
|
+
function storeFor(logicalStore) {
|
|
41
|
+
const store = typeof logicalStore === 'string' ? logicalStore.trim() : '';
|
|
42
|
+
const resource = store && typeof kvBindings[store] === 'string' ? kvBindings[store].trim() : '';
|
|
43
|
+
if (!store || !resource) {
|
|
44
|
+
throw fastlyJavascriptProviderError(
|
|
45
|
+
'PULSE_FASTLY_ASSETS_BINDING_REQUIRED',
|
|
46
|
+
'Fastly JavaScript Assets requires a logical store-to-KV mapping.',
|
|
47
|
+
{ store, configuration: store ? `bindings.kv.${store}` : 'bindings.kv' }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (!stores.has(resource)) {
|
|
51
|
+
if (typeof apis.KVStore !== 'function') {
|
|
52
|
+
throw fastlyJavascriptProviderError(
|
|
53
|
+
'PULSE_FASTLY_JAVASCRIPT_API_UNAVAILABLE',
|
|
54
|
+
'Fastly JavaScript Assets requires the KVStore API.',
|
|
55
|
+
{ api: 'KVStore' }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
try { stores.set(resource, new apis.KVStore(resource)); }
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw fastlyJavascriptProviderError(
|
|
61
|
+
'PULSE_FASTLY_ASSETS_STORE_OPEN_FAILED',
|
|
62
|
+
'Fastly JavaScript could not open the configured Assets KV Store.',
|
|
63
|
+
{ store, resource },
|
|
64
|
+
error
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze({ store, resource, value: stores.get(resource) });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return async function fastlyJavascriptAssetsLookup(payload = {}) {
|
|
72
|
+
const opened = storeFor(payload.store);
|
|
73
|
+
const key = typeof payload.key === 'string' ? payload.key : '';
|
|
74
|
+
if (!key) {
|
|
75
|
+
throw fastlyJavascriptProviderError(
|
|
76
|
+
'PULSE_FASTLY_ASSETS_KEY_INVALID',
|
|
77
|
+
'Fastly JavaScript Assets requires a non-empty asset key.',
|
|
78
|
+
{ store: opened.store }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
let entry;
|
|
82
|
+
try { entry = await opened.value.get(key); }
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (isPulseRuntimeError(error)) throw error;
|
|
85
|
+
throw fastlyJavascriptProviderError(
|
|
86
|
+
'PULSE_FASTLY_ASSETS_LOOKUP_FAILED',
|
|
87
|
+
'Fastly JavaScript Assets lookup failed.',
|
|
88
|
+
{ store: opened.store, key },
|
|
89
|
+
error
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (entry === null || entry === undefined) {
|
|
93
|
+
return runtimeHost.markOpaqueResponse(new Response(null, {
|
|
94
|
+
status: 404,
|
|
95
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
if (!entry || !('body' in Object(entry))) {
|
|
99
|
+
throw fastlyJavascriptProviderError(
|
|
100
|
+
'PULSE_FASTLY_ASSETS_ENTRY_INVALID',
|
|
101
|
+
'Fastly JavaScript Assets received an invalid KV entry.',
|
|
102
|
+
{ store: opened.store, key }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const body = entry.body;
|
|
106
|
+
const method = String(payload.method || 'GET').toUpperCase();
|
|
107
|
+
const headers = responseHeaders(payload);
|
|
108
|
+
if (method === 'HEAD') {
|
|
109
|
+
if (body && typeof body.cancel === 'function') {
|
|
110
|
+
try { await body.cancel(); } catch (_) { /* Best-effort host stream cleanup. */ }
|
|
111
|
+
}
|
|
112
|
+
return runtimeHost.markOpaqueResponse(new Response(null, { status: 200, headers }));
|
|
113
|
+
}
|
|
114
|
+
return runtimeHost.markOpaqueResponse(new Response(body, { status: 200, headers }));
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = Object.freeze({
|
|
119
|
+
FASTLY_JAVASCRIPT_ASSETS_ADAPTER_VERSION,
|
|
120
|
+
contentTypeForKey,
|
|
121
|
+
createFastlyJavascriptAssetsLookup
|
|
122
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
const { fastlyJavascriptProviderError, isPulseRuntimeError } = require('./errors.js');
|
|
5
|
+
|
|
6
|
+
const FASTLY_JAVASCRIPT_BINDINGS_ADAPTER_VERSION = 'pulse.fastly-javascript-bindings-adapter.v1';
|
|
7
|
+
|
|
8
|
+
function requireConstructor(apis, name) {
|
|
9
|
+
const value = apis && apis[name];
|
|
10
|
+
if (typeof value !== 'function') {
|
|
11
|
+
throw fastlyJavascriptProviderError(
|
|
12
|
+
'PULSE_FASTLY_JAVASCRIPT_API_UNAVAILABLE',
|
|
13
|
+
`Fastly JavaScript ${name} capability is unavailable.`,
|
|
14
|
+
{ api: name }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requireResourceName(value, field) {
|
|
21
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
throw fastlyJavascriptProviderError(
|
|
24
|
+
'PULSE_FASTLY_JAVASCRIPT_BINDING_REQUIRED',
|
|
25
|
+
`Fastly JavaScript ${field} must name a configured resource.`,
|
|
26
|
+
{ configuration: field }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function providerFailure(error, code, message, detail) {
|
|
33
|
+
if (isPulseRuntimeError(error)) throw error;
|
|
34
|
+
throw fastlyJavascriptProviderError(code, message, detail, error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createFastlyJavascriptBindingCapabilities(options = {}) {
|
|
38
|
+
const apis = options.apis || {};
|
|
39
|
+
const bindings = options.bindings && typeof options.bindings === 'object' ? options.bindings : {};
|
|
40
|
+
const kvBindings = bindings.kv && typeof bindings.kv === 'object' ? bindings.kv : {};
|
|
41
|
+
const limits = {
|
|
42
|
+
maxBindingNameBytes: options.maxBindingNameBytes,
|
|
43
|
+
maxBindingValueBytes: options.maxBindingValueBytes,
|
|
44
|
+
maxKvNamespaceBytes: options.maxKvNamespaceBytes,
|
|
45
|
+
maxKvKeyBytes: options.maxKvKeyBytes,
|
|
46
|
+
maxKvValueBytes: options.maxKvValueBytes,
|
|
47
|
+
maxKvValueDepth: options.maxKvValueDepth,
|
|
48
|
+
maxKvValueEntries: options.maxKvValueEntries
|
|
49
|
+
};
|
|
50
|
+
let configStore;
|
|
51
|
+
let secretStore;
|
|
52
|
+
const kvStores = new Map();
|
|
53
|
+
|
|
54
|
+
function openConfigStore() {
|
|
55
|
+
if (!configStore) {
|
|
56
|
+
const ConfigStore = requireConstructor(apis, 'ConfigStore');
|
|
57
|
+
const resource = requireResourceName(bindings.configStore, 'bindings.configStore');
|
|
58
|
+
try { configStore = new ConfigStore(resource); }
|
|
59
|
+
catch (error) {
|
|
60
|
+
providerFailure(
|
|
61
|
+
error,
|
|
62
|
+
'PULSE_FASTLY_CONFIG_STORE_OPEN_FAILED',
|
|
63
|
+
'Fastly JavaScript could not open the configured Config Store.',
|
|
64
|
+
{ configuration: 'bindings.configStore', resource }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return configStore;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function openSecretStore() {
|
|
72
|
+
if (!secretStore) {
|
|
73
|
+
const SecretStore = requireConstructor(apis, 'SecretStore');
|
|
74
|
+
const resource = requireResourceName(bindings.secretStore, 'bindings.secretStore');
|
|
75
|
+
try { secretStore = new SecretStore(resource); }
|
|
76
|
+
catch (error) {
|
|
77
|
+
providerFailure(
|
|
78
|
+
error,
|
|
79
|
+
'PULSE_FASTLY_SECRET_STORE_OPEN_FAILED',
|
|
80
|
+
'Fastly JavaScript could not open the configured Secret Store.',
|
|
81
|
+
{ configuration: 'bindings.secretStore', resource }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return secretStore;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function physicalKvStore(namespaceInput) {
|
|
89
|
+
const namespace = runtimeHost.normalizeKvNamespace(namespaceInput, limits);
|
|
90
|
+
const resource = typeof kvBindings[namespace] === 'string' ? kvBindings[namespace].trim() : '';
|
|
91
|
+
if (!resource) {
|
|
92
|
+
throw fastlyJavascriptProviderError(
|
|
93
|
+
'PULSE_FASTLY_KV_BINDING_REQUIRED',
|
|
94
|
+
'Fastly JavaScript KV requires a logical namespace mapping.',
|
|
95
|
+
{ namespace, configuration: `bindings.kv.${namespace}` }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!kvStores.has(resource)) {
|
|
99
|
+
const KVStore = requireConstructor(apis, 'KVStore');
|
|
100
|
+
try { kvStores.set(resource, new KVStore(resource)); }
|
|
101
|
+
catch (error) {
|
|
102
|
+
providerFailure(
|
|
103
|
+
error,
|
|
104
|
+
'PULSE_FASTLY_KV_STORE_OPEN_FAILED',
|
|
105
|
+
'Fastly JavaScript could not open a configured KV Store.',
|
|
106
|
+
{ namespace, resource }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({ namespace, resource, store: kvStores.get(resource) });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
version: FASTLY_JAVASCRIPT_BINDINGS_ADAPTER_VERSION,
|
|
115
|
+
config(nameInput) {
|
|
116
|
+
const name = runtimeHost.normalizeBindingName('config', nameInput, limits);
|
|
117
|
+
try {
|
|
118
|
+
const value = openConfigStore().get(name);
|
|
119
|
+
return value === null ? undefined : value;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
providerFailure(
|
|
122
|
+
error,
|
|
123
|
+
'PULSE_FASTLY_CONFIG_GET_FAILED',
|
|
124
|
+
'Fastly JavaScript Config Store lookup failed.',
|
|
125
|
+
{ name }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async secret(nameInput) {
|
|
130
|
+
const name = runtimeHost.normalizeBindingName('secret', nameInput, limits);
|
|
131
|
+
let entry;
|
|
132
|
+
try { entry = await openSecretStore().get(name); }
|
|
133
|
+
catch (error) {
|
|
134
|
+
providerFailure(
|
|
135
|
+
error,
|
|
136
|
+
'PULSE_FASTLY_SECRET_GET_FAILED',
|
|
137
|
+
'Fastly JavaScript Secret Store lookup failed.',
|
|
138
|
+
{ name }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (entry === null || entry === undefined) return undefined;
|
|
142
|
+
try {
|
|
143
|
+
if (!entry || typeof entry.plaintext !== 'function') {
|
|
144
|
+
throw fastlyJavascriptProviderError(
|
|
145
|
+
'PULSE_FASTLY_SECRET_ENTRY_INVALID',
|
|
146
|
+
'Fastly JavaScript Secret Store returned an invalid entry.',
|
|
147
|
+
{ name }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return entry.plaintext();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
providerFailure(
|
|
153
|
+
error,
|
|
154
|
+
'PULSE_FASTLY_SECRET_PLAINTEXT_FAILED',
|
|
155
|
+
'Fastly JavaScript could not read a Secret Store entry.',
|
|
156
|
+
{ name }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
kv(namespaceInput) {
|
|
161
|
+
const opened = physicalKvStore(namespaceInput);
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
async get(keyInput) {
|
|
164
|
+
const key = runtimeHost.normalizeKvKey(keyInput, limits);
|
|
165
|
+
let entry;
|
|
166
|
+
try { entry = await opened.store.get(key); }
|
|
167
|
+
catch (error) {
|
|
168
|
+
providerFailure(
|
|
169
|
+
error,
|
|
170
|
+
'PULSE_FASTLY_KV_GET_FAILED',
|
|
171
|
+
'Fastly JavaScript KV lookup failed.',
|
|
172
|
+
{ namespace: opened.namespace, key }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (entry === null || entry === undefined) return undefined;
|
|
176
|
+
try {
|
|
177
|
+
if (!entry || typeof entry.json !== 'function') {
|
|
178
|
+
throw fastlyJavascriptProviderError(
|
|
179
|
+
'PULSE_FASTLY_KV_ENTRY_INVALID',
|
|
180
|
+
'Fastly JavaScript KV returned an invalid entry.',
|
|
181
|
+
{ namespace: opened.namespace, key }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return await entry.json();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
providerFailure(
|
|
187
|
+
error,
|
|
188
|
+
'PULSE_FASTLY_KV_VALUE_INVALID',
|
|
189
|
+
'Fastly JavaScript KV value is not valid Pulse JSON data.',
|
|
190
|
+
{ namespace: opened.namespace, key }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async put(keyInput, value) {
|
|
195
|
+
const key = runtimeHost.normalizeKvKey(keyInput, limits);
|
|
196
|
+
let body;
|
|
197
|
+
try { body = JSON.stringify(value); }
|
|
198
|
+
catch (error) {
|
|
199
|
+
providerFailure(
|
|
200
|
+
error,
|
|
201
|
+
'PULSE_FASTLY_KV_VALUE_INVALID',
|
|
202
|
+
'Fastly JavaScript KV value is not valid Pulse JSON data.',
|
|
203
|
+
{ namespace: opened.namespace, key }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
try { await opened.store.put(key, body); }
|
|
207
|
+
catch (error) {
|
|
208
|
+
providerFailure(
|
|
209
|
+
error,
|
|
210
|
+
'PULSE_FASTLY_KV_PUT_FAILED',
|
|
211
|
+
'Fastly JavaScript KV write failed.',
|
|
212
|
+
{ namespace: opened.namespace, key }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function withFastlyBindingCapabilities(capabilities, options = {}) {
|
|
223
|
+
const base = capabilities && typeof capabilities === 'object' ? capabilities : {};
|
|
224
|
+
const configured = options.bindingCapabilities || createFastlyJavascriptBindingCapabilities(options);
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
...base,
|
|
227
|
+
...(typeof base.config === 'function' ? {} : { config: configured.config }),
|
|
228
|
+
...(typeof base.secret === 'function' ? {} : { secret: configured.secret }),
|
|
229
|
+
...(typeof base.kv === 'function' ? {} : { kv: configured.kv })
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = Object.freeze({
|
|
234
|
+
FASTLY_JAVASCRIPT_BINDINGS_ADAPTER_VERSION,
|
|
235
|
+
createFastlyJavascriptBindingCapabilities,
|
|
236
|
+
withFastlyBindingCapabilities
|
|
237
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
|
|
5
|
+
const FASTLY_JAVASCRIPT_PROVIDER_ERROR_VERSION = 'pulse.fastly-javascript-provider-error.v1';
|
|
6
|
+
|
|
7
|
+
function fastlyJavascriptProviderError(code, message, detail = {}, cause) {
|
|
8
|
+
const safeDetail = {
|
|
9
|
+
...detail,
|
|
10
|
+
...(cause && typeof cause.name === 'string' ? { causeName: cause.name } : {})
|
|
11
|
+
};
|
|
12
|
+
return new runtimeHost.PulseRuntimeContractError(code, message, {
|
|
13
|
+
detail: Object.freeze(safeDetail)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isPulseRuntimeError(error) {
|
|
18
|
+
return error instanceof runtimeHost.PulseRuntimeContractError
|
|
19
|
+
|| Boolean(error && typeof error.code === 'string' && error.code.startsWith('PULSE_'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = Object.freeze({
|
|
23
|
+
FASTLY_JAVASCRIPT_PROVIDER_ERROR_VERSION,
|
|
24
|
+
fastlyJavascriptProviderError,
|
|
25
|
+
isPulseRuntimeError
|
|
26
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
|
+
const { fastlyJavascriptProviderError, isPulseRuntimeError } = require('./errors.js');
|
|
5
|
+
|
|
6
|
+
const FASTLY_JAVASCRIPT_FETCH_ADAPTER_VERSION = 'pulse.fastly-javascript-fetch-adapter.v1';
|
|
7
|
+
|
|
8
|
+
function normalizeOrigin(url) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(String(url));
|
|
11
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) throw new TypeError('unsupported protocol');
|
|
12
|
+
return parsed.origin;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw fastlyJavascriptProviderError(
|
|
15
|
+
'PULSE_FASTLY_FETCH_URL_INVALID',
|
|
16
|
+
'Fastly JavaScript fetch requires an absolute HTTP or HTTPS URL.',
|
|
17
|
+
{ url: String(url) },
|
|
18
|
+
error
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createFastlyJavascriptFetchCapability(options = {}) {
|
|
24
|
+
const implementation = options.fetchImplementation;
|
|
25
|
+
const bindings = options.bindings && typeof options.bindings === 'object' ? options.bindings : {};
|
|
26
|
+
const backends = bindings.backends && typeof bindings.backends === 'object' ? bindings.backends : {};
|
|
27
|
+
const dynamicBackends = bindings.dynamicBackends === true;
|
|
28
|
+
|
|
29
|
+
return async function fastlyJavascriptFetch(url, init = {}, execution = {}) {
|
|
30
|
+
if (typeof implementation !== 'function') {
|
|
31
|
+
throw fastlyJavascriptProviderError(
|
|
32
|
+
'PULSE_FASTLY_FETCH_IMPLEMENTATION_UNAVAILABLE',
|
|
33
|
+
'Fastly JavaScript fetch is unavailable.',
|
|
34
|
+
{ capability: 'fetch' }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const origin = normalizeOrigin(url);
|
|
38
|
+
const configuredBackend = typeof backends[origin] === 'string' ? backends[origin].trim() : '';
|
|
39
|
+
if (!configuredBackend && !dynamicBackends) {
|
|
40
|
+
throw fastlyJavascriptProviderError(
|
|
41
|
+
'PULSE_FASTLY_FETCH_BACKEND_REQUIRED',
|
|
42
|
+
'Fastly JavaScript fetch requires an origin-to-backend mapping unless dynamic backends are enabled.',
|
|
43
|
+
{ origin, configuration: `bindings.backends.${origin}`, dynamicBackends: false }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const requestInit = {
|
|
48
|
+
method: init.method,
|
|
49
|
+
headers: init.headers,
|
|
50
|
+
body: init.body,
|
|
51
|
+
signal: execution.signal || init.signal,
|
|
52
|
+
redirect: 'follow',
|
|
53
|
+
...(configuredBackend ? { backend: configuredBackend } : {})
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const response = await implementation(String(url), requestInit);
|
|
57
|
+
if (!(response instanceof Response) && !runtimeHost.isPulseFetchResponse(response)) {
|
|
58
|
+
throw fastlyJavascriptProviderError(
|
|
59
|
+
'PULSE_FASTLY_FETCH_RESPONSE_INVALID',
|
|
60
|
+
'Fastly JavaScript fetch must resolve to a Web Response.',
|
|
61
|
+
{ origin, type: typeof response }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return response;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isPulseRuntimeError(error)) throw error;
|
|
67
|
+
if (requestInit.signal && requestInit.signal.aborted) {
|
|
68
|
+
const reason = requestInit.signal.reason;
|
|
69
|
+
if (reason instanceof Error) throw reason;
|
|
70
|
+
}
|
|
71
|
+
throw fastlyJavascriptProviderError(
|
|
72
|
+
'PULSE_FASTLY_FETCH_NETWORK',
|
|
73
|
+
'Fastly JavaScript fetch failed.',
|
|
74
|
+
{ origin, method: String(init.method || 'GET').toUpperCase() },
|
|
75
|
+
error
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function withFastlyFetchCapability(capabilities, options = {}) {
|
|
82
|
+
const base = capabilities && typeof capabilities === 'object' ? capabilities : {};
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
...base,
|
|
85
|
+
...(typeof base.fetch === 'function' ? {} : { fetch: createFastlyJavascriptFetchCapability(options) })
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = Object.freeze({
|
|
90
|
+
FASTLY_JAVASCRIPT_FETCH_ADAPTER_VERSION,
|
|
91
|
+
createFastlyJavascriptFetchCapability,
|
|
92
|
+
withFastlyFetchCapability
|
|
93
|
+
});
|