@pulse-compute/provider-node 1.0.0-beta.1 → 1.0.0-beta.2
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 +4 -3
- package/package.json +9 -8
- package/src/capabilities.js +9 -0
- package/src/config/s3.js +20 -0
- package/src/javascript/bindings-adapter.js +5 -35
- package/src/javascript/node-adapter.js +3 -0
- package/src/javascript/package-effects.js +6 -0
- package/src/javascript/runtime-host.js +5 -0
- package/src/javascript/s3.js +21 -0
- package/src/javascript/target-support-policy.js +3 -1
- package/src/javascript/target.js +1 -1
- package/src/javascript/test-runtime.js +1 -0
- package/src/native/target.js +1 -1
- package/src/runtime/canonical-api-runtime.js +14 -42
- package/src/runtime/conditional-kv.js +93 -0
- package/src/runtime/index.js +1 -0
- package/src/runtime/s3-reader.js +131 -0
- package/src/toolchain.js +16 -5
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
> **Install directly:** No for application projects; select provider: node through the CLI.<br>
|
|
7
7
|
> **Supported entry points:** `@pulse-compute/provider-node/toolchain`<br>
|
|
8
8
|
> **Stability:** The listed built-in toolchain entry is synchronized with the Beta bootstrap contract; other canonical Node provider interfaces remain internal.<br>
|
|
9
|
+
> **npm:** [`@pulse-compute/provider-node`](https://www.npmjs.com/package/@pulse-compute/provider-node)<br>
|
|
9
10
|
> **Canonical replacement:** `provider: 'node' through @pulse-compute/cli`<br>
|
|
10
|
-
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
11
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.2/packages/implementation-packages/)
|
|
11
12
|
>
|
|
12
|
-
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.
|
|
13
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.2` package policy.
|
|
13
14
|
<!-- pulse-package-status:end -->
|
|
14
15
|
|
|
15
16
|
Canonical Node provider implementation used internally when a project selects `provider: 'node'`.
|
|
@@ -59,4 +60,4 @@ The package exposes focused compiler and runtime modules for the synchronized
|
|
|
59
60
|
repository, including `runtime/canonical-api-runtime`. Those exports are
|
|
60
61
|
implementation interfaces, not application-author APIs.
|
|
61
62
|
|
|
62
|
-
See [Contracts and providers](https://pulsecompute.io/v1.0.0-beta.
|
|
63
|
+
See [Contracts and providers](https://pulsecompute.io/v1.0.0-beta.2/concepts/contracts-and-providers/) and [Implementation packages](https://pulsecompute.io/v1.0.0-beta.2/packages/implementation-packages/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/provider-node",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -42,12 +42,13 @@
|
|
|
42
42
|
"./javascript/source-package": "./src/javascript/source-package.js"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@pulse-compute/crypto": "1.0.0-beta.
|
|
46
|
-
"@pulse-compute/jwt": "1.0.0-beta.
|
|
47
|
-
"@pulse-compute/runtime": "1.0.0-beta.
|
|
48
|
-
"@pulse-compute/
|
|
49
|
-
"@pulse-compute/wasm-
|
|
50
|
-
"@pulse-compute/wasm-
|
|
45
|
+
"@pulse-compute/crypto": "1.0.0-beta.2",
|
|
46
|
+
"@pulse-compute/jwt": "1.0.0-beta.2",
|
|
47
|
+
"@pulse-compute/runtime": "1.0.0-beta.2",
|
|
48
|
+
"@pulse-compute/s3": "1.0.0-beta.2",
|
|
49
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.2",
|
|
50
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.2",
|
|
51
|
+
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.2"
|
|
51
52
|
},
|
|
52
53
|
"publishConfig": {
|
|
53
54
|
"access": "public"
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
64
65
|
"directory": "packages/provider-node"
|
|
65
66
|
},
|
|
66
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
67
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.2/packages/implementation-packages/",
|
|
67
68
|
"bugs": {
|
|
68
69
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
69
70
|
}
|
package/src/capabilities.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const NODE_JWT_VERIFY_CAPABILITY = 'jwt.verify';
|
|
4
4
|
|
|
5
5
|
const NODE_CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
|
|
6
|
+
's3.head', 's3.getText', 's3.putText',
|
|
6
7
|
'request',
|
|
7
8
|
'response.json',
|
|
8
9
|
'response.text',
|
|
@@ -12,6 +13,7 @@ const NODE_CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
|
|
|
12
13
|
'secret.get',
|
|
13
14
|
'kv.get',
|
|
14
15
|
'kv.put',
|
|
16
|
+
'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap',
|
|
15
17
|
'event.ingress',
|
|
16
18
|
'event.emit',
|
|
17
19
|
'assets.lookup',
|
|
@@ -30,8 +32,14 @@ const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
|
|
|
30
32
|
'secret.get': 'node.runtime.secret.get',
|
|
31
33
|
'kv.get': 'node.runtime.kv.get',
|
|
32
34
|
'kv.put': 'node.runtime.kv.put',
|
|
35
|
+
'kv.getVersioned': 'node.runtime.kv.getVersioned',
|
|
36
|
+
'kv.insertIfAbsent': 'node.runtime.kv.insertIfAbsent',
|
|
37
|
+
'kv.compareAndSwap': 'node.runtime.kv.compareAndSwap',
|
|
33
38
|
'event.ingress': 'node.event.reference.ingress',
|
|
34
39
|
'event.emit': 'node.event.reference.accept',
|
|
40
|
+
's3.head': 'node.native.s3.head',
|
|
41
|
+
's3.getText': 'node.native.s3.getText',
|
|
42
|
+
's3.putText': 'node.native.s3.putText',
|
|
35
43
|
'assets.lookup': 'node.runtime.assets.lookup',
|
|
36
44
|
'grip.broadcast': 'node.http.grip.publish',
|
|
37
45
|
[NODE_JWT_VERIFY_CAPABILITY]: 'node.native.jwt.verify',
|
|
@@ -39,6 +47,7 @@ const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
|
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
const NODE_NATIVE_TARGET_CAPABILITIES = Object.freeze([
|
|
50
|
+
's3.head', 's3.getText', 's3.putText',
|
|
42
51
|
NODE_JWT_VERIFY_CAPABILITY,
|
|
43
52
|
'jwt.verify.hs256',
|
|
44
53
|
'jwt.verify.es256',
|
package/src/config/s3.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { normalizeBinding, namePattern } = require('@pulse-compute/s3/provider');
|
|
3
|
+
function normalizeNodeS3(input = {}) {
|
|
4
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || ![null, Object.prototype].includes(Object.getPrototypeOf(input))) throw new TypeError('Node 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('Node S3 bindings require data properties.');
|
|
8
|
+
}
|
|
9
|
+
return Object.freeze(Object.fromEntries(Object.entries(input).map(([name, value]) => {
|
|
10
|
+
if (!namePattern.test(name)) throw new TypeError('Node S3 logical binding name is invalid.');
|
|
11
|
+
return [name, normalizeBinding(value)];
|
|
12
|
+
})));
|
|
13
|
+
}
|
|
14
|
+
function validateNodeS3Operations(metadata, bindings) {
|
|
15
|
+
for (const effect of metadata.providerOperations || []) {
|
|
16
|
+
if (!['s3.head', 's3.getText', 's3.putText'].includes(effect.kind)) continue;
|
|
17
|
+
if (!bindings.s3 || !Object.hasOwn(bindings.s3, effect.resource && effect.resource.binding)) throw new TypeError('The required Node S3 binding is missing.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
module.exports = { normalizeNodeS3, validateNodeS3Operations };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { createNodeKvReference } = require('../runtime/conditional-kv.js');
|
|
3
4
|
const runtimeHost = require('@pulse-compute/runtime/host');
|
|
4
5
|
|
|
5
6
|
const NODE_JAVASCRIPT_BINDINGS_ADAPTER_VERSION = 'pulse.node-javascript-bindings-adapter.v1';
|
|
@@ -31,7 +32,7 @@ function recordEntries(input, label = 'binding record') {
|
|
|
31
32
|
function createNodeJavascriptBindingCapabilities(options = {}) {
|
|
32
33
|
const config = new Map();
|
|
33
34
|
const secrets = new Map();
|
|
34
|
-
const
|
|
35
|
+
const reference = options.kvReference || createNodeKvReference(options);
|
|
35
36
|
|
|
36
37
|
for (const [rawName, rawValue] of recordEntries(options.config, 'config')) {
|
|
37
38
|
const name = runtimeHost.normalizeBindingName('config', rawName, options);
|
|
@@ -41,22 +42,6 @@ function createNodeJavascriptBindingCapabilities(options = {}) {
|
|
|
41
42
|
const name = runtimeHost.normalizeBindingName('secret', rawName, options);
|
|
42
43
|
secrets.set(name, runtimeHost.normalizeBindingValue('secret.get', name, rawValue, options));
|
|
43
44
|
}
|
|
44
|
-
for (const [rawNamespace, values] of recordEntries(options.kv, 'KV namespaces')) {
|
|
45
|
-
const namespace = runtimeHost.normalizeKvNamespace(rawNamespace, options);
|
|
46
|
-
const entries = new Map();
|
|
47
|
-
for (const [rawKey, value] of recordEntries(values, `KV namespace ${JSON.stringify(namespace)}`)) {
|
|
48
|
-
const key = runtimeHost.normalizeKvKey(rawKey, options);
|
|
49
|
-
entries.set(key, runtimeHost.cloneKvValue(value, options));
|
|
50
|
-
}
|
|
51
|
-
stores.set(namespace, entries);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function storeFor(namespaceInput) {
|
|
55
|
-
const namespace = runtimeHost.normalizeKvNamespace(namespaceInput, options);
|
|
56
|
-
if (!stores.has(namespace)) stores.set(namespace, new Map());
|
|
57
|
-
return Object.freeze({ namespace, store: stores.get(namespace) });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
45
|
return Object.freeze({
|
|
61
46
|
version: NODE_JAVASCRIPT_BINDINGS_ADAPTER_VERSION,
|
|
62
47
|
config(nameInput) {
|
|
@@ -67,23 +52,8 @@ function createNodeJavascriptBindingCapabilities(options = {}) {
|
|
|
67
52
|
const name = runtimeHost.normalizeBindingName('secret', nameInput, options);
|
|
68
53
|
return secrets.has(name) ? secrets.get(name) : undefined;
|
|
69
54
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return Object.freeze({
|
|
73
|
-
get(keyInput) {
|
|
74
|
-
const key = runtimeHost.normalizeKvKey(keyInput, options);
|
|
75
|
-
return runtimeHost.cloneKvValue(store.has(key) ? store.get(key) : undefined, {
|
|
76
|
-
...options,
|
|
77
|
-
allowUndefined: true
|
|
78
|
-
});
|
|
79
|
-
},
|
|
80
|
-
put(keyInput, value) {
|
|
81
|
-
const key = runtimeHost.normalizeKvKey(keyInput, options);
|
|
82
|
-
store.set(key, runtimeHost.cloneKvValue(value, options));
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
55
|
+
prepareConditionalKv: reference.prepareConditionalKv,
|
|
56
|
+
kv: reference.kv
|
|
87
57
|
});
|
|
88
58
|
}
|
|
89
59
|
|
|
@@ -97,7 +67,7 @@ function withNodeBindingCapabilities(capabilities, options = {}) {
|
|
|
97
67
|
const configured = options.bindings || createNodeJavascriptBindingCapabilities(options);
|
|
98
68
|
if (needsConfig) output.config = configured.config;
|
|
99
69
|
if (needsSecret) output.secret = configured.secret;
|
|
100
|
-
if (needsKv) output.kv = configured.kv;
|
|
70
|
+
if (needsKv) { output.kv = configured.kv; output.prepareConditionalKv = configured.prepareConditionalKv; }
|
|
101
71
|
return Object.freeze(output);
|
|
102
72
|
}
|
|
103
73
|
|
|
@@ -179,6 +179,7 @@ function createNodeJavascriptHandler(application, options = {}) {
|
|
|
179
179
|
config: options.config,
|
|
180
180
|
secrets: options.secrets,
|
|
181
181
|
kv: options.kv,
|
|
182
|
+
kvReference: options.kvReference,
|
|
182
183
|
maxBindingNameBytes: options.maxBindingNameBytes,
|
|
183
184
|
maxBindingValueBytes: options.maxBindingValueBytes,
|
|
184
185
|
maxKvNamespaceBytes: options.maxKvNamespaceBytes,
|
|
@@ -203,6 +204,7 @@ function createNodeJavascriptHandler(application, options = {}) {
|
|
|
203
204
|
const kv = hasDynamicBindings ? await resolveRequestOption(options.kv, requestContext) : options.kv;
|
|
204
205
|
const bindings = staticBindings || createNodeJavascriptBindingCapabilities({
|
|
205
206
|
config, secrets, kv,
|
|
207
|
+
kvReference: options.kvReference,
|
|
206
208
|
maxBindingNameBytes: options.maxBindingNameBytes,
|
|
207
209
|
maxBindingValueBytes: options.maxBindingValueBytes,
|
|
208
210
|
maxKvNamespaceBytes: options.maxKvNamespaceBytes,
|
|
@@ -234,6 +236,7 @@ function createNodeJavascriptHandler(application, options = {}) {
|
|
|
234
236
|
assets: options.assets,
|
|
235
237
|
grip: options.grip,
|
|
236
238
|
maxEffects: options.maxEffects,
|
|
239
|
+
kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
|
|
237
240
|
maxRequestBodyBytes: options.maxRequestBodyBytes ?? options.maxBodyBytes,
|
|
238
241
|
maxFetchBodyBytes: options.maxFetchBodyBytes,
|
|
239
242
|
maxStructuredBodyBytes: options.maxStructuredBodyBytes,
|
|
@@ -31,9 +31,14 @@ function createNodeJavascriptPackageEffectCapabilities(options = {}) {
|
|
|
31
31
|
const assetsLookup = options.assetsLookup || (options.assets && options.assets.lookup);
|
|
32
32
|
const gripBroadcast = options.gripBroadcast || (options.grip && options.grip.broadcast);
|
|
33
33
|
const jwtVerify = options.jwtVerify || (options.jwt && options.jwt.verify);
|
|
34
|
+
const s3 = options.s3;
|
|
34
35
|
return Object.freeze({
|
|
35
36
|
version: NODE_JAVASCRIPT_PACKAGE_EFFECTS_VERSION,
|
|
36
37
|
async effect(effect, execution) {
|
|
38
|
+
if (effect && effect.contractId === 'pulse.s3') {
|
|
39
|
+
if (typeof s3 !== 'function') return unavailable(effect);
|
|
40
|
+
return s3(effect, execution);
|
|
41
|
+
}
|
|
37
42
|
if (effect && effect.contractId === 'pulse.assets' && effect.operation === 'lookup') {
|
|
38
43
|
if (typeof assetsLookup !== 'function') return unavailable(effect);
|
|
39
44
|
return assetsLookup(effect.payload, execution);
|
|
@@ -61,6 +66,7 @@ function withNodePackageEffectCapabilities(capabilities, options = {}) {
|
|
|
61
66
|
effect.contractId === 'pulse.assets'
|
|
62
67
|
|| effect.contractId === 'pulse.grip'
|
|
63
68
|
|| effect.contractId === 'pulse.jwt'
|
|
69
|
+
|| effect.contractId === 'pulse.s3'
|
|
64
70
|
)) {
|
|
65
71
|
return packageCapabilities.effect(effect, execution);
|
|
66
72
|
}
|
|
@@ -10,6 +10,7 @@ const { createNodeJavascriptFixtureFetch, withNodeFetchCapability } = require('.
|
|
|
10
10
|
const { createNodeJavascriptAssetsLookup } = require('./assets-adapter.js');
|
|
11
11
|
const { createNodeJavascriptGripBroadcast } = require('./grip-broadcast.js');
|
|
12
12
|
const { createNodeJavascriptJwtVerify } = require('./jwt-verifier.js');
|
|
13
|
+
const { createNodeJavascriptS3 } = require('./s3.js');
|
|
13
14
|
const { createNodeJavascriptPackageEffectCapabilities, withNodePackageEffectCapabilities } = require('./package-effects.js');
|
|
14
15
|
const { NODE_JAVASCRIPT_TARGET_DESCRIPTOR } = require('./target.js');
|
|
15
16
|
|
|
@@ -34,6 +35,7 @@ function nodeJavascriptProviderCapabilities(options) {
|
|
|
34
35
|
...(Object.prototype.hasOwnProperty.call(options, 'config') ? { config: options.config } : {}),
|
|
35
36
|
...(Object.prototype.hasOwnProperty.call(options, 'secrets') ? { secrets: options.secrets } : {}),
|
|
36
37
|
...(Object.prototype.hasOwnProperty.call(options, 'kv') ? { kv: options.kv } : {}),
|
|
38
|
+
kvReference: options.kvReference, kvInstanceId: options.kvInstanceId, kvGenerationStart: options.kvGenerationStart, kvHooks: options.kvHooks,
|
|
37
39
|
maxBindingNameBytes: options.maxBindingNameBytes,
|
|
38
40
|
maxBindingValueBytes: options.maxBindingValueBytes,
|
|
39
41
|
maxKvNamespaceBytes: options.maxKvNamespaceBytes,
|
|
@@ -65,6 +67,7 @@ function nodeJavascriptProviderCapabilities(options) {
|
|
|
65
67
|
assetsLookup: configuredAssetsLookup,
|
|
66
68
|
gripBroadcast: configuredGripBroadcast,
|
|
67
69
|
jwtVerify: configuredJwtVerify,
|
|
70
|
+
s3: createNodeJavascriptS3(options, bindingCapabilities.secret),
|
|
68
71
|
assets: options.assets,
|
|
69
72
|
grip: options.grip
|
|
70
73
|
}
|
|
@@ -87,6 +90,7 @@ async function executeNodeJavascriptApplication(application, request, options =
|
|
|
87
90
|
application: options.application,
|
|
88
91
|
requestHeaders: options.requestHeaders,
|
|
89
92
|
signal: options.signal,
|
|
93
|
+
kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
|
|
90
94
|
maxEffects: options.maxEffects,
|
|
91
95
|
maxRequestBodyBytes: options.maxRequestBodyBytes,
|
|
92
96
|
maxFetchBodyBytes: options.maxFetchBodyBytes,
|
|
@@ -118,6 +122,7 @@ async function executeNodeJavascriptEvent(application, frame, options = {}) {
|
|
|
118
122
|
effectAdapter: options.effectAdapter,
|
|
119
123
|
application: options.application,
|
|
120
124
|
signal: options.signal,
|
|
125
|
+
kvClock: options.kvClock, deadlineMonotonicMs: options.deadlineMonotonicMs,
|
|
121
126
|
maxEffects: options.maxEffects,
|
|
122
127
|
maxBindingNameBytes: options.maxBindingNameBytes,
|
|
123
128
|
maxBindingValueBytes: options.maxBindingValueBytes,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { bindJavascriptDigestMac } = require('@pulse-compute/crypto/provider');
|
|
3
|
+
const { normalizeNodeS3 } = require('../config/s3.js');
|
|
4
|
+
const { readS3 } = require('../runtime/s3-reader.js');
|
|
5
|
+
|
|
6
|
+
function createNodeJavascriptS3(options, secretLookup) {
|
|
7
|
+
const s3 = normalizeNodeS3(options.s3 || options.bindings && options.bindings.s3);
|
|
8
|
+
return (effect, execution = {}) => {
|
|
9
|
+
// This provider target explicitly selects Crypto's Web Crypto realization.
|
|
10
|
+
// Native execution supplies its own Wasm exports to the same Node transport.
|
|
11
|
+
let selected;
|
|
12
|
+
try { selected = bindJavascriptDigestMac(['SHA-256', 'HMAC-SHA256']); }
|
|
13
|
+
catch { selected = null; }
|
|
14
|
+
return readS3(effect, {
|
|
15
|
+
s3, signal: execution.signal, registerRedactionValue: execution.registerRedactionValue,
|
|
16
|
+
fetchImplementation: options.fetchImplementation, cryptoTarget: 'javascript',
|
|
17
|
+
cryptoVerifier: selected, cryptoRealization: selected && selected.realization
|
|
18
|
+
}, (name) => secretLookup(name, execution));
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
module.exports = { createNodeJavascriptS3 };
|
|
@@ -10,6 +10,7 @@ function decision(id, status, reasonId, owner, required = true) {
|
|
|
10
10
|
|
|
11
11
|
function classifyNodeJavascriptCapability(id, options = {}) {
|
|
12
12
|
const capability = String(id);
|
|
13
|
+
if (['s3.head', 's3.getText', 's3.putText'].includes(capability)) return decision(capability, 'eligible', 'node-s3-bounded-origin-transport', 'provider-node');
|
|
13
14
|
if (isJavascriptCoreCapability(capability)) {
|
|
14
15
|
return decision(capability, 'eligible', 'node-router-context-parity', 'provider-node');
|
|
15
16
|
}
|
|
@@ -75,6 +76,7 @@ function classifyNodeJavascriptCapability(id, options = {}) {
|
|
|
75
76
|
|
|
76
77
|
function classifyNodeJavascriptProviderRequirement(id, compilerCapabilities, options = {}) {
|
|
77
78
|
const requirement = String(id);
|
|
79
|
+
if (['s3.head', 's3.getText', 's3.putText'].includes(requirement)) return decision(requirement, 'eligible', 'node-s3-bounded-origin-transport', 'provider-node');
|
|
78
80
|
if (requirement === 'request') {
|
|
79
81
|
const bodyRequired = compilerCapabilities.some((entry) => entry === 'request.json' || entry.startsWith('request.body'));
|
|
80
82
|
return bodyRequired
|
|
@@ -87,7 +89,7 @@ function classifyNodeJavascriptProviderRequirement(id, compilerCapabilities, opt
|
|
|
87
89
|
if (requirement === 'fetch' || requirement === 'opaque.pass-through' || ['body', 'headers', 'result'].includes(requirement)) {
|
|
88
90
|
return decision(requirement, 'eligible', 'javascript-fetch-projection-realization', 'runtime');
|
|
89
91
|
}
|
|
90
|
-
if (['config.get', 'secret.get', 'kv.get', 'kv.put'].includes(requirement)) {
|
|
92
|
+
if (['config.get', 'secret.get', 'kv.get', 'kv.put', 'kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(requirement)) {
|
|
91
93
|
return options.bindingsRedaction === true
|
|
92
94
|
? decision(requirement, 'eligible', 'javascript-binding-redaction-implemented', 'provider-node')
|
|
93
95
|
: decision(requirement, 'pending', 'javascript-binding-effect-pending', 'provider-node');
|
package/src/javascript/target.js
CHANGED
|
@@ -17,7 +17,7 @@ const NODE_JAVASCRIPT_TARGET_DESCRIPTOR = normalizeJavascriptTargetDescriptor({
|
|
|
17
17
|
},
|
|
18
18
|
crypto: defineCryptoTargetCapabilities({
|
|
19
19
|
target: 'javascript',
|
|
20
|
-
algorithms: [{
|
|
20
|
+
algorithms: [...['SHA-256', 'HMAC-SHA256'].map((algorithm) => ({ algorithm, realization: 'runtime-builtin', implemented: true, status: 'implemented-o3' })), {
|
|
21
21
|
algorithm: 'HS256',
|
|
22
22
|
realization: 'runtime-builtin',
|
|
23
23
|
implemented: true,
|
|
@@ -52,6 +52,7 @@ async function executeNodeJavascriptTestCase(application, testCase, options = {}
|
|
|
52
52
|
jwtCaptureWallClock: options.jwtCaptureWallClock,
|
|
53
53
|
config: testCase.config || {},
|
|
54
54
|
secrets: testCase.secrets || {},
|
|
55
|
+
s3: options.s3 || options.bindings && options.bindings.s3,
|
|
55
56
|
kv: testCase.kv || {},
|
|
56
57
|
application: options.application,
|
|
57
58
|
requestHeaders: adapted.headerPairs,
|
package/src/native/target.js
CHANGED
|
@@ -60,7 +60,7 @@ const NODE_NATIVE_TARGET_DESCRIPTOR = Object.freeze({
|
|
|
60
60
|
automaticFallback: false,
|
|
61
61
|
crypto: defineCryptoTargetCapabilities({
|
|
62
62
|
target: 'native',
|
|
63
|
-
algorithms: [{
|
|
63
|
+
algorithms: [...['SHA-256', 'HMAC-SHA256'].map((algorithm) => ({ algorithm, realization: 'guest-source:pulse-hmac-as', implemented: true, status: 'implemented-o2' })), {
|
|
64
64
|
algorithm: 'HS256',
|
|
65
65
|
realization: 'guest-source:pulse-hmac-as',
|
|
66
66
|
implemented: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { createNodeKvReference } = require('./conditional-kv.js');
|
|
2
3
|
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
const { Readable } = require('node:stream');
|
|
@@ -361,6 +362,7 @@ function effectToProviderEffect(normalized) {
|
|
|
361
362
|
|
|
362
363
|
function createNodeProviderAdapter(baseOptions = {}) {
|
|
363
364
|
const kvExecutions = new Map();
|
|
365
|
+
const sharedKv = baseOptions.kvReference || createNodeKvReference(baseOptions);
|
|
364
366
|
const verifyNativeJwt = createNodeNativeJwtVerify({
|
|
365
367
|
captureWallClock: baseOptions.captureJwtWallClock,
|
|
366
368
|
secretLookup(name, executionOptions) {
|
|
@@ -449,29 +451,14 @@ function createNodeProviderAdapter(baseOptions = {}) {
|
|
|
449
451
|
};
|
|
450
452
|
}
|
|
451
453
|
|
|
452
|
-
function
|
|
454
|
+
function kvReferenceFor(executionOptions) {
|
|
455
|
+
if (executionOptions.kvReference) return executionOptions.kvReference;
|
|
456
|
+
if (!Object.prototype.hasOwnProperty.call(executionOptions, 'kv') || executionOptions.kv === baseOptions.kv) return sharedKv;
|
|
453
457
|
const executionId = String(executionOptions.executionId || 'default');
|
|
454
|
-
if (!kvExecutions.has(executionId)) {
|
|
455
|
-
const stores = new Map();
|
|
456
|
-
const limits = bindingOptions(executionOptions);
|
|
457
|
-
for (const [rawName, values] of recordEntries(valuesFor(executionOptions, 'kv'), 'KV namespace record')) {
|
|
458
|
-
const name = portableRuntimeHost.normalizeKvNamespace(rawName, limits);
|
|
459
|
-
const entries = new Map();
|
|
460
|
-
for (const [rawKey, value] of recordEntries(values, `KV namespace ${JSON.stringify(name)}`)) {
|
|
461
|
-
const key = portableRuntimeHost.normalizeKvKey(rawKey, limits);
|
|
462
|
-
entries.set(key, portableRuntimeHost.cloneKvValue(value, {
|
|
463
|
-
operation: 'seed',
|
|
464
|
-
maxBytes: limits.maxKvValueBytes,
|
|
465
|
-
maxDepth: limits.maxKvValueDepth,
|
|
466
|
-
maxEntries: limits.maxKvValueEntries
|
|
467
|
-
}));
|
|
468
|
-
}
|
|
469
|
-
stores.set(name, entries);
|
|
470
|
-
}
|
|
471
|
-
kvExecutions.set(executionId, stores);
|
|
472
|
-
}
|
|
458
|
+
if (!kvExecutions.has(executionId)) kvExecutions.set(executionId, createNodeKvReference({ ...baseOptions, ...executionOptions, ...bindingOptions(executionOptions) }));
|
|
473
459
|
return kvExecutions.get(executionId);
|
|
474
460
|
}
|
|
461
|
+
function kvStoresFor(executionOptions) { return kvReferenceFor(executionOptions).legacyStores(); }
|
|
475
462
|
|
|
476
463
|
async function dispatchFetch(normalized, executionOptions = {}) {
|
|
477
464
|
const fetchFixtures = normalizeFetchFixtures(executionOptions.fetches || baseOptions.fetches);
|
|
@@ -505,6 +492,7 @@ function createNodeProviderAdapter(baseOptions = {}) {
|
|
|
505
492
|
}
|
|
506
493
|
|
|
507
494
|
return Object.freeze({
|
|
495
|
+
prepareConditionalKv(effect, execution) { return kvReferenceFor(execution).prepareConditionalKv(effect, execution); },
|
|
508
496
|
id: 'node',
|
|
509
497
|
version: CANONICAL_NODE_RUNTIME_VERSION,
|
|
510
498
|
createCapabilities() { return {}; },
|
|
@@ -513,6 +501,9 @@ function createNodeProviderAdapter(baseOptions = {}) {
|
|
|
513
501
|
},
|
|
514
502
|
dispatchFetch,
|
|
515
503
|
async dispatchEffect(effect, executionOptions = {}) {
|
|
504
|
+
if (['s3.head', 's3.getText', 's3.putText'].includes(effect.kind)) {
|
|
505
|
+
return require('./s3-reader.js').readS3(effect, { ...baseOptions, ...executionOptions }, (name) => bindingValue(executionOptions, 'secrets', name));
|
|
506
|
+
}
|
|
516
507
|
if (effect.kind === 'fetch') return dispatchFetch(effect, executionOptions);
|
|
517
508
|
if (effect.kind === 'jwt.verify') return verifyNativeJwt(effect, executionOptions);
|
|
518
509
|
if (effect.kind === 'grip.broadcast') {
|
|
@@ -543,28 +534,9 @@ function createNodeProviderAdapter(baseOptions = {}) {
|
|
|
543
534
|
);
|
|
544
535
|
}
|
|
545
536
|
if (effect.kind === 'kv.get' || effect.kind === 'kv.put') {
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
const stores = kvStoresFor(executionOptions);
|
|
550
|
-
if (!stores.has(namespace)) stores.set(namespace, new Map());
|
|
551
|
-
const store = stores.get(namespace);
|
|
552
|
-
if (effect.kind === 'kv.get') {
|
|
553
|
-
return portableRuntimeHost.cloneKvValue(store.get(key), {
|
|
554
|
-
operation: 'get',
|
|
555
|
-
allowUndefined: true,
|
|
556
|
-
maxBytes: bindingOptions(executionOptions).maxKvValueBytes,
|
|
557
|
-
maxDepth: bindingOptions(executionOptions).maxKvValueDepth,
|
|
558
|
-
maxEntries: bindingOptions(executionOptions).maxKvValueEntries
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
store.set(key, portableRuntimeHost.cloneKvValue(effect.value, {
|
|
562
|
-
operation: 'put',
|
|
563
|
-
maxBytes: bindingOptions(executionOptions).maxKvValueBytes,
|
|
564
|
-
maxDepth: bindingOptions(executionOptions).maxKvValueDepth,
|
|
565
|
-
maxEntries: bindingOptions(executionOptions).maxKvValueEntries
|
|
566
|
-
}));
|
|
567
|
-
return true;
|
|
537
|
+
const namespace = portableRuntimeHost.normalizeKvNamespace(effect.store, bindingOptions(executionOptions));
|
|
538
|
+
const store = kvReferenceFor(executionOptions).kv(namespace);
|
|
539
|
+
return effect.kind === 'kv.get' ? store.get(effect.key) : store.put(effect.key, effect.value);
|
|
568
540
|
}
|
|
569
541
|
if (effect.kind === 'event.emit') {
|
|
570
542
|
const eventAdapter = executionOptions.eventAdapter || baseOptions.eventAdapter;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
|
+
const host = require('@pulse-compute/runtime/host');
|
|
5
|
+
|
|
6
|
+
// One explicit instance is one local authority. Reuse it across requests/targets
|
|
7
|
+
// to exercise shared storage. Identity/sequence injection is for deterministic
|
|
8
|
+
// host tests, never application input. This is not durable production storage.
|
|
9
|
+
function createNodeKvReference(options = {}) {
|
|
10
|
+
const identity = options.kvInstanceId === undefined ? randomUUID() : options.kvInstanceId;
|
|
11
|
+
if (typeof identity !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(identity)) throw new TypeError('Node KV instance identity must be bounded ASCII.');
|
|
12
|
+
let sequence = options.kvGenerationStart === undefined ? 0n : BigInt(options.kvGenerationStart);
|
|
13
|
+
if (sequence < 0n || sequence >= (1n << 128n)) throw new TypeError('Node KV generation sequence is out of range.');
|
|
14
|
+
const stores = new Map(), records = new Map();
|
|
15
|
+
const hooks = options.kvHooks || {};
|
|
16
|
+
function nextGeneration() {
|
|
17
|
+
if (sequence >= (1n << 128n) - 1n) throw new Error('Node KV generation space exhausted.');
|
|
18
|
+
sequence += 1n;
|
|
19
|
+
return `node-kv-v1:${identity}:${sequence.toString(16).padStart(32, '0')}`;
|
|
20
|
+
}
|
|
21
|
+
function namespace(name) {
|
|
22
|
+
host.normalizeKvNamespace(name, options);
|
|
23
|
+
if (!stores.has(name)) { stores.set(name, new Map()); records.set(name, new Map()); }
|
|
24
|
+
return { values: stores.get(name), entries: records.get(name) };
|
|
25
|
+
}
|
|
26
|
+
function put(name, key, value) {
|
|
27
|
+
const { entries, values } = namespace(name);
|
|
28
|
+
const wire = host.encodeConditionalKvValue(value, options);
|
|
29
|
+
const detached = host.decodeConditionalKvValue(wire, options);
|
|
30
|
+
const generation = nextGeneration();
|
|
31
|
+
entries.set(key, { wire, generation }); values.set(key, detached);
|
|
32
|
+
}
|
|
33
|
+
// Reject accessors instead of executing configuration as ambient code.
|
|
34
|
+
function entries(input) {
|
|
35
|
+
if (input === undefined || input === null) return [];
|
|
36
|
+
if (typeof input !== 'object' || Array.isArray(input) || ![null, Object.prototype].includes(Object.getPrototypeOf(input))) throw new TypeError('Node KV seed must be a data record.');
|
|
37
|
+
return Reflect.ownKeys(input).map((key) => {
|
|
38
|
+
const d = Object.getOwnPropertyDescriptor(input, key);
|
|
39
|
+
if (typeof key !== 'string' || !d.enumerable || !Object.prototype.hasOwnProperty.call(d, 'value')) throw new TypeError('Node KV seed must contain data properties.');
|
|
40
|
+
return [key, d.value];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
for (const [name, values] of entries(options.kv)) for (const [key, value] of entries(values)) {
|
|
44
|
+
host.normalizeKvKey(key, options); put(name, key, value);
|
|
45
|
+
}
|
|
46
|
+
function apply(effect) {
|
|
47
|
+
const { entries } = namespace(effect.namespace);
|
|
48
|
+
const current = entries.get(effect.key);
|
|
49
|
+
if (effect.kind === 'kv.getVersioned') return current
|
|
50
|
+
? Object.freeze({ status: 'found', generation: current.generation, value: host.decodeConditionalKvValue(current.wire, options) })
|
|
51
|
+
: Object.freeze({ status: 'not-found' });
|
|
52
|
+
if (effect.kind === 'kv.compareAndSwap' && !new RegExp(`^node-kv-v1:${identity}:[0-9a-f]{32}$`).test(effect.generation)) {
|
|
53
|
+
return Object.freeze({ status: 'not-stored', reason: 'invalid-generation' });
|
|
54
|
+
}
|
|
55
|
+
// No await, callback, or provider GET/PUT between predicate and mutation.
|
|
56
|
+
const matches = effect.kind === 'kv.insertIfAbsent' ? !current : current && current.generation === effect.generation;
|
|
57
|
+
if (!matches) return Object.freeze({ status: 'conflict' });
|
|
58
|
+
put(effect.namespace, effect.key, effect.value);
|
|
59
|
+
return Object.freeze({ status: 'stored' });
|
|
60
|
+
}
|
|
61
|
+
async function prepareConditionalKv(effect, execution = {}) {
|
|
62
|
+
if (hooks.prepare) await hooks.prepare(effect, execution);
|
|
63
|
+
return async () => {
|
|
64
|
+
if (hooks.dispatch) await hooks.dispatch(effect, execution);
|
|
65
|
+
if (execution.signal && execution.signal.aborted) throw new Error('Node KV operation stopped before acceptance.');
|
|
66
|
+
const result = apply(effect);
|
|
67
|
+
if (result.status === 'found' && hooks.read) return hooks.read(result, effect, execution);
|
|
68
|
+
if (result.status === 'stored' && hooks.afterCommit) await hooks.afterCommit(effect, execution);
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
version: 'pulse.node-kv-reference.v1',
|
|
74
|
+
prepareConditionalKv,
|
|
75
|
+
// Historical Assets reads use this semantic view, never the generation table.
|
|
76
|
+
legacyStores() { return stores; },
|
|
77
|
+
kv(name) {
|
|
78
|
+
const { values } = namespace(name);
|
|
79
|
+
const conditional = (operation, key, generation, value) => {
|
|
80
|
+
const admitted = host.admitConditionalKv({ kind: `kv.${operation}`, namespace: name, key, generation, value }, options);
|
|
81
|
+
return host.executeConditionalKv(admitted, prepareConditionalKv, {}, options);
|
|
82
|
+
};
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
get(key) { host.normalizeKvKey(key, options); return host.cloneKvValue(values.get(key), { ...options, allowUndefined: true }); },
|
|
85
|
+
put(key, value) { host.normalizeKvKey(key, options); put(name, key, value); return true; },
|
|
86
|
+
getVersioned(key) { return conditional('getVersioned', key); },
|
|
87
|
+
insertIfAbsent(key, value) { return conditional('insertIfAbsent', key, undefined, value); },
|
|
88
|
+
compareAndSwap(key, generation, value) { return conditional('compareAndSwap', key, generation, value); }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
module.exports = Object.freeze({ createNodeKvReference });
|
package/src/runtime/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const routeHandlerEffects = require('./route-handler-effects.js');
|
|
|
6
6
|
const canonicalApiRuntime = require('./canonical-api-runtime.js');
|
|
7
7
|
|
|
8
8
|
module.exports = {
|
|
9
|
+
...require('./conditional-kv.js'),
|
|
9
10
|
compiledWasmNodeAdapterKv,
|
|
10
11
|
assetsProvider,
|
|
11
12
|
routeHandlerEffects,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const https = require('node:https');
|
|
3
|
+
const { Readable } = require('node:stream');
|
|
4
|
+
const { performance } = require('node:perf_hooks');
|
|
5
|
+
const protocol = require('@pulse-compute/s3/provider');
|
|
6
|
+
|
|
7
|
+
function originRequest(request, signal) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const req = https.request(request.url, { method: request.method, headers: request.headers, signal, maxHeaderSize: 16384 }, (response) => {
|
|
10
|
+
let headers = [];
|
|
11
|
+
try {
|
|
12
|
+
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
13
|
+
// Node exposes HTTP header octets as Latin-1 strings. Interpret the
|
|
14
|
+
// bounded protocol metadata as UTF-8, matching the Native host ABI.
|
|
15
|
+
for (let i = 0; i < response.rawHeaders.length; i += 2) headers.push([response.rawHeaders[i], decoder.decode(Buffer.from(response.rawHeaders[i + 1], 'latin1'))]);
|
|
16
|
+
} catch { headers = null; }
|
|
17
|
+
resolve({ status: response.statusCode, headers, body: response, close: () => response.destroy() });
|
|
18
|
+
});
|
|
19
|
+
req.once('error', reject);
|
|
20
|
+
req.end(request.body);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function fetchFixture(fetch, request, signal) {
|
|
24
|
+
const pending = Promise.resolve().then(() => fetch(request.url, { method: request.method, headers: request.headers, ...(request.body === undefined ? {} : { body: request.body }), redirect: 'manual', cache: 'no-store', signal }));
|
|
25
|
+
pending.then((response) => { if (signal.aborted && response.body) void response.body.cancel().catch(() => {}); }, () => {});
|
|
26
|
+
const response = await abortable(pending, signal);
|
|
27
|
+
const body = response.body ? Readable.fromWeb(response.body, { signal }) : null;
|
|
28
|
+
// A deadline can fire before iteration begins; always observe the stream error.
|
|
29
|
+
if (body) body.on('error', () => {});
|
|
30
|
+
return { status: response.status, headers: [...response.headers], body, close: () => { if (body) body.destroy(); } };
|
|
31
|
+
}
|
|
32
|
+
function abortable(promise, signal) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const abort = () => reject(signal.reason);
|
|
35
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
36
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
37
|
+
if (signal.aborted) abort();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function readS3(effect, options, lookup) {
|
|
41
|
+
const operation = effect.operation, put = operation === 'putText';
|
|
42
|
+
const payload = effect.payload || {};
|
|
43
|
+
if (!['head', 'getText', 'putText'].includes(operation) || effect.kind !== `s3.${operation}` || effect.capability !== effect.kind
|
|
44
|
+
|| effect.package !== '@pulse-compute/s3' || effect.contractId !== 'pulse.s3' || effect.providerKind !== 's3') throw new TypeError('Invalid S3 package effect authority.');
|
|
45
|
+
let dispatched = false, status;
|
|
46
|
+
const failure = (reason) => put ? protocol.putFailure(reason, dispatched, status) : protocol.failure(reason, status);
|
|
47
|
+
if (options.signal && options.signal.aborted) throw options.signal.reason || new Error('Request cancelled.');
|
|
48
|
+
const configured = typeof payload.binding === 'string' && options.s3 && Object.hasOwn(options.s3, payload.binding) && options.s3[payload.binding];
|
|
49
|
+
let binding;
|
|
50
|
+
try { binding = protocol.normalizeBinding(configured); } catch { return failure('configuration'); }
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const onAbort = () => controller.abort(options.signal.reason);
|
|
53
|
+
if (options.signal) options.signal.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
const deadline = performance.now() + binding.timeoutMs;
|
|
56
|
+
const check = () => {
|
|
57
|
+
if (performance.now() >= deadline) { timedOut = true; controller.abort(); }
|
|
58
|
+
if (controller.signal.aborted) throw controller.signal.reason || new Error('S3 deadline expired.');
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, binding.timeoutMs);
|
|
61
|
+
const wait = async (value) => { const result = await abortable(Promise.resolve(value), controller.signal); check(); return result; };
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
const key = protocol.encodeKey(payload.key);
|
|
65
|
+
if (key === null) return failure('invalid-key');
|
|
66
|
+
let body, contentType;
|
|
67
|
+
if (put) {
|
|
68
|
+
if (typeof payload.text !== 'string' || !payload.text.isWellFormed()) return failure('invalid-text');
|
|
69
|
+
body = protocol.bytes(payload.text);
|
|
70
|
+
if (body.length > binding.maxTextBytes) return failure('too-large');
|
|
71
|
+
try { contentType = protocol.normalizePutOptions({ contentType: payload.contentType }).contentType; }
|
|
72
|
+
catch { return failure('configuration'); }
|
|
73
|
+
}
|
|
74
|
+
let accessId, secret, token;
|
|
75
|
+
try {
|
|
76
|
+
accessId = await wait(lookup(binding.accessKeyIdSecret));
|
|
77
|
+
secret = await wait(lookup(binding.secretAccessKeySecret));
|
|
78
|
+
token = binding.sessionTokenSecret === undefined ? undefined : await wait(lookup(binding.sessionTokenSecret));
|
|
79
|
+
} catch (error) { check(); return failure('credentials'); }
|
|
80
|
+
if (!protocol.credentialsValid(accessId, secret, token)) return failure('credentials');
|
|
81
|
+
for (const value of [accessId, secret, token]) if (typeof options.registerRedactionValue === 'function' && typeof value === 'string') options.registerRedactionValue(value);
|
|
82
|
+
const crypto = options.cryptoVerifier && options.cryptoVerifier.bytes;
|
|
83
|
+
const algorithms = options.cryptoRealization && options.cryptoRealization.algorithms;
|
|
84
|
+
const realization = options.cryptoTarget === 'javascript' ? 'runtime-builtin' : 'guest-source:pulse-hmac-as';
|
|
85
|
+
if (!crypto || !Array.isArray(algorithms) || ['SHA-256', 'HMAC-SHA256'].some((algorithm) => !algorithms.some((entry) => entry.algorithm === algorithm && entry.available && entry.realization === realization))) return failure('configuration');
|
|
86
|
+
let request;
|
|
87
|
+
try {
|
|
88
|
+
request = await wait(protocol.signRequest({ binding, method: put ? 'PUT' : operation === 'head' ? 'HEAD' : 'GET', encodedKey: key, accessId, secret, token, now: Date.now(), body, contentType }, crypto));
|
|
89
|
+
} catch (error) { check(); return failure('configuration'); }
|
|
90
|
+
check();
|
|
91
|
+
// The send primitive may have side effects even if it throws synchronously.
|
|
92
|
+
dispatched = true;
|
|
93
|
+
response = options.fetchImplementation
|
|
94
|
+
? await fetchFixture(options.fetchImplementation, request, controller.signal)
|
|
95
|
+
: await originRequest(request, controller.signal);
|
|
96
|
+
status = response.status;
|
|
97
|
+
check();
|
|
98
|
+
const rejected = put ? protocol.putStatusResult(status) : protocol.statusResult(status);
|
|
99
|
+
if (rejected && (!put || rejected.status === 'unknown')) return rejected;
|
|
100
|
+
const metadata = protocol.metadataFromHeaders(response.headers, operation === 'head');
|
|
101
|
+
if (!metadata) return failure('protocol');
|
|
102
|
+
if (operation === 'head') return protocol.normalizeResult(operation, { status: 'found', ...metadata });
|
|
103
|
+
const maximum = put ? rejected ? binding.maxTextBytes : 0 : binding.maxTextBytes;
|
|
104
|
+
if (metadata.byteLength > maximum) return failure(put ? 'protocol' : 'too-large');
|
|
105
|
+
const chunks = []; let total = 0;
|
|
106
|
+
if (response.body) for await (const chunk of response.body) {
|
|
107
|
+
check(); total += chunk.length;
|
|
108
|
+
if (total > maximum) return failure(put ? 'protocol' : 'too-large');
|
|
109
|
+
if (!put) chunks.push(Buffer.from(chunk));
|
|
110
|
+
}
|
|
111
|
+
check();
|
|
112
|
+
if (metadata.byteLength !== undefined && metadata.byteLength !== total) return failure('protocol');
|
|
113
|
+
if (put) return rejected || protocol.normalizeResult(operation, { status: 'stored', byteLength: body.length, sha256: request.headers['x-amz-content-sha256'], ...(metadata.etag === undefined ? {} : { etag: metadata.etag }) });
|
|
114
|
+
const raw = Buffer.concat(chunks, total);
|
|
115
|
+
const digest = protocol.hex(await wait(crypto.sha256(raw)));
|
|
116
|
+
let text;
|
|
117
|
+
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(raw); }
|
|
118
|
+
catch { return failure('invalid-utf8'); }
|
|
119
|
+
check();
|
|
120
|
+
return protocol.normalizeResult(operation, { status: 'found', ...metadata, byteLength: total, text, sha256: digest });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (options.signal && options.signal.aborted) throw options.signal.reason || error;
|
|
123
|
+
return failure(timedOut ? 'timeout' : status === undefined ? 'transport' : 'protocol');
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
if (options.signal) options.signal.removeEventListener('abort', onAbort);
|
|
127
|
+
controller.abort();
|
|
128
|
+
if (response) { try { await response.close(); } catch {} }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
module.exports = { readS3, originRequest };
|
package/src/toolchain.js
CHANGED
|
@@ -32,8 +32,11 @@ const reportingTargetCapability = Object.freeze({
|
|
|
32
32
|
levels: Object.freeze(['off', 'error', 'warn', 'info', 'debug'])
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
function genericProviderConfig() {
|
|
36
|
-
|
|
35
|
+
function genericProviderConfig(value) {
|
|
36
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
37
|
+
const bindings = input.bindings || {};
|
|
38
|
+
if (Object.keys(bindings).some((key) => key !== 's3')) throw new TypeError('Unknown Node binding field.');
|
|
39
|
+
return Object.freeze({ kind: 'node', bindings: Object.freeze({ s3: require('./config/s3.js').normalizeNodeS3(bindings.s3) }), local: Object.freeze({}) });
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function nodeRealization(nativeArtifact) {
|
|
@@ -139,14 +142,21 @@ function createDriver() {
|
|
|
139
142
|
sourceOnlySupported: false,
|
|
140
143
|
targetSupport: Object.freeze({ javascript: NODE_JAVASCRIPT_TARGET_SUPPORT_DECLARATION }),
|
|
141
144
|
normalizeConfig: genericProviderConfig,
|
|
145
|
+
configReference: Object.freeze({
|
|
146
|
+
sections: [{ id: 'node', title: 'Node provider options', description: 'Provider-owned Node profile configuration.' }],
|
|
147
|
+
fields: [{ section: 'node', path: 'node.bindings.s3', type: 'Readonly<Record<string, S3Binding>>', default: '`{}`', scope: 'Node Native and JavaScript S3',
|
|
148
|
+
description: 'Maps literal logical names to fixed HTTPS endpoint, bucket, region, accessKeyIdSecret, secretAccessKeySecret, optional sessionTokenSecret, maxTextBytes (1–32768, default 32768) and timeoutMs (1–30000, default 10000).',
|
|
149
|
+
security: 'Only named credential references are configuration. Runtime keys cannot override authority.' }]
|
|
150
|
+
}),
|
|
142
151
|
defaultLocalNetworkFetch: true,
|
|
143
|
-
projectConfigDocument() { return undefined; },
|
|
152
|
+
projectConfigDocument(config) { return Object.keys(config.bindings.s3).length ? config : undefined; },
|
|
144
153
|
initTemplate() {
|
|
145
154
|
return Object.freeze({ profileFragment: '', dependencies: Object.freeze({}) });
|
|
146
155
|
},
|
|
147
|
-
executionOptions(
|
|
156
|
+
executionOptions(config, values) {
|
|
148
157
|
return Object.freeze({
|
|
149
158
|
...values,
|
|
159
|
+
s3: config.bindings.s3,
|
|
150
160
|
providerAdapter: nodeRuntime.createNodeProviderAdapter()
|
|
151
161
|
});
|
|
152
162
|
},
|
|
@@ -205,7 +215,8 @@ function createDriver() {
|
|
|
205
215
|
javascript: NODE_JAVASCRIPT_TARGET_DESCRIPTOR
|
|
206
216
|
}),
|
|
207
217
|
execute: nodeRuntime.executeCanonicalProgram,
|
|
208
|
-
createLoweringPlan(metadata) {
|
|
218
|
+
createLoweringPlan(metadata, config = {}) {
|
|
219
|
+
require('./config/s3.js').validateNodeS3Operations(metadata, config.bindings || {});
|
|
209
220
|
return canonicalProvider.createProviderLoweringPlan(metadata, nodeRuntime.NODE_PROVIDER_DESCRIPTOR, {
|
|
210
221
|
grip: 'node-reference-broadcaster'
|
|
211
222
|
});
|