space-data-module-sdk 0.8.11 → 0.8.12
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 +92 -0
- package/bin/space-data-module.js +33 -1
- package/package.json +2 -1
- package/src/browser.js +11 -0
- package/src/capabilities.js +39 -0
- package/src/compliance/index.js +2 -0
- package/src/compliance/pluginCompliance.js +2 -18
- package/src/flow/flowCompiler.js +215 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/host/browserModuleHarness.js +26 -0
- package/src/host/isomorphicLoader.js +57 -11
- package/src/host/runtimeTargetGate.js +256 -0
- package/src/host/workerModuleHarness.js +7 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +11 -0
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +287 -27
|
@@ -21,12 +21,14 @@
|
|
|
21
21
|
import {
|
|
22
22
|
createBrowserModuleHarness,
|
|
23
23
|
toLoadableWasmBytes,
|
|
24
|
+
zeroWasmBytes,
|
|
24
25
|
} from "./browserModuleHarness.js";
|
|
25
26
|
import {
|
|
26
27
|
resolveModuleSignaturePolicy,
|
|
27
28
|
verifyModuleArtifact,
|
|
28
29
|
} from "../bundle/signing.js";
|
|
29
30
|
import { attachHostDispatch, inspectModule } from "./isomorphicLoaderCore.js";
|
|
31
|
+
import { assertArtifactRuntimeTarget } from "./runtimeTargetGate.js";
|
|
30
32
|
|
|
31
33
|
export { inspectModule, attachHostDispatch };
|
|
32
34
|
|
|
@@ -203,21 +205,65 @@ export async function loadModule(options = {}) {
|
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
|
|
208
|
+
const runtimeKind = options.runtimeKind ?? "wasmedge";
|
|
209
|
+
|
|
210
|
+
// ONE READ of the artifact, serving every question this function asks of the
|
|
211
|
+
// bytes: the signature, the runtime-target declaration, and the profile
|
|
212
|
+
// inspection below. Each of those used to open the file for itself. The
|
|
213
|
+
// plaintext buffer this function materialized is scrubbed the moment the
|
|
214
|
+
// last of them is answered, matching the browser harness's
|
|
215
|
+
// ownedArtifactBytes contract.
|
|
216
|
+
const runtimeHostRequested =
|
|
217
|
+
String(options.hostProfile ?? "").trim().toLowerCase() === "runtime-host" ||
|
|
218
|
+
Array.isArray(options.modules) ||
|
|
219
|
+
(typeof options.defaultModuleId === "string" &&
|
|
220
|
+
options.defaultModuleId.trim().length > 0);
|
|
221
|
+
// The profile inspection is the ONLY consumer here that needs a compiled
|
|
222
|
+
// module. The runtime-host path does not take it, and it must not start
|
|
223
|
+
// paying a V8 compile of the whole artifact on every load just because the
|
|
224
|
+
// runtime-target gate arrived — the gate reads a custom section, which is a
|
|
225
|
+
// byte-level question.
|
|
226
|
+
const needsCompiledModule =
|
|
227
|
+
runtimeKind === "wasmedge" &&
|
|
228
|
+
!runtimeHostRequested &&
|
|
229
|
+
!options.wasmEdgeRunnerBinary;
|
|
230
|
+
const needsBytes = Boolean(signaturePolicy) || runtimeKind === "wasmedge";
|
|
231
|
+
let artifactBytes = null;
|
|
232
|
+
let wasmModule = null;
|
|
233
|
+
let embeddedManifest = null;
|
|
234
|
+
if (needsBytes) {
|
|
207
235
|
const { readFile } = await import("node:fs/promises");
|
|
208
|
-
|
|
236
|
+
artifactBytes = new Uint8Array(await readFile(source));
|
|
237
|
+
if (signaturePolicy) {
|
|
238
|
+
await verifyModuleArtifact(artifactBytes, signaturePolicy);
|
|
239
|
+
}
|
|
240
|
+
if (runtimeKind === "wasmedge") {
|
|
241
|
+
const { locateEmbeddedPlgManifest } = await import(
|
|
242
|
+
"../compliance/index.js"
|
|
243
|
+
);
|
|
244
|
+
embeddedManifest =
|
|
245
|
+
locateEmbeddedPlgManifest(artifactBytes)?.decoded ?? null;
|
|
246
|
+
}
|
|
247
|
+
if (needsCompiledModule) {
|
|
248
|
+
wasmModule = await WebAssembly.compile(toLoadableWasmBytes(artifactBytes));
|
|
249
|
+
}
|
|
250
|
+
zeroWasmBytes(artifactBytes);
|
|
251
|
+
artifactBytes = null;
|
|
209
252
|
}
|
|
210
253
|
|
|
211
|
-
const runtimeKind = options.runtimeKind ?? "wasmedge";
|
|
212
254
|
if (runtimeKind === "wasmedge") {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
// MIRROR OF THE BROWSER GATE. Enforcing the declaration on one leg only is
|
|
256
|
+
// itself a divergence between the legs: a `["browser"]`-only artifact
|
|
257
|
+
// handed to WasmEdge would run until it reached something WasmEdge does
|
|
258
|
+
// not serve, while the reverse case is refused at the door. Both legs read
|
|
259
|
+
// the same declaration and refuse the same way.
|
|
260
|
+
assertArtifactRuntimeTarget({
|
|
261
|
+
embeddedManifest,
|
|
262
|
+
manifest: options.manifest,
|
|
263
|
+
leg: "wasmedge",
|
|
264
|
+
});
|
|
265
|
+
if (needsCompiledModule) {
|
|
266
|
+
const inspection = await inspectModule(wasmModule);
|
|
221
267
|
if (
|
|
222
268
|
(inspection.profile === "standalone" || inspection.profile === "module-host-abi") &&
|
|
223
269
|
inspection.exports.includes("_start")
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RUNTIME-TARGET GATE — one assert, called by every loader that stands for
|
|
3
|
+
* a runtime leg.
|
|
4
|
+
*
|
|
5
|
+
* Composed flows derive their `runtimeTargets` from their parts (see
|
|
6
|
+
* `collectFlowRuntimeTargets` in `src/flow/flowCompiler.js`), so an artifact
|
|
7
|
+
* that legitimately runs on only ONE leg now exists. That artifact must never
|
|
8
|
+
* be loaded QUIETLY on a leg it does not declare: the failure would surface as
|
|
9
|
+
* a hostcall trap several frames deep in a capability that leg cannot serve,
|
|
10
|
+
* which is indistinguishable from the cross-runtime divergence the isomorphism
|
|
11
|
+
* invariant exists to catch. Refuse at the door, by name, quoting the
|
|
12
|
+
* declaration that made it ineligible.
|
|
13
|
+
*
|
|
14
|
+
* The gate lives here rather than inside one harness because there are two
|
|
15
|
+
* doors into a composed artifact — `createBrowserModuleHarness` (a single
|
|
16
|
+
* module) and `createFlowRuntimeHost` (the composed flow runtime) — and a gate
|
|
17
|
+
* on only one of them is not a gate.
|
|
18
|
+
*
|
|
19
|
+
* Browser-safe: no node builtins, no bundle/signing surface, and the embedded
|
|
20
|
+
* declaration is read with `WebAssembly.Module.customSections`, which needs no
|
|
21
|
+
* copy of the source bytes (they are scrubbed by the time this runs).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { SDS_MANIFEST_SECTION_NAME } from "../bundle/constants.js";
|
|
25
|
+
import { decodePlgManifest, isPlgManifestBuffer } from "../manifest/plgCodec.js";
|
|
26
|
+
import { LegIncompatibleCapabilityIds } from "../capabilities.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `wasi` is the STRICT PORTABILITY BASELINE, not a fourth runtime: compliance
|
|
30
|
+
* confines a `wasi`-declaring artifact to the pure WASI capability subset and
|
|
31
|
+
* the `command` invoke surface. So a declaration of `wasi` admits either leg —
|
|
32
|
+
* WITH ONE EXCEPTION, which is why capabilities are part of this question.
|
|
33
|
+
* `pipe` is in both the standalone-WASI subset and the browser-incompatible
|
|
34
|
+
* set, so `runtimeTargets:["wasi"] + capabilities:["pipe"]` passes compliance
|
|
35
|
+
* (it names no browser target for the rule to fire on) and would otherwise be
|
|
36
|
+
* admitted to the browser leg against the SDK's own policy. The baseline
|
|
37
|
+
* admits a leg only when the artifact carries nothing that leg cannot serve.
|
|
38
|
+
*
|
|
39
|
+
* DO NOT "UNIFY" THIS WITH THE LITERAL PATH. A declaration that names the leg
|
|
40
|
+
* outright is NOT capability-checked here, and that asymmetry is deliberate:
|
|
41
|
+
*
|
|
42
|
+
* declaration -> trust the author; inference -> prove it.
|
|
43
|
+
*
|
|
44
|
+
* An explicit `browser` target is the author's statement and the embedder's
|
|
45
|
+
* business — five of the browser-incompatible capabilities (`network`, `ipfs`,
|
|
46
|
+
* `protocol_dial`, `protocol_handle`, `wallet_sign`) ARE served by
|
|
47
|
+
* `BrowserHost` when the embedder injects an adapter, so refusing them at load
|
|
48
|
+
* time would break legitimate embedders. Publish-time compliance already
|
|
49
|
+
* refuses the incoherent declaration. The `wasi` path, by contrast, is an
|
|
50
|
+
* inference the SDK makes on the author's behalf, and an inference must be
|
|
51
|
+
* conservative.
|
|
52
|
+
*/
|
|
53
|
+
const LEG_SATISFYING_TARGETS = Object.freeze({
|
|
54
|
+
browser: Object.freeze(["browser", "wasi"]),
|
|
55
|
+
wasmedge: Object.freeze(["wasmedge", "wasi"]),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const WASI_PORTABILITY_TARGET = "wasi";
|
|
59
|
+
|
|
60
|
+
function legIncompatibleCapabilities(capabilities, leg) {
|
|
61
|
+
const incompatible = LegIncompatibleCapabilityIds[leg];
|
|
62
|
+
if (!incompatible || !Array.isArray(capabilities)) return [];
|
|
63
|
+
return capabilities
|
|
64
|
+
.map((capability) =>
|
|
65
|
+
typeof capability === "string"
|
|
66
|
+
? capability
|
|
67
|
+
: (capability?.capabilityId ?? capability?.name ?? ""),
|
|
68
|
+
)
|
|
69
|
+
.filter((capability) => incompatible.includes(capability))
|
|
70
|
+
.sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Does a declared target set admit this leg?
|
|
75
|
+
*
|
|
76
|
+
* ONE RULE, used by the loaders AND by the flow compiler's derivation. Two
|
|
77
|
+
* rules for one field is how a manifest comes to mean different things to the
|
|
78
|
+
* compiler and to the door: the compiler stripped the browser leg from a
|
|
79
|
+
* `["wasi","wasmedge"]` part while the gate happily admitted the same
|
|
80
|
+
* declaration to the browser.
|
|
81
|
+
*
|
|
82
|
+
* An empty/absent declaration admits everything, matching compliance, which
|
|
83
|
+
* skips the runtime-target rule on an absent field.
|
|
84
|
+
*
|
|
85
|
+
* @param {string[]|undefined} targets declared runtimeTargets
|
|
86
|
+
* @param {string} leg the runtime target being asked about
|
|
87
|
+
* @param {Array<string|{capabilityId?: string, name?: string}>} [capabilities]
|
|
88
|
+
* the artifact's declared capabilities; consulted only to keep the `wasi`
|
|
89
|
+
* baseline from admitting a leg that cannot serve them
|
|
90
|
+
*/
|
|
91
|
+
export function runtimeTargetSatisfies(targets, leg, capabilities) {
|
|
92
|
+
if (!Array.isArray(targets) || targets.length === 0) return true;
|
|
93
|
+
const accepted = LEG_SATISFYING_TARGETS[leg] ?? [leg];
|
|
94
|
+
if (targets.includes(leg)) return true;
|
|
95
|
+
if (!targets.some((target) => accepted.includes(target))) return false;
|
|
96
|
+
// Admitted only via the portability baseline — prove it, do not assume it.
|
|
97
|
+
return (
|
|
98
|
+
targets.includes(WASI_PORTABILITY_TARGET) &&
|
|
99
|
+
legIncompatibleCapabilities(capabilities, leg).length === 0
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class RuntimeTargetError extends Error {
|
|
104
|
+
constructor(message, { declaredTargets, source, leg }) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = "RuntimeTargetError";
|
|
107
|
+
this.code = "runtime-target-out-of-scope";
|
|
108
|
+
this.declaredTargets = declaredTargets;
|
|
109
|
+
this.leg = leg;
|
|
110
|
+
// "embedded" (the artifact's own signed record) or "caller" (the manifest
|
|
111
|
+
// the loader was handed). Which one refused is evidence, not trivia.
|
|
112
|
+
this.declarationSource = source;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function normalizedRuntimeTargets(manifest) {
|
|
117
|
+
return Array.isArray(manifest?.runtimeTargets)
|
|
118
|
+
? manifest.runtimeTargets
|
|
119
|
+
.map((target) => String(target ?? "").trim().toLowerCase())
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
: [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The artifact's OWN declaration, read straight out of the embedded `$PLG`
|
|
126
|
+
* custom section, so a caller that hands a loader nothing but bytes still gets
|
|
127
|
+
* the refusal.
|
|
128
|
+
*
|
|
129
|
+
* @param {WebAssembly.Module} wasmModule
|
|
130
|
+
* @returns {string[]} declared targets, or [] when the artifact declares none
|
|
131
|
+
*/
|
|
132
|
+
export function embeddedPlgManifest(wasmModule) {
|
|
133
|
+
let sections;
|
|
134
|
+
try {
|
|
135
|
+
sections = WebAssembly.Module.customSections(
|
|
136
|
+
wasmModule,
|
|
137
|
+
SDS_MANIFEST_SECTION_NAME,
|
|
138
|
+
);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
for (const section of sections ?? []) {
|
|
143
|
+
try {
|
|
144
|
+
const bytes = new Uint8Array(section);
|
|
145
|
+
if (!isPlgManifestBuffer(bytes)) continue;
|
|
146
|
+
// FIRST DECODABLE MANIFEST WINS — the same rule the node-side locator
|
|
147
|
+
// (`locateEmbeddedPlgManifest`) applies. Skipping a manifest merely
|
|
148
|
+
// because its runtimeTargets are empty made this reader disagree with
|
|
149
|
+
// that one on the very same bytes, so an artifact could be scoped by one
|
|
150
|
+
// declaration and enforced against another.
|
|
151
|
+
return decodePlgManifest(bytes);
|
|
152
|
+
} catch {
|
|
153
|
+
// An UNDECODABLE section is not a declaration. Keep looking.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function embeddedRuntimeTargets(wasmModule) {
|
|
160
|
+
return normalizedRuntimeTargets(embeddedPlgManifest(wasmModule));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve which declaration refuses this leg, if either does.
|
|
165
|
+
*
|
|
166
|
+
* EITHER source can refuse, and the EMBEDDED one wins on conflict. The
|
|
167
|
+
* embedded `$PLG` is what the artifact's signature covers; `manifest` is
|
|
168
|
+
* caller input. Trusting the caller first would make
|
|
169
|
+
* `{manifest: {runtimeTargets: ["browser"]}}` a bypass for a signed
|
|
170
|
+
* WasmEdge-only artifact — and the isomorphic flow host passes exactly such a
|
|
171
|
+
* caller-side manifest for every child it mounts.
|
|
172
|
+
*
|
|
173
|
+
* An artifact that declares NOTHING is unconstrained, matching compliance,
|
|
174
|
+
* which skips the runtime-target rule on an absent field.
|
|
175
|
+
*
|
|
176
|
+
* @returns {{targets: string[], source: "embedded"|"caller"}|null}
|
|
177
|
+
*/
|
|
178
|
+
export function resolveRuntimeTargetRefusal({
|
|
179
|
+
wasmModule,
|
|
180
|
+
manifest,
|
|
181
|
+
leg,
|
|
182
|
+
embeddedManifest: providedEmbeddedManifest,
|
|
183
|
+
}) {
|
|
184
|
+
// A caller that already located the artifact's own manifest from BYTES (the
|
|
185
|
+
// node leg does, via `locateEmbeddedPlgManifest`) passes it here rather than
|
|
186
|
+
// making this gate compile the module just to read a custom section. The two
|
|
187
|
+
// locators apply the same first-decodable-wins rule; the byte-side one can
|
|
188
|
+
// additionally reach a manifest carried in a bundle entry or a data segment,
|
|
189
|
+
// which only ever makes that leg MORE likely to refuse — the safe direction.
|
|
190
|
+
const embeddedManifest =
|
|
191
|
+
providedEmbeddedManifest ??
|
|
192
|
+
(wasmModule ? embeddedPlgManifest(wasmModule) : null);
|
|
193
|
+
const embedded = normalizedRuntimeTargets(embeddedManifest);
|
|
194
|
+
if (!runtimeTargetSatisfies(embedded, leg, embeddedManifest?.capabilities)) {
|
|
195
|
+
return { targets: embedded, source: "embedded" };
|
|
196
|
+
}
|
|
197
|
+
const caller = normalizedRuntimeTargets(manifest);
|
|
198
|
+
if (!runtimeTargetSatisfies(caller, leg, manifest?.capabilities)) {
|
|
199
|
+
return { targets: caller, source: "caller" };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {object} options
|
|
206
|
+
* @param {WebAssembly.Module} [options.wasmModule] compiled artifact
|
|
207
|
+
* @param {object} [options.manifest] caller-supplied manifest
|
|
208
|
+
* @param {string} options.leg the runtime target this loader stands for
|
|
209
|
+
* @param {string} [options.what] noun used in the message ("module" by default)
|
|
210
|
+
*/
|
|
211
|
+
export function assertArtifactRuntimeTarget({
|
|
212
|
+
wasmModule,
|
|
213
|
+
manifest,
|
|
214
|
+
embeddedManifest,
|
|
215
|
+
leg,
|
|
216
|
+
what = "module",
|
|
217
|
+
}) {
|
|
218
|
+
const refusal = resolveRuntimeTargetRefusal({
|
|
219
|
+
wasmModule,
|
|
220
|
+
manifest,
|
|
221
|
+
embeddedManifest,
|
|
222
|
+
leg,
|
|
223
|
+
});
|
|
224
|
+
if (!refusal) return;
|
|
225
|
+
const { targets, source } = refusal;
|
|
226
|
+
throw new RuntimeTargetError(
|
|
227
|
+
`Refusing to load a ${what} that does not target "${leg}": its ` +
|
|
228
|
+
`${source === "embedded" ? "embedded manifest" : "supplied manifest"} declares ` +
|
|
229
|
+
`runtimeTargets [${targets.join(", ")}], which does not admit this leg. Loading it ` +
|
|
230
|
+
`here would trap on the first capability the "${leg}" host cannot serve. Run it on ` +
|
|
231
|
+
"a host that provides one of its declared targets, or split the out-of-scope " +
|
|
232
|
+
"surface into its own module.",
|
|
233
|
+
{ declaredTargets: targets, source, leg },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Is this JavaScript running in a browser (window or a Worker)? Used only to
|
|
239
|
+
* DEFAULT the leg for loaders that are genuinely runtime-agnostic; every
|
|
240
|
+
* caller may state its leg explicitly and that always wins.
|
|
241
|
+
*/
|
|
242
|
+
export function detectBrowserLeg() {
|
|
243
|
+
try {
|
|
244
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
const workerScope = globalThis.WorkerGlobalScope;
|
|
248
|
+
return (
|
|
249
|
+
typeof workerScope === "function" &&
|
|
250
|
+
typeof self !== "undefined" &&
|
|
251
|
+
self instanceof workerScope
|
|
252
|
+
);
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./sabHostcallChannel.js";
|
|
28
28
|
import { WASI_THREAD_HOSTCALL_MESSAGE } from "./wasiThreadWorkerRuntime.js";
|
|
29
29
|
import { importNodeBuiltin } from "./nodeBuiltinSpecifier.js";
|
|
30
|
+
import { assertBrowserRuntimeTarget } from "./browserModuleHarness.js";
|
|
30
31
|
|
|
31
32
|
const WORKER_URL = new URL("./workerModuleHarnessWorker.js", import.meta.url);
|
|
32
33
|
|
|
@@ -130,6 +131,12 @@ async function toWasmModule(source, label) {
|
|
|
130
131
|
*/
|
|
131
132
|
export async function createWorkerModuleHarness(options = {}) {
|
|
132
133
|
const wasmModule = await toWasmModule(options.wasmSource, "wasmSource");
|
|
134
|
+
// Fail on THIS thread, before a worker, a SAB channel and a BroadcastChannel
|
|
135
|
+
// are stood up for an artifact the browser leg must not run. The in-worker
|
|
136
|
+
// createBrowserModuleHarness would refuse it too, but a rejection that
|
|
137
|
+
// arrives after the whole rig exists reads like a worker fault rather than
|
|
138
|
+
// the declaration it actually is.
|
|
139
|
+
assertBrowserRuntimeTarget(wasmModule, options.harnessOptions?.manifest);
|
|
133
140
|
const host = options.host ?? createBrowserHost(options.hostOptions);
|
|
134
141
|
const dispatch = options.dispatchHost ?? createAsyncHostDispatcher(host);
|
|
135
142
|
const buffer = createSabHostcallBuffer({
|
package/src/index.d.ts
CHANGED
|
@@ -2337,8 +2337,55 @@ export interface FlowRuntimeHostOptions {
|
|
|
2337
2337
|
engineLink?: {
|
|
2338
2338
|
exports: WebAssembly.Exports & { memory: WebAssembly.Memory };
|
|
2339
2339
|
} | null;
|
|
2340
|
+
/**
|
|
2341
|
+
* The runtime leg this host stands for. A composed flow DERIVES its
|
|
2342
|
+
* runtimeTargets from its parts, so a single-leg artifact exists and must be
|
|
2343
|
+
* refused by name on any other leg. Omit and a real browser is detected and
|
|
2344
|
+
* gated; pass `null` to opt out explicitly.
|
|
2345
|
+
*/
|
|
2346
|
+
runtimeTarget?: string | null;
|
|
2347
|
+
/** Caller-side manifest; the artifact's embedded record wins on conflict. */
|
|
2348
|
+
manifest?: PluginManifest | Record<string, unknown>;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
export declare class RuntimeTargetError extends Error {
|
|
2352
|
+
code: "runtime-target-out-of-scope";
|
|
2353
|
+
declaredTargets: string[];
|
|
2354
|
+
leg: string;
|
|
2355
|
+
declarationSource: "embedded" | "caller";
|
|
2340
2356
|
}
|
|
2341
2357
|
|
|
2358
|
+
/**
|
|
2359
|
+
* Does a declared target set admit this leg? `wasi` is the portability
|
|
2360
|
+
* baseline and admits either leg, but only when `capabilities` carries nothing
|
|
2361
|
+
* that leg cannot serve.
|
|
2362
|
+
*/
|
|
2363
|
+
export function runtimeTargetSatisfies(
|
|
2364
|
+
targets: string[] | undefined,
|
|
2365
|
+
leg: string,
|
|
2366
|
+
capabilities?: Array<string | { capabilityId?: string; name?: string }>,
|
|
2367
|
+
): boolean;
|
|
2368
|
+
|
|
2369
|
+
/** The artifact's own declared targets, read from its embedded `$PLG`. */
|
|
2370
|
+
export function embeddedRuntimeTargets(wasmModule: WebAssembly.Module): string[];
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Which declaration refuses this leg, if either does. The artifact's embedded
|
|
2374
|
+
* record wins over a caller-supplied manifest.
|
|
2375
|
+
*/
|
|
2376
|
+
export function resolveRuntimeTargetRefusal(options: {
|
|
2377
|
+
wasmModule?: WebAssembly.Module;
|
|
2378
|
+
manifest?: PluginManifest | Record<string, unknown>;
|
|
2379
|
+
embeddedManifest?: PluginManifest | Record<string, unknown> | null;
|
|
2380
|
+
leg: string;
|
|
2381
|
+
}): { targets: string[]; source: "embedded" | "caller" } | null;
|
|
2382
|
+
|
|
2383
|
+
/** Throws a RuntimeTargetError when the artifact does not target the browser. */
|
|
2384
|
+
export function assertBrowserRuntimeTarget(
|
|
2385
|
+
wasmModule: WebAssembly.Module,
|
|
2386
|
+
manifest?: PluginManifest | Record<string, unknown>,
|
|
2387
|
+
): void;
|
|
2388
|
+
|
|
2342
2389
|
export const FLOW_INVALID_INDEX: number;
|
|
2343
2390
|
export function createFlowRuntimeHost(
|
|
2344
2391
|
options: FlowRuntimeHostOptions,
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,17 @@ export * from "./deployment/index.js";
|
|
|
16
16
|
export * from "./app/index.js";
|
|
17
17
|
export { FLOW_INVALID_INDEX, createFlowRuntimeHost } from "./flow/flowRuntimeHost.js";
|
|
18
18
|
export { createIsomorphicFlowRuntimeHost } from "./flow/isomorphicFlowHost.js";
|
|
19
|
+
// The runtime-target gate: a composed flow derives its runtimeTargets, so a
|
|
20
|
+
// single-leg artifact exists and every loader refuses one that is not its own.
|
|
21
|
+
// Exported so a consumer can catch the refusal by CLASS rather than by
|
|
22
|
+
// matching a message string, and can ask the same question before offering a
|
|
23
|
+
// module to a leg.
|
|
24
|
+
export {
|
|
25
|
+
RuntimeTargetError,
|
|
26
|
+
runtimeTargetSatisfies,
|
|
27
|
+
resolveRuntimeTargetRefusal,
|
|
28
|
+
embeddedRuntimeTargets,
|
|
29
|
+
} from "./host/runtimeTargetGate.js";
|
|
19
30
|
export {
|
|
20
31
|
DefaultInvokeExports,
|
|
21
32
|
DefaultManifestExports,
|
|
@@ -17,6 +17,7 @@ import { createBrowserModuleHarness } from "../host/browserModuleHarness.js";
|
|
|
17
17
|
const OK = "ok";
|
|
18
18
|
const GUEST_ERROR = "guest-error";
|
|
19
19
|
const TRAP = "trap";
|
|
20
|
+
const OUT_OF_SCOPE = "out-of-declared-scope";
|
|
20
21
|
|
|
21
22
|
function base64ToBytes(value) {
|
|
22
23
|
const binary = atob(String(value ?? ""));
|
|
@@ -42,6 +43,16 @@ function classifyBrowserError(error) {
|
|
|
42
43
|
if (error?.name === "WasiExitError") {
|
|
43
44
|
return { exitClass: GUEST_ERROR, exitDetail: `exit=${error.code}` };
|
|
44
45
|
}
|
|
46
|
+
// The harness refused the artifact because the ARTIFACT declares it does not
|
|
47
|
+
// run here. That is the contract working. Classing it TRAP would score a
|
|
48
|
+
// correct refusal as a P1 cross-runtime divergence — the gate failing the
|
|
49
|
+
// very artifacts the compiler now legitimately emits.
|
|
50
|
+
if (error?.name === "RuntimeTargetError") {
|
|
51
|
+
return {
|
|
52
|
+
exitClass: OUT_OF_SCOPE,
|
|
53
|
+
exitDetail: `declared runtimeTargets [${(error.declaredTargets ?? []).join(", ")}]`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
45
56
|
return {
|
|
46
57
|
exitClass: TRAP,
|
|
47
58
|
exitDetail: `${error?.name ?? "Error"}: ${error?.message ?? String(error)}`,
|