@pulse-compute/provider-node 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.
- package/README.md +60 -1
- package/package.json +61 -6
- package/src/capabilities.js +56 -0
- package/src/compiler/assets-provider-proof.js +434 -0
- package/src/compiler/backend-json-request-body-proof.js +90 -0
- package/src/compiler/index.js +38 -0
- package/src/compiler/node-adapter.js +452 -0
- package/src/compiler/node-compiled-wasm-assets-lifecycle-proof.js +435 -0
- package/src/compiler/request-json-body-proof.js +181 -0
- package/src/compiler/route-handler-compiled-wasm-bridge.js +323 -0
- package/src/compiler/route-handler-effect-proof.js +215 -0
- package/src/compiler/route-handler-live-origin-proof.js +372 -0
- package/src/compiler/schema-decode-result-proof.js +180 -0
- package/src/compiler/schema-response-codec-proof.js +149 -0
- package/src/events/reference-adapter.js +145 -0
- package/src/index.js +57 -0
- package/src/javascript/assets-adapter.js +68 -0
- package/src/javascript/bindings-adapter.js +120 -0
- package/src/javascript/fetch-adapter.js +187 -0
- package/src/javascript/grip-broadcast.js +243 -0
- package/src/javascript/jwt-verifier.js +137 -0
- package/src/javascript/lifecycle.js +69 -0
- package/src/javascript/node-adapter.js +281 -0
- package/src/javascript/package-effects.js +78 -0
- package/src/javascript/runtime-host.js +165 -0
- package/src/javascript/source-package.js +439 -0
- package/src/javascript/support.js +205 -0
- package/src/javascript/target-support-policy.js +134 -0
- package/src/javascript/target.js +37 -0
- package/src/javascript/test-runtime.js +111 -0
- package/src/native/target.js +78 -0
- package/src/runtime/assets-provider.js +776 -0
- package/src/runtime/canonical-api-runtime.js +632 -0
- package/src/runtime/compiled-wasm-node-adapter-kv.js +15 -0
- package/src/runtime/index.js +17 -0
- package/src/runtime/jwt-verifier.js +264 -0
- package/src/runtime/route-handler-effects.js +757 -0
- package/src/toolchain.js +258 -0
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
1
|
# @pulse-compute/provider-node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- pulse-package-status:start -->
|
|
4
|
+
> **Support tier:** Implementation/transitive surface<br>
|
|
5
|
+
> **Audience:** Pulse canonical Node provider and compiler maintainers.<br>
|
|
6
|
+
> **Install directly:** No for application projects; select provider: node through the CLI.<br>
|
|
7
|
+
> **Supported entry points:** `@pulse-compute/provider-node/toolchain`<br>
|
|
8
|
+
> **Stability:** The listed built-in toolchain entry is synchronized with the Beta bootstrap contract; other canonical Node provider interfaces remain internal.<br>
|
|
9
|
+
> **Canonical replacement:** `provider: 'node' through @pulse-compute/cli`<br>
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.1/packages/implementation-packages/)
|
|
11
|
+
>
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.1` package policy.
|
|
13
|
+
<!-- pulse-package-status:end -->
|
|
14
|
+
|
|
15
|
+
Canonical Node provider implementation used internally when a project selects `provider: 'node'`.
|
|
16
|
+
|
|
17
|
+
Application projects do not install or import this package directly. The supported entry is provider selection through `@pulse-compute/cli`.
|
|
18
|
+
|
|
19
|
+
## Responsibilities
|
|
20
|
+
|
|
21
|
+
- declare the Node canonical provider descriptor and capability mappings;
|
|
22
|
+
- execute canonical programs through the Node local/runtime path;
|
|
23
|
+
- realize fetch, config, secret, KV, structured response, opaque pass-through, supported package effects, and invocation-scoped event ingress/acceptance;
|
|
24
|
+
- normalize provider failures into canonical runtime diagnostics;
|
|
25
|
+
- supply compiler/runtime proofs for maintained internal lifecycle surfaces.
|
|
26
|
+
|
|
27
|
+
## Boundaries
|
|
28
|
+
|
|
29
|
+
- Canonical handlers never receive Node request/response objects or ambient process authority.
|
|
30
|
+
- Package-specific payload rules remain in their owning package/contracts.
|
|
31
|
+
- Product command behavior remains in the CLI package.
|
|
32
|
+
- JavaScript execution remains an explicitly selected Node target and never an automatic fallback.
|
|
33
|
+
- Event ingress is a bounded FIFO reference adapter with one active invocation per instance; outbound acceptance is a separate exact-frame ledger with no loopback, delivery, retry, persistence, or process-global singleton.
|
|
34
|
+
|
|
35
|
+
## JavaScript application host boundary
|
|
36
|
+
|
|
37
|
+
The provider-maintainer JavaScript subpaths are:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
@pulse-compute/provider-node/javascript/target
|
|
41
|
+
@pulse-compute/provider-node/javascript/runtime-host
|
|
42
|
+
@pulse-compute/provider-node/javascript/node-adapter
|
|
43
|
+
@pulse-compute/provider-node/javascript/lifecycle
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The graph-backed loader, Web request/response adapter, provider-owned `pulse dev`
|
|
47
|
+
lifecycle, capability adapters, schema codecs, Assets, and bounded GRIP behavior
|
|
48
|
+
are maintained under the target-support and cross-target conformance contracts.
|
|
49
|
+
No automatic fallback occurs, and application projects do not import these
|
|
50
|
+
implementation subpaths directly.
|
|
51
|
+
|
|
52
|
+
The Node JavaScript provider also realizes canonical `pulse.jwt` verification
|
|
53
|
+
through the package-owned `jose`/Web Crypto verifier. It owns named-secret
|
|
54
|
+
resolution and wall-clock capture; applications cannot select a crypto backend.
|
|
55
|
+
|
|
56
|
+
## Implementation exports
|
|
57
|
+
|
|
58
|
+
The package exposes focused compiler and runtime modules for the synchronized
|
|
59
|
+
repository, including `runtime/canonical-api-runtime`. Those exports are
|
|
60
|
+
implementation interfaces, not application-author APIs.
|
|
61
|
+
|
|
62
|
+
See [Contracts and providers](https://pulsecompute.io/v1.0.0-beta.1/concepts/contracts-and-providers/) and [Implementation packages](https://pulsecompute.io/v1.0.0-beta.1/packages/implementation-packages/).
|
package/package.json
CHANGED
|
@@ -1,15 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/provider-node",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Inert namespace bootstrap for Pulse.",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
5
4
|
"license": "Apache-2.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "^22.14.0 || ^24.0.0"
|
|
7
|
+
},
|
|
8
|
+
"description": "Node-specific Pulse provider adapter, local runtime, and package-owned builders.",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./compiler": "./src/compiler/index.js",
|
|
14
|
+
"./compiler/node-adapter": "./src/compiler/node-adapter.js",
|
|
15
|
+
"./compiler/assets-provider-proof": "./src/compiler/assets-provider-proof.js",
|
|
16
|
+
"./compiler/node-compiled-wasm-assets-lifecycle-proof": "./src/compiler/node-compiled-wasm-assets-lifecycle-proof.js",
|
|
17
|
+
"./compiler/route-handler-effect-proof": "./src/compiler/route-handler-effect-proof.js",
|
|
18
|
+
"./compiler/route-handler-live-origin-proof": "./src/compiler/route-handler-live-origin-proof.js",
|
|
19
|
+
"./compiler/route-handler-compiled-wasm-bridge": "./src/compiler/route-handler-compiled-wasm-bridge.js",
|
|
20
|
+
"./compiler/request-json-body-proof": "./src/compiler/request-json-body-proof.js",
|
|
21
|
+
"./runtime": "./src/runtime/index.js",
|
|
22
|
+
"./runtime/compiled-wasm-node-adapter-kv": "./src/runtime/compiled-wasm-node-adapter-kv.js",
|
|
23
|
+
"./runtime/assets-provider": "./src/runtime/assets-provider.js",
|
|
24
|
+
"./runtime/route-handler-effects": "./src/runtime/route-handler-effects.js",
|
|
25
|
+
"./compiler/schema-decode-result-proof": "./src/compiler/schema-decode-result-proof.js",
|
|
26
|
+
"./compiler/schema-response-codec-proof": "./src/compiler/schema-response-codec-proof.js",
|
|
27
|
+
"./compiler/backend-json-request-body-proof": "./src/compiler/backend-json-request-body-proof.js",
|
|
28
|
+
"./runtime/canonical-api-runtime": "./src/runtime/canonical-api-runtime.js",
|
|
29
|
+
"./native/target": "./src/native/target.js",
|
|
30
|
+
"./toolchain": "./src/toolchain.js",
|
|
31
|
+
"./javascript/target": "./src/javascript/target.js",
|
|
32
|
+
"./javascript/runtime-host": "./src/javascript/runtime-host.js",
|
|
33
|
+
"./javascript/bindings-adapter": "./src/javascript/bindings-adapter.js",
|
|
34
|
+
"./javascript/node-adapter": "./src/javascript/node-adapter.js",
|
|
35
|
+
"./javascript/lifecycle": "./src/javascript/lifecycle.js",
|
|
36
|
+
"./javascript/test-runtime": "./src/javascript/test-runtime.js",
|
|
37
|
+
"./javascript/support": "./src/javascript/support.js",
|
|
38
|
+
"./javascript/target-support-policy": "./src/javascript/target-support-policy.js",
|
|
39
|
+
"./javascript/package-effects": "./src/javascript/package-effects.js",
|
|
40
|
+
"./javascript/jwt-verifier": "./src/javascript/jwt-verifier.js",
|
|
41
|
+
"./javascript/grip-broadcast": "./src/javascript/grip-broadcast.js",
|
|
42
|
+
"./javascript/source-package": "./src/javascript/source-package.js"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@pulse-compute/crypto": "1.0.0-beta.1",
|
|
46
|
+
"@pulse-compute/jwt": "1.0.0-beta.1",
|
|
47
|
+
"@pulse-compute/runtime": "1.0.0-beta.1",
|
|
48
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.1",
|
|
49
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.1",
|
|
50
|
+
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.1"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
6
55
|
"files": [
|
|
56
|
+
"src",
|
|
7
57
|
"README.md",
|
|
8
58
|
"LICENSE",
|
|
9
59
|
"NOTICE"
|
|
10
60
|
],
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
64
|
+
"directory": "packages/provider-node"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.1/packages/implementation-packages/",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
14
69
|
}
|
|
15
|
-
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const NODE_JWT_VERIFY_CAPABILITY = 'jwt.verify';
|
|
4
|
+
|
|
5
|
+
const NODE_CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
|
|
6
|
+
'request',
|
|
7
|
+
'response.json',
|
|
8
|
+
'response.text',
|
|
9
|
+
'response.custom',
|
|
10
|
+
'fetch',
|
|
11
|
+
'config.get',
|
|
12
|
+
'secret.get',
|
|
13
|
+
'kv.get',
|
|
14
|
+
'kv.put',
|
|
15
|
+
'event.ingress',
|
|
16
|
+
'event.emit',
|
|
17
|
+
'assets.lookup',
|
|
18
|
+
'grip.broadcast',
|
|
19
|
+
NODE_JWT_VERIFY_CAPABILITY,
|
|
20
|
+
'opaque.pass-through'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const NODE_CANONICAL_PROVIDER_LOWERING = Object.freeze({
|
|
24
|
+
request: 'node.http.request',
|
|
25
|
+
'response.json': 'node.http.response.json',
|
|
26
|
+
'response.text': 'node.http.response.text',
|
|
27
|
+
'response.custom': 'node.http.response.custom',
|
|
28
|
+
fetch: 'node.fetch.dispatch',
|
|
29
|
+
'config.get': 'node.runtime.config.get',
|
|
30
|
+
'secret.get': 'node.runtime.secret.get',
|
|
31
|
+
'kv.get': 'node.runtime.kv.get',
|
|
32
|
+
'kv.put': 'node.runtime.kv.put',
|
|
33
|
+
'event.ingress': 'node.event.reference.ingress',
|
|
34
|
+
'event.emit': 'node.event.reference.accept',
|
|
35
|
+
'assets.lookup': 'node.runtime.assets.lookup',
|
|
36
|
+
'grip.broadcast': 'node.http.grip.publish',
|
|
37
|
+
[NODE_JWT_VERIFY_CAPABILITY]: 'node.native.jwt.verify',
|
|
38
|
+
'opaque.pass-through': 'node.http.response.stream'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const NODE_NATIVE_TARGET_CAPABILITIES = Object.freeze([
|
|
42
|
+
NODE_JWT_VERIFY_CAPABILITY,
|
|
43
|
+
'jwt.verify.hs256',
|
|
44
|
+
'jwt.verify.es256',
|
|
45
|
+
'secret.get',
|
|
46
|
+
'event.ingress',
|
|
47
|
+
'event.emit',
|
|
48
|
+
'time.wall-clock'
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
module.exports = Object.freeze({
|
|
52
|
+
NODE_JWT_VERIFY_CAPABILITY,
|
|
53
|
+
NODE_CANONICAL_PROVIDER_CAPABILITIES,
|
|
54
|
+
NODE_CANONICAL_PROVIDER_LOWERING,
|
|
55
|
+
NODE_NATIVE_TARGET_CAPABILITIES
|
|
56
|
+
});
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadAssetsContracts() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/assets/contracts');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../../wasm/packages/contracts/src/assets/contracts.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadDiagnostics() {
|
|
15
|
+
try {
|
|
16
|
+
return require('@pulse-compute/wasm-contracts/diagnostics');
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
19
|
+
return require('../../../../wasm/packages/contracts/src/diagnostics.js');
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const assetsContracts = loadAssetsContracts();
|
|
26
|
+
const {
|
|
27
|
+
PACKAGE_VERSION,
|
|
28
|
+
normalizeArtifact,
|
|
29
|
+
normalizeDiagnostic
|
|
30
|
+
} = loadDiagnostics();
|
|
31
|
+
const {
|
|
32
|
+
createNodeAssetsProvider,
|
|
33
|
+
createDefaultStoresFromPlan,
|
|
34
|
+
normalizeHeaders
|
|
35
|
+
} = require('../runtime/assets-provider.js');
|
|
36
|
+
|
|
37
|
+
const phaseName = 'node-assets-provider-proof';
|
|
38
|
+
|
|
39
|
+
function clone(value) {
|
|
40
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
41
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clone(entry)]));
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizePlan(input) {
|
|
46
|
+
const plan = input && input.artifact ? input.artifact : input;
|
|
47
|
+
if (!plan || typeof plan !== 'object') return undefined;
|
|
48
|
+
return {
|
|
49
|
+
...plan,
|
|
50
|
+
entries: Array.isArray(plan.entries) ? plan.entries.map(clone) : [],
|
|
51
|
+
payloadModes: plan.payloadModes ? clone(plan.payloadModes) : undefined
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function plannedEntries(plan) {
|
|
56
|
+
return (plan && Array.isArray(plan.entries) ? plan.entries : [])
|
|
57
|
+
.filter((entry) => entry && entry.status !== 'unsupported' && entry.lookup && entry.lookup.store && entry.lookup.key)
|
|
58
|
+
.map(clone);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeKey(key) {
|
|
62
|
+
const text = String(key || '');
|
|
63
|
+
return text.startsWith('/') ? text : `/${text}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeDiagnostic(code, message, severity, details) {
|
|
67
|
+
return normalizeDiagnostic({
|
|
68
|
+
phase: phaseName,
|
|
69
|
+
severity: severity || 'error',
|
|
70
|
+
code,
|
|
71
|
+
message,
|
|
72
|
+
hint: details && details.hint,
|
|
73
|
+
details,
|
|
74
|
+
loc: { file: '<node-assets-provider-proof>' }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function headerValue(headers, name) {
|
|
79
|
+
const lower = String(name).toLowerCase();
|
|
80
|
+
const found = normalizeHeaders(headers).find(([key]) => String(key).toLowerCase() === lower);
|
|
81
|
+
return found ? found[1] : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resultBody(result) {
|
|
85
|
+
if (!result) return undefined;
|
|
86
|
+
if (result.body !== undefined) return String(result.body);
|
|
87
|
+
if (result.kind === 'stream' && result.bodyStream && Array.isArray(result.bodyStream.chunks)) {
|
|
88
|
+
return result.bodyStream.chunks.map((chunk) => Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)).join('');
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function check(name, passed, details, failures, diagnostics) {
|
|
94
|
+
const out = { name, status: passed ? 'ok' : 'error', details: details || {} };
|
|
95
|
+
if (!passed) {
|
|
96
|
+
failures.push(out);
|
|
97
|
+
diagnostics.push(makeDiagnostic(
|
|
98
|
+
assetsContracts.ASSETS_DIAGNOSTIC_CODES.NODE_PROVIDER_SMOKE_FAILED,
|
|
99
|
+
`Node assets provider proof check failed: ${name}.`,
|
|
100
|
+
'error',
|
|
101
|
+
{ check: name, details: out.details }
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cloneEntryForPayload(entry, payloadMode, idSuffix) {
|
|
108
|
+
const next = clone(entry);
|
|
109
|
+
next.id = `${entry.id || 'asset'}-${idSuffix}`;
|
|
110
|
+
next.lookup = { ...(next.lookup || {}), payloadMode };
|
|
111
|
+
next.response = {
|
|
112
|
+
...(next.response || {}),
|
|
113
|
+
lookupId: next.id,
|
|
114
|
+
payloadMode,
|
|
115
|
+
payloadStatus: payloadMode === 'binary-buffer' ? 'reserved-with-diagnostic' : 'lowering-plan-only'
|
|
116
|
+
};
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function selectEntry(entries, method) {
|
|
121
|
+
const upper = String(method || 'GET').toUpperCase();
|
|
122
|
+
if (upper === 'HEAD') {
|
|
123
|
+
return entries.find((entry) => String(entry.lookup.method || 'GET').toUpperCase() === 'HEAD') || entries[0];
|
|
124
|
+
}
|
|
125
|
+
return entries.find((entry) => String(entry.lookup.method || 'GET').toUpperCase() === upper) || entries[0];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bodyFixtureForEntry(entry, label) {
|
|
129
|
+
return `${label}:${entry.lookup.store}:${normalizeKey(entry.lookup.key)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function storesWithBody(entry, body, options = {}) {
|
|
133
|
+
return {
|
|
134
|
+
[entry.lookup.store]: {
|
|
135
|
+
[normalizeKey(entry.lookup.key)]: {
|
|
136
|
+
status: options.status || (entry.response && entry.response.status) || 200,
|
|
137
|
+
body,
|
|
138
|
+
payloadMode: options.payloadMode,
|
|
139
|
+
cacheControl: entry.lookup.cacheControl,
|
|
140
|
+
headers: [
|
|
141
|
+
['Content-Type', options.contentType || 'text/plain; charset=utf-8'],
|
|
142
|
+
['X-PulseWasm-Node-Proof', options.label || '1']
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildNodeAssetsProviderProof(inputs = {}) {
|
|
150
|
+
const cwd = inputs.cwd || process.cwd();
|
|
151
|
+
const generatedBy = inputs.generatedBy || PACKAGE_VERSION;
|
|
152
|
+
const plan = normalizePlan(inputs.assetsLoweringPlan || inputs.plan);
|
|
153
|
+
const diagnostics = [];
|
|
154
|
+
if (!plan) {
|
|
155
|
+
diagnostics.push(makeDiagnostic(
|
|
156
|
+
assetsContracts.ASSETS_DIAGNOSTIC_CODES.NODE_PROVIDER_PLAN_REQUIRED,
|
|
157
|
+
'Node assets provider proof requires an assets-lowering-plan artifact.',
|
|
158
|
+
'error',
|
|
159
|
+
{ hint: 'Emit --assets-lowering-plan before requesting the Node assets provider proof.' }
|
|
160
|
+
));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const entries = plannedEntries(plan);
|
|
164
|
+
if (plan && entries.length === 0) {
|
|
165
|
+
diagnostics.push(makeDiagnostic(
|
|
166
|
+
assetsContracts.ASSETS_DIAGNOSTIC_CODES.NODE_PROVIDER_PLAN_REQUIRED,
|
|
167
|
+
'Node assets provider proof requires at least one static assets lookup/respond entry from the lowering plan.',
|
|
168
|
+
'error',
|
|
169
|
+
{ entries: 0, hint: 'Add assets.lookup("public", "/file.txt") and assets.respond(handle) to the entry source.' }
|
|
170
|
+
));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const failures = [];
|
|
174
|
+
const checks = [];
|
|
175
|
+
const results = {};
|
|
176
|
+
let proofStores = inputs.stores;
|
|
177
|
+
let provider;
|
|
178
|
+
|
|
179
|
+
if (entries.length > 0) {
|
|
180
|
+
const getEntry = selectEntry(entries, 'GET');
|
|
181
|
+
const headEntry = selectEntry(entries, 'HEAD') || getEntry;
|
|
182
|
+
if (!proofStores) {
|
|
183
|
+
proofStores = createDefaultStoresFromPlan(plan);
|
|
184
|
+
proofStores[getEntry.lookup.store] = {
|
|
185
|
+
...(proofStores[getEntry.lookup.store] || {}),
|
|
186
|
+
[normalizeKey(getEntry.lookup.key)]: {
|
|
187
|
+
status: (getEntry.response && getEntry.response.status) || 200,
|
|
188
|
+
body: bodyFixtureForEntry(getEntry, 'node-proof'),
|
|
189
|
+
cacheControl: getEntry.lookup.cacheControl,
|
|
190
|
+
headers: [['Content-Type', 'text/plain; charset=utf-8']]
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
proofStores[headEntry.lookup.store] = {
|
|
194
|
+
...(proofStores[headEntry.lookup.store] || {}),
|
|
195
|
+
[normalizeKey(headEntry.lookup.key)]: {
|
|
196
|
+
status: (headEntry.response && headEntry.response.status) || 200,
|
|
197
|
+
body: bodyFixtureForEntry(headEntry, 'node-proof-head'),
|
|
198
|
+
cacheControl: headEntry.lookup.cacheControl,
|
|
199
|
+
headers: [['Content-Type', 'text/plain; charset=utf-8']]
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
provider = createNodeAssetsProvider({ plan, stores: proofStores, mode: inputs.mode || 'local-static-fixture' });
|
|
205
|
+
diagnostics.push(...(provider.diagnostics || []));
|
|
206
|
+
|
|
207
|
+
const getResult = provider.executeRequest({ method: 'GET', path: getEntry.lookup.key });
|
|
208
|
+
results.getText = {
|
|
209
|
+
status: getResult.status,
|
|
210
|
+
kind: getResult.kind,
|
|
211
|
+
body: resultBody(getResult),
|
|
212
|
+
headers: normalizeHeaders(getResult.headers),
|
|
213
|
+
diagnostics: getResult.diagnostics || []
|
|
214
|
+
};
|
|
215
|
+
checks.push(check(
|
|
216
|
+
'GET text body response returns 200/status and body through Node result ABI',
|
|
217
|
+
getResult.status === ((getEntry.response && getEntry.response.status) || 200) && getResult.kind === 'text' && typeof getResult.body === 'string' && getResult.body.length > 0,
|
|
218
|
+
{ status: getResult.status, kind: getResult.kind, bodyLength: getResult.body ? String(getResult.body).length : 0 },
|
|
219
|
+
failures,
|
|
220
|
+
diagnostics
|
|
221
|
+
));
|
|
222
|
+
checks.push(check(
|
|
223
|
+
'GET headers/status pass through from provider asset record',
|
|
224
|
+
Boolean(headerValue(getResult.headers, 'Content-Type')) && headerValue(getResult.headers, 'X-PulseWasm-Asset-Key') === normalizeKey(getEntry.lookup.key),
|
|
225
|
+
{ contentType: headerValue(getResult.headers, 'Content-Type'), assetKey: headerValue(getResult.headers, 'X-PulseWasm-Asset-Key') },
|
|
226
|
+
failures,
|
|
227
|
+
diagnostics
|
|
228
|
+
));
|
|
229
|
+
if (getEntry.lookup.cacheControl) {
|
|
230
|
+
checks.push(check(
|
|
231
|
+
'GET cache-control policy is preserved when declared in the lowering plan',
|
|
232
|
+
headerValue(getResult.headers, 'Cache-Control') === getEntry.lookup.cacheControl,
|
|
233
|
+
{ expected: getEntry.lookup.cacheControl, actual: headerValue(getResult.headers, 'Cache-Control') },
|
|
234
|
+
failures,
|
|
235
|
+
diagnostics
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const headResult = provider.executeRequest({ method: 'HEAD', path: headEntry.lookup.key });
|
|
240
|
+
results.head = {
|
|
241
|
+
status: headResult.status,
|
|
242
|
+
kind: headResult.kind,
|
|
243
|
+
body: resultBody(headResult),
|
|
244
|
+
headers: normalizeHeaders(headResult.headers),
|
|
245
|
+
diagnostics: headResult.diagnostics || []
|
|
246
|
+
};
|
|
247
|
+
checks.push(check(
|
|
248
|
+
'HEAD returns status/headers with no body',
|
|
249
|
+
headResult.status === ((headEntry.response && headEntry.response.status) || 200) && headResult.kind === 'empty' && headResult.body === undefined,
|
|
250
|
+
{ status: headResult.status, kind: headResult.kind, body: headResult.body },
|
|
251
|
+
failures,
|
|
252
|
+
diagnostics
|
|
253
|
+
));
|
|
254
|
+
|
|
255
|
+
const missingPath = '/__pulsewasm-missing-asset__';
|
|
256
|
+
const missingResult = provider.executeRequest({ method: 'GET', path: missingPath });
|
|
257
|
+
results.missing = {
|
|
258
|
+
status: missingResult.status,
|
|
259
|
+
kind: missingResult.kind,
|
|
260
|
+
passThroughOn404: missingResult.passThroughOn404,
|
|
261
|
+
diagnostics: missingResult.diagnostics || []
|
|
262
|
+
};
|
|
263
|
+
checks.push(check(
|
|
264
|
+
'missing asset follows 404/pass-through policy',
|
|
265
|
+
missingResult.status === 404 && missingResult.kind === 'empty' && missingResult.passThroughOn404 === true,
|
|
266
|
+
{ status: missingResult.status, kind: missingResult.kind, passThroughOn404: missingResult.passThroughOn404 },
|
|
267
|
+
failures,
|
|
268
|
+
diagnostics
|
|
269
|
+
));
|
|
270
|
+
|
|
271
|
+
const methodResult = provider.executeRequest({ method: 'POST', path: getEntry.lookup.key });
|
|
272
|
+
results.unsupportedMethod = {
|
|
273
|
+
status: methodResult.status,
|
|
274
|
+
kind: methodResult.kind,
|
|
275
|
+
headers: normalizeHeaders(methodResult.headers),
|
|
276
|
+
diagnostics: methodResult.diagnostics || []
|
|
277
|
+
};
|
|
278
|
+
checks.push(check(
|
|
279
|
+
'unsupported methods are diagnosed and rejected',
|
|
280
|
+
methodResult.status === 405 && (methodResult.diagnostics || []).some((diag) => diag.code === assetsContracts.ASSETS_DIAGNOSTIC_CODES.UNSUPPORTED_METHOD),
|
|
281
|
+
{ status: methodResult.status, diagnosticCodes: (methodResult.diagnostics || []).map((diag) => diag.code) },
|
|
282
|
+
failures,
|
|
283
|
+
diagnostics
|
|
284
|
+
));
|
|
285
|
+
|
|
286
|
+
const streamEntry = cloneEntryForPayload(getEntry, 'stream-pass-through-response', 'stream');
|
|
287
|
+
const streamPlan = { ...plan, entries: [streamEntry] };
|
|
288
|
+
const streamProvider = createNodeAssetsProvider({
|
|
289
|
+
plan: streamPlan,
|
|
290
|
+
stores: storesWithBody(streamEntry, bodyFixtureForEntry(streamEntry, 'node-proof-stream'), { payloadMode: 'stream-pass-through-response', label: 'stream' })
|
|
291
|
+
});
|
|
292
|
+
const streamResult = streamProvider.executeRequest({ method: 'GET', path: streamEntry.lookup.key });
|
|
293
|
+
results.stream = {
|
|
294
|
+
status: streamResult.status,
|
|
295
|
+
kind: streamResult.kind,
|
|
296
|
+
providerStreamWired: streamResult.providerStreamWired,
|
|
297
|
+
body: resultBody(streamResult),
|
|
298
|
+
diagnostics: streamResult.diagnostics || []
|
|
299
|
+
};
|
|
300
|
+
checks.push(check(
|
|
301
|
+
'stream pass-through response is wired for the Node proof path',
|
|
302
|
+
streamResult.status === 200 && streamResult.kind === 'stream' && streamResult.providerStreamWired === true && resultBody(streamResult).length > 0,
|
|
303
|
+
{ status: streamResult.status, kind: streamResult.kind, providerStreamWired: streamResult.providerStreamWired, bodyLength: resultBody(streamResult).length },
|
|
304
|
+
failures,
|
|
305
|
+
diagnostics
|
|
306
|
+
));
|
|
307
|
+
|
|
308
|
+
const binaryEntry = cloneEntryForPayload(getEntry, 'binary-buffer', 'binary');
|
|
309
|
+
const binaryPlan = { ...plan, entries: [binaryEntry] };
|
|
310
|
+
const binaryProvider = createNodeAssetsProvider({
|
|
311
|
+
plan: binaryPlan,
|
|
312
|
+
stores: storesWithBody(binaryEntry, Buffer.from('binary-proof'), { payloadMode: 'binary-buffer', label: 'binary' })
|
|
313
|
+
});
|
|
314
|
+
const binaryResult = binaryProvider.executeRequest({ method: 'GET', path: binaryEntry.lookup.key });
|
|
315
|
+
results.binary = {
|
|
316
|
+
status: binaryResult.status,
|
|
317
|
+
kind: binaryResult.kind,
|
|
318
|
+
diagnostics: binaryResult.diagnostics || []
|
|
319
|
+
};
|
|
320
|
+
checks.push(check(
|
|
321
|
+
'binary body/buffer mode remains reserved with diagnostic',
|
|
322
|
+
binaryResult.status === 501 && (binaryResult.diagnostics || []).some((diag) => diag.code === assetsContracts.ASSETS_DIAGNOSTIC_CODES.BINARY_BODY_RESERVED),
|
|
323
|
+
{ status: binaryResult.status, diagnosticCodes: (binaryResult.diagnostics || []).map((diag) => diag.code) },
|
|
324
|
+
failures,
|
|
325
|
+
diagnostics
|
|
326
|
+
));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const errorDiagnostics = diagnostics.filter((entry) => (entry.severity || 'error') === 'error');
|
|
330
|
+
const artifact = normalizeArtifact({
|
|
331
|
+
version: assetsContracts.ASSETS_NODE_PROVIDER_PROOF_VERSION,
|
|
332
|
+
generatedBy,
|
|
333
|
+
phase: phaseName,
|
|
334
|
+
status: errorDiagnostics.length === 0 ? 'ok' : 'error',
|
|
335
|
+
provider: 'node',
|
|
336
|
+
providerPackage: '@pulse-compute/provider-node',
|
|
337
|
+
contractId: assetsContracts.ASSETS_CONTRACT_ID,
|
|
338
|
+
sourceAssetsLoweringPlanVersion: plan && plan.version,
|
|
339
|
+
loweringPlanConnected: Boolean(plan && entries.length > 0),
|
|
340
|
+
providerBehaviorImplemented: true,
|
|
341
|
+
compiledWasmLowering: false,
|
|
342
|
+
proofMode: 'direct-provider-smoke-connected-to-lowering-plan',
|
|
343
|
+
scope: {
|
|
344
|
+
mode: inputs.mode || 'local-static-fixture',
|
|
345
|
+
methods: clone(assetsContracts.ASSETS_ALLOWED_METHODS),
|
|
346
|
+
staticStoreKeyFromLoweringPlan: true,
|
|
347
|
+
uploads: false,
|
|
348
|
+
invalidation: false,
|
|
349
|
+
mutationApis: false,
|
|
350
|
+
cacheControlPolicy: 'pass-through-when-declared',
|
|
351
|
+
notFoundPolicy: '404-pass-through'
|
|
352
|
+
},
|
|
353
|
+
payloadModes: {
|
|
354
|
+
textResponseBody: {
|
|
355
|
+
mode: 'text-response-body',
|
|
356
|
+
status: 'implemented-now',
|
|
357
|
+
proof: 'GET response body via Node result ABI'
|
|
358
|
+
},
|
|
359
|
+
headerPassThrough: {
|
|
360
|
+
mode: 'header-pass-through',
|
|
361
|
+
status: 'implemented-now',
|
|
362
|
+
proof: 'Content-Type, X-PulseWasm-Asset-Key, and Cache-Control header checks'
|
|
363
|
+
},
|
|
364
|
+
statusResultPassThrough: {
|
|
365
|
+
mode: 'status-result-pass-through',
|
|
366
|
+
status: 'implemented-now',
|
|
367
|
+
proof: 'GET and HEAD status checks'
|
|
368
|
+
},
|
|
369
|
+
streamPassThroughResponse: {
|
|
370
|
+
mode: 'stream-pass-through-response',
|
|
371
|
+
contractStatus: assetsContracts.ASSETS_PAYLOAD_MODE_CLASSIFICATION.streamPassThroughResponse.status,
|
|
372
|
+
planStatus: plan && plan.payloadModes && plan.payloadModes.stream ? plan.payloadModes.stream.status : undefined,
|
|
373
|
+
hostSurfaceStatus: assetsContracts.ASSETS_PAYLOAD_MODE_CLASSIFICATION.streamPassThroughResponse.hostSurfaceStatus,
|
|
374
|
+
status: entries.length > 0 ? 'implemented-node-proof' : 'not-exercised',
|
|
375
|
+
providerWired: entries.length > 0,
|
|
376
|
+
proof: 'direct Node provider returns kind=stream with bodyStream chunks'
|
|
377
|
+
},
|
|
378
|
+
binaryBodyBufferResponse: {
|
|
379
|
+
mode: 'binary-buffer',
|
|
380
|
+
contractStatus: assetsContracts.ASSETS_PAYLOAD_MODE_CLASSIFICATION.binaryBodyBufferResponse.status,
|
|
381
|
+
planStatus: plan && plan.payloadModes && plan.payloadModes.binary ? plan.payloadModes.binary.status : undefined,
|
|
382
|
+
status: 'reserved-with-diagnostic',
|
|
383
|
+
providerWired: false,
|
|
384
|
+
diagnosticCode: assetsContracts.ASSETS_DIAGNOSTIC_CODES.BINARY_BODY_RESERVED
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
entriesConsumed: entries.map((entry) => ({
|
|
388
|
+
id: entry.id,
|
|
389
|
+
store: entry.lookup.store,
|
|
390
|
+
key: entry.lookup.key,
|
|
391
|
+
method: entry.lookup.method || 'GET',
|
|
392
|
+
payloadMode: (entry.response && entry.response.payloadMode) || entry.lookup.payloadMode || 'text-response-body',
|
|
393
|
+
response: entry.response ? { status: entry.response.status, payloadMode: entry.response.payloadMode } : undefined
|
|
394
|
+
})),
|
|
395
|
+
smoke: {
|
|
396
|
+
checks,
|
|
397
|
+
failedChecks: failures,
|
|
398
|
+
results
|
|
399
|
+
},
|
|
400
|
+
policy: {
|
|
401
|
+
compilerOwnsProviderBehavior: false,
|
|
402
|
+
providerOwnsImplementation: true,
|
|
403
|
+
planBuilderOwner: plan && plan.policy && plan.policy.builderOwner ? plan.policy.builderOwner : '@pulse-compute/assets',
|
|
404
|
+
providerOwner: '@pulse-compute/provider-node',
|
|
405
|
+
consumesLoweringPlan: true
|
|
406
|
+
},
|
|
407
|
+
summary: {
|
|
408
|
+
entries: entries.length,
|
|
409
|
+
checks: checks.length,
|
|
410
|
+
failedChecks: failures.length,
|
|
411
|
+
getTextValidated: checks.some((entry) => entry.name.startsWith('GET text') && entry.status === 'ok'),
|
|
412
|
+
headValidated: checks.some((entry) => entry.name.startsWith('HEAD') && entry.status === 'ok'),
|
|
413
|
+
notFoundValidated: checks.some((entry) => entry.name.startsWith('missing asset') && entry.status === 'ok'),
|
|
414
|
+
streamValidated: checks.some((entry) => entry.name.startsWith('stream pass-through') && entry.status === 'ok'),
|
|
415
|
+
binaryReservedValidated: checks.some((entry) => entry.name.startsWith('binary body') && entry.status === 'ok'),
|
|
416
|
+
diagnostics: diagnostics.length,
|
|
417
|
+
errorDiagnostics: errorDiagnostics.length
|
|
418
|
+
},
|
|
419
|
+
diagnostics
|
|
420
|
+
}, cwd);
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
artifact,
|
|
424
|
+
diagnostics,
|
|
425
|
+
entries: artifact.entriesConsumed,
|
|
426
|
+
smoke: artifact.smoke,
|
|
427
|
+
hasErrors: errorDiagnostics.length > 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = {
|
|
432
|
+
buildNodeAssetsProviderProof,
|
|
433
|
+
phaseName
|
|
434
|
+
};
|