space-data-module-sdk 0.5.18 → 0.5.21

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.
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Browser host adapter for the SDN host contract.
3
+ *
4
+ * Mirrors the NodeHost public interface using browser-native Web APIs.
5
+ * Plugs into createJsonHostcallBridge() exactly like NodeHost does.
6
+ */
7
+
8
+ import { RuntimeTarget } from "../runtime/constants.js";
9
+ import {
10
+ parseCronExpression,
11
+ matchesCronExpression,
12
+ nextCronOccurrence,
13
+ } from "./cron.js";
14
+ import { createBrowserEdgeShims } from "./browserEdgeShims.js";
15
+
16
+ export const BrowserHostSupportedCapabilities = Object.freeze([
17
+ "clock",
18
+ "random",
19
+ "timers",
20
+ "schedule_cron",
21
+ "http",
22
+ "websocket",
23
+ "filesystem",
24
+ "context_read",
25
+ "context_write",
26
+ "crypto_hash",
27
+ "crypto_encrypt",
28
+ "crypto_decrypt",
29
+ "logging",
30
+ ]);
31
+
32
+ export const BrowserHostSupportedOperations = Object.freeze([
33
+ "host.runtimeTarget",
34
+ "host.listCapabilities",
35
+ "host.listSupportedCapabilities",
36
+ "host.listOperations",
37
+ "host.hasCapability",
38
+ "clock.now",
39
+ "clock.monotonicNow",
40
+ "clock.nowIso",
41
+ "random.bytes",
42
+ "schedule.parse",
43
+ "schedule.matches",
44
+ "schedule.next",
45
+ "http.request",
46
+ "websocket.exchange",
47
+ "filesystem.resolvePath",
48
+ "filesystem.readFile",
49
+ "filesystem.writeFile",
50
+ "filesystem.appendFile",
51
+ "filesystem.deleteFile",
52
+ "filesystem.mkdir",
53
+ "filesystem.readdir",
54
+ "filesystem.stat",
55
+ "filesystem.rename",
56
+ "context.get",
57
+ "context.set",
58
+ "context.delete",
59
+ "context.listKeys",
60
+ "context.listScopes",
61
+ "crypto.sha256",
62
+ "crypto.sha512",
63
+ "crypto.aesGcmEncrypt",
64
+ "crypto.aesGcmDecrypt",
65
+ ]);
66
+
67
+ export class BrowserHostCapabilityError extends Error {
68
+ constructor(capability, operation, message) {
69
+ super(message ?? `Capability "${capability}" is not available in this host.`);
70
+ this.name = "BrowserHostCapabilityError";
71
+ this.capability = capability;
72
+ this.operation = operation;
73
+ }
74
+ }
75
+
76
+ export class BrowserHost {
77
+ constructor(options = {}) {
78
+ this.runtimeTarget = RuntimeTarget.BROWSER;
79
+
80
+ const granted = options.capabilities
81
+ ? new Set(options.capabilities)
82
+ : new Set(BrowserHostSupportedCapabilities);
83
+ const edgeShims = createBrowserEdgeShims({
84
+ ...options.edgeShims,
85
+ fetch: options.fetch ?? options.edgeShims?.fetch,
86
+ WebSocket: options.WebSocket ?? options.edgeShims?.WebSocket,
87
+ crypto: options.crypto ?? options.edgeShims?.crypto,
88
+ performance: options.performance ?? options.edgeShims?.performance,
89
+ filesystem: options.filesystem ?? options.edgeShims?.filesystem,
90
+ filesystemRoot: options.filesystemRoot ?? options.edgeShims?.filesystemRoot,
91
+ });
92
+ const performanceApi = edgeShims.performance ?? {
93
+ now: () => Date.now(),
94
+ timeOrigin: 0,
95
+ };
96
+ const cryptoApi = edgeShims.crypto;
97
+ const fetchImpl = edgeShims.fetch;
98
+ const WebSocketImpl = edgeShims.WebSocket;
99
+ const filesystem = edgeShims.filesystem;
100
+
101
+ this._grantedCapabilities = granted;
102
+ this._contextStore = options.contextStore ?? new Map();
103
+ this.filesystemRoot = filesystem?.filesystemRoot ?? "/";
104
+
105
+ // --- Capability objects (frozen, browser-native) ---
106
+
107
+ this.clock = Object.freeze({
108
+ now: () => {
109
+ this.#assertCapability("clock", "clock.now");
110
+ return Date.now();
111
+ },
112
+ monotonicNow: () => {
113
+ this.#assertCapability("clock", "clock.monotonicNow");
114
+ return performanceApi.now();
115
+ },
116
+ nowIso: () => {
117
+ this.#assertCapability("clock", "clock.nowIso");
118
+ return new Date().toISOString();
119
+ },
120
+ });
121
+
122
+ this.random = Object.freeze({
123
+ bytes: (length) => {
124
+ this.#assertCapability("random", "random.bytes");
125
+ if (!cryptoApi?.getRandomValues) {
126
+ throw new Error("No crypto.getRandomValues implementation is available.");
127
+ }
128
+ const len = Number(length) || 32;
129
+ const buf = new Uint8Array(len);
130
+ cryptoApi.getRandomValues(buf);
131
+ return buf;
132
+ },
133
+ });
134
+
135
+ this.timers = Object.freeze({
136
+ delay: (ms) => {
137
+ this.#assertCapability("timers", "timers.delay");
138
+ return new Promise((resolve) => setTimeout(resolve, ms));
139
+ },
140
+ });
141
+
142
+ this.schedule = Object.freeze({
143
+ parse: (expression) => {
144
+ this.#assertCapability("schedule_cron", "schedule.parse");
145
+ return parseCronExpression(expression);
146
+ },
147
+ matches: (expression, date) => {
148
+ this.#assertCapability("schedule_cron", "schedule.matches");
149
+ return matchesCronExpression(expression, date ? new Date(date) : new Date());
150
+ },
151
+ next: (expression, from) => {
152
+ this.#assertCapability("schedule_cron", "schedule.next");
153
+ return nextCronOccurrence(expression, from ? new Date(from) : new Date());
154
+ },
155
+ });
156
+
157
+ this.http = Object.freeze({
158
+ request: async (params) => {
159
+ this.#assertCapability("http", "http.request");
160
+ if (typeof fetchImpl !== "function") {
161
+ throw new Error("No fetch implementation is available for the browser host.");
162
+ }
163
+ const controller = new AbortController();
164
+ const timeout = params.timeoutMs
165
+ ? setTimeout(() => controller.abort(), params.timeoutMs)
166
+ : null;
167
+ try {
168
+ const response = await fetchImpl(params.url, {
169
+ method: params.method ?? "GET",
170
+ headers: params.headers ?? undefined,
171
+ body: params.body ?? undefined,
172
+ signal: controller.signal,
173
+ });
174
+ const responseType = params.responseType ?? "utf8";
175
+ let body;
176
+ if (responseType === "json") body = await response.json();
177
+ else if (responseType === "bytes") body = new Uint8Array(await response.arrayBuffer());
178
+ else body = await response.text();
179
+ return {
180
+ status: response.status,
181
+ statusText: response.statusText,
182
+ ok: response.ok,
183
+ headers: Object.fromEntries(response.headers.entries()),
184
+ body,
185
+ };
186
+ } finally {
187
+ if (timeout) clearTimeout(timeout);
188
+ }
189
+ },
190
+ });
191
+
192
+ this.websocket = Object.freeze({
193
+ exchange: async (params) => {
194
+ this.#assertCapability("websocket", "websocket.exchange");
195
+ if (!WebSocketImpl) {
196
+ throw new Error("No WebSocket implementation is available for the browser host.");
197
+ }
198
+ return new Promise((resolve, reject) => {
199
+ const ws = new WebSocketImpl(params.url, params.protocols ?? undefined);
200
+ const timeout = params.timeoutMs
201
+ ? setTimeout(() => {
202
+ ws.close();
203
+ reject(new Error("WebSocket exchange timed out."));
204
+ }, params.timeoutMs)
205
+ : null;
206
+
207
+ ws.onopen = () => {
208
+ if (params.message != null) ws.send(params.message);
209
+ if (!params.expectResponse) {
210
+ if (timeout) clearTimeout(timeout);
211
+ ws.close();
212
+ resolve({
213
+ url: params.url,
214
+ protocol: ws.protocol ?? "",
215
+ extensions: ws.extensions ?? "",
216
+ closeCode: null,
217
+ closeReason: "",
218
+ body: null,
219
+ });
220
+ }
221
+ };
222
+ ws.onmessage = (event) => {
223
+ if (timeout) clearTimeout(timeout);
224
+ ws.close();
225
+ resolve({
226
+ url: params.url,
227
+ protocol: ws.protocol ?? "",
228
+ extensions: ws.extensions ?? "",
229
+ closeCode: null,
230
+ closeReason: "",
231
+ body: event.data,
232
+ });
233
+ };
234
+ ws.onerror = (event) => {
235
+ if (timeout) clearTimeout(timeout);
236
+ reject(new Error(`WebSocket error: ${event.type}`));
237
+ };
238
+ });
239
+ },
240
+ });
241
+
242
+ this.context = Object.freeze({
243
+ get: (scope, key) => {
244
+ this.#assertCapability("context_read", "context.get");
245
+ const scopeMap = this._contextStore.get(scope);
246
+ return scopeMap ? (scopeMap.get(key) ?? null) : null;
247
+ },
248
+ set: (scope, key, value) => {
249
+ this.#assertCapability("context_write", "context.set");
250
+ if (!this._contextStore.has(scope)) this._contextStore.set(scope, new Map());
251
+ this._contextStore.get(scope).set(key, value);
252
+ },
253
+ delete: (scope, key) => {
254
+ this.#assertCapability("context_write", "context.delete");
255
+ const scopeMap = this._contextStore.get(scope);
256
+ if (scopeMap) scopeMap.delete(key);
257
+ },
258
+ listKeys: (scope) => {
259
+ this.#assertCapability("context_read", "context.listKeys");
260
+ const scopeMap = this._contextStore.get(scope);
261
+ return scopeMap ? [...scopeMap.keys()] : [];
262
+ },
263
+ listScopes: () => {
264
+ this.#assertCapability("context_read", "context.listScopes");
265
+ return [...this._contextStore.keys()];
266
+ },
267
+ });
268
+
269
+ this.crypto = Object.freeze({
270
+ sha256: async (data) => {
271
+ this.#assertCapability("crypto_hash", "crypto.sha256");
272
+ if (!cryptoApi?.subtle) {
273
+ throw new Error("No Web Crypto subtle implementation is available.");
274
+ }
275
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
276
+ return new Uint8Array(await cryptoApi.subtle.digest("SHA-256", bytes));
277
+ },
278
+ sha512: async (data) => {
279
+ this.#assertCapability("crypto_hash", "crypto.sha512");
280
+ if (!cryptoApi?.subtle) {
281
+ throw new Error("No Web Crypto subtle implementation is available.");
282
+ }
283
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
284
+ return new Uint8Array(await cryptoApi.subtle.digest("SHA-512", bytes));
285
+ },
286
+ aesGcmEncrypt: async (params) => {
287
+ this.#assertCapability("crypto_encrypt", "crypto.aesGcmEncrypt");
288
+ if (!cryptoApi?.subtle || !cryptoApi?.getRandomValues) {
289
+ throw new Error("No Web Crypto implementation is available.");
290
+ }
291
+ const key = await cryptoApi.subtle.importKey(
292
+ "raw",
293
+ params.key,
294
+ { name: "AES-GCM" },
295
+ false,
296
+ ["encrypt"],
297
+ );
298
+ const iv = params.iv ?? cryptoApi.getRandomValues(new Uint8Array(12));
299
+ const ciphertext = new Uint8Array(
300
+ await cryptoApi.subtle.encrypt({ name: "AES-GCM", iv }, key, params.plaintext),
301
+ );
302
+ return { ciphertext, iv };
303
+ },
304
+ aesGcmDecrypt: async (params) => {
305
+ this.#assertCapability("crypto_decrypt", "crypto.aesGcmDecrypt");
306
+ if (!cryptoApi?.subtle) {
307
+ throw new Error("No Web Crypto subtle implementation is available.");
308
+ }
309
+ const key = await cryptoApi.subtle.importKey(
310
+ "raw",
311
+ params.key,
312
+ { name: "AES-GCM" },
313
+ false,
314
+ ["decrypt"],
315
+ );
316
+ return new Uint8Array(
317
+ await cryptoApi.subtle.decrypt(
318
+ { name: "AES-GCM", iv: params.iv },
319
+ key,
320
+ params.ciphertext,
321
+ ),
322
+ );
323
+ },
324
+ });
325
+
326
+ this.filesystem = Object.freeze({
327
+ resolvePath: (path) => {
328
+ this.#assertCapability("filesystem", "filesystem.resolvePath");
329
+ return filesystem.resolvePath(path);
330
+ },
331
+ readFile: async (path, options) => {
332
+ this.#assertCapability("filesystem", "filesystem.readFile");
333
+ return filesystem.readFile(path, options);
334
+ },
335
+ writeFile: async (path, value, options) => {
336
+ this.#assertCapability("filesystem", "filesystem.writeFile");
337
+ return filesystem.writeFile(path, value, options);
338
+ },
339
+ appendFile: async (path, value, options) => {
340
+ this.#assertCapability("filesystem", "filesystem.appendFile");
341
+ return filesystem.appendFile(path, value, options);
342
+ },
343
+ deleteFile: async (path) => {
344
+ this.#assertCapability("filesystem", "filesystem.deleteFile");
345
+ return filesystem.deleteFile(path);
346
+ },
347
+ mkdir: async (path, options) => {
348
+ this.#assertCapability("filesystem", "filesystem.mkdir");
349
+ return filesystem.mkdir(path, options);
350
+ },
351
+ readdir: async (path = ".") => {
352
+ this.#assertCapability("filesystem", "filesystem.readdir");
353
+ return filesystem.readdir(path);
354
+ },
355
+ stat: async (path) => {
356
+ this.#assertCapability("filesystem", "filesystem.stat");
357
+ return filesystem.stat(path);
358
+ },
359
+ rename: async (fromPath, toPath) => {
360
+ this.#assertCapability("filesystem", "filesystem.rename");
361
+ return filesystem.rename(fromPath, toPath);
362
+ },
363
+ });
364
+ }
365
+
366
+ // --- Public interface (mirrors NodeHost) ---
367
+
368
+ listCapabilities() {
369
+ return [...this._grantedCapabilities];
370
+ }
371
+
372
+ listSupportedCapabilities() {
373
+ return [...BrowserHostSupportedCapabilities];
374
+ }
375
+
376
+ listOperations() {
377
+ return [...BrowserHostSupportedOperations];
378
+ }
379
+
380
+ hasCapability(capability) {
381
+ return this._grantedCapabilities.has(capability);
382
+ }
383
+
384
+ assertCapability(capability, operation) {
385
+ this.#assertCapability(capability, operation);
386
+ }
387
+
388
+ // --- Private ---
389
+
390
+ #assertCapability(capability, operation) {
391
+ if (!this._grantedCapabilities.has(capability)) {
392
+ throw new BrowserHostCapabilityError(capability, operation);
393
+ }
394
+ }
395
+ }
396
+
397
+ export function createBrowserHost(options = {}) {
398
+ return new BrowserHost(options);
399
+ }
package/src/host/index.js CHANGED
@@ -1,3 +1,7 @@
1
1
  export * from "./cron.js";
2
2
  export * from "./nodeHost.js";
3
+ export * from "./browserHost.js";
4
+ export * from "./browserEdgeShims.js";
5
+ export * from "./wasiShim.js";
6
+ export * from "./isomorphicLoader.js";
3
7
  export * from "./abi.js";
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Isomorphic module loader.
3
+ *
4
+ * Unified entry point that detects the runtime environment and artifact
5
+ * profile, then loads the module through the appropriate path:
6
+ * - Browser: createBrowserModuleHarness (WASI shim + optional sdn_host)
7
+ * - Node/WasmEdge: createModuleHarness (subprocess)
8
+ *
9
+ * The same compiled .wasm artifact works in both environments.
10
+ */
11
+
12
+ import {
13
+ createBrowserModuleHarness,
14
+ detectArtifactProfile,
15
+ } from "../testing/browserModuleHarness.js";
16
+
17
+ const isBrowser =
18
+ typeof globalThis.window !== "undefined" &&
19
+ typeof globalThis.document !== "undefined";
20
+
21
+ async function createWasmEdgeCommandHarness(options = {}) {
22
+ const [
23
+ { spawn },
24
+ { readFile },
25
+ pathModule,
26
+ { encodePluginInvokeRequest, decodePluginInvokeResponse },
27
+ { buildWasmEdgeSpawnEnv },
28
+ { DefaultInvokeExports },
29
+ { toUint8Array },
30
+ ] = await Promise.all([
31
+ import("node:child_process"),
32
+ import("node:fs/promises"),
33
+ import("node:path"),
34
+ import("../invoke/codec.js"),
35
+ import("../testing/processInvoke.js"),
36
+ import("../runtime/constants.js"),
37
+ import("../runtime/bufferLike.js"),
38
+ ]);
39
+ const path = pathModule.default ?? pathModule;
40
+ const wasmPath = path.resolve(String(options.wasmSource));
41
+ const wasmBytes = await readFile(wasmPath);
42
+ const inspection = await inspectModule(wasmBytes);
43
+
44
+ if (!inspection.exports.includes(DefaultInvokeExports.commandSymbol)) {
45
+ throw new Error(
46
+ "Standalone WasmEdge loading requires a command-surface artifact with the _start export.",
47
+ );
48
+ }
49
+
50
+ const command = options.wasmEdgeBinary ?? "wasmedge";
51
+ const args = [
52
+ ...(options.enableThreads === false ? [] : ["--enable-threads"]),
53
+ wasmPath,
54
+ ...(Array.isArray(options.args) ? options.args : []),
55
+ ];
56
+ const launchPlan = {
57
+ command,
58
+ args,
59
+ env: buildWasmEdgeSpawnEnv(options.env),
60
+ cwd: options.cwd ?? process.cwd(),
61
+ wasmPath,
62
+ };
63
+
64
+ async function invokeRaw(requestBytes) {
65
+ const normalizedRequest = toUint8Array(requestBytes);
66
+ if (!normalizedRequest) {
67
+ throw new TypeError(
68
+ "Expected Uint8Array, ArrayBufferView, or ArrayBuffer request bytes.",
69
+ );
70
+ }
71
+
72
+ return new Promise((resolve, reject) => {
73
+ const child = spawn(launchPlan.command, launchPlan.args, {
74
+ cwd: launchPlan.cwd,
75
+ env: launchPlan.env,
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ });
78
+ const stdoutChunks = [];
79
+ const stderrChunks = [];
80
+
81
+ function formatFailure(message, cause = null) {
82
+ const stderrText = Buffer.concat(stderrChunks).toString("utf8").trim();
83
+ const details = stderrText ? `${message}\n${stderrText}` : message;
84
+ return cause ? new Error(details, { cause }) : new Error(details);
85
+ }
86
+
87
+ child.stdout.on("data", (chunk) => {
88
+ stdoutChunks.push(Buffer.from(chunk));
89
+ });
90
+ child.stderr.on("data", (chunk) => {
91
+ stderrChunks.push(Buffer.from(chunk));
92
+ });
93
+ child.on("error", (error) => {
94
+ reject(
95
+ formatFailure(
96
+ "Failed to launch WasmEdge command harness.",
97
+ error,
98
+ ),
99
+ );
100
+ });
101
+ child.on("close", (code, signal) => {
102
+ if (code !== 0 || signal !== null) {
103
+ reject(
104
+ formatFailure(
105
+ `WasmEdge command harness exited with ${
106
+ signal ? `signal ${signal}` : `code ${code}`
107
+ }.`,
108
+ ),
109
+ );
110
+ return;
111
+ }
112
+ resolve(new Uint8Array(Buffer.concat(stdoutChunks)));
113
+ });
114
+ child.stdin.end(Buffer.from(normalizedRequest));
115
+ });
116
+ }
117
+
118
+ return {
119
+ runtime: {
120
+ kind: "wasmedge",
121
+ profile: inspection.profile,
122
+ surface: "command",
123
+ },
124
+ launchPlan,
125
+ invokeRaw,
126
+ async invoke(request = {}) {
127
+ const requestBytes = encodePluginInvokeRequest(request);
128
+ const responseBytes = await invokeRaw(requestBytes);
129
+ return decodePluginInvokeResponse(responseBytes);
130
+ },
131
+ readManifest() {
132
+ return null;
133
+ },
134
+ async destroy() {},
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Load a WASM module isomorphically.
140
+ *
141
+ * @param {Object} options
142
+ * @param {Uint8Array|ArrayBuffer|Response|string|WebAssembly.Module} options.wasmSource
143
+ * The WASM artifact — same binary for all runtimes.
144
+ * @param {Object} [options.host] - Host instance (BrowserHost or NodeHost).
145
+ * @param {string[]} [options.args] - WASI args.
146
+ * @param {Object} [options.env] - WASI environment variables.
147
+ * @param {string} [options.surface] - "direct" or "command".
148
+ * @param {Object} [options.runtimeHost] - Runtime host for row/region ops.
149
+ * @returns {Promise<Object>} Harness with invoke(), readManifest(), destroy().
150
+ */
151
+ export async function loadModule(options = {}) {
152
+ if (isBrowser) {
153
+ return createBrowserModuleHarness(options);
154
+ }
155
+
156
+ const { createModuleHarness } = await import("../testing/moduleHarness.js");
157
+ const source = options.wasmSource;
158
+ if (typeof source !== "string") {
159
+ throw new TypeError(
160
+ "Server-side isomorphic loader expects a file path string for wasmSource.",
161
+ );
162
+ }
163
+
164
+ const runtimeKind = options.runtimeKind ?? "wasmedge";
165
+ if (runtimeKind === "wasmedge") {
166
+ const runtimeHostRequested =
167
+ String(options.hostProfile ?? "").trim().toLowerCase() === "runtime-host" ||
168
+ Array.isArray(options.modules) ||
169
+ (typeof options.defaultModuleId === "string" &&
170
+ options.defaultModuleId.trim().length > 0);
171
+ if (!runtimeHostRequested && !options.wasmEdgeRunnerBinary) {
172
+ const { readFile } = await import("node:fs/promises");
173
+ const inspection = await inspectModule(await readFile(source));
174
+ if (
175
+ (inspection.profile === "standalone" || inspection.profile === "sdn-abi") &&
176
+ inspection.exports.includes("_start")
177
+ ) {
178
+ return createWasmEdgeCommandHarness(options);
179
+ }
180
+ }
181
+
182
+ return createModuleHarness({
183
+ runtime: {
184
+ kind: "wasmedge",
185
+ wasmPath: source,
186
+ wasmEdgeBinary: options.wasmEdgeBinary,
187
+ wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
188
+ enableThreads: options.enableThreads,
189
+ env: options.env,
190
+ cwd: options.cwd,
191
+ hostProfile: options.hostProfile,
192
+ modules: options.modules,
193
+ defaultModuleId: options.defaultModuleId,
194
+ metadata: options.metadata,
195
+ },
196
+ });
197
+ }
198
+
199
+ return createModuleHarness({
200
+ runtime: {
201
+ kind: runtimeKind,
202
+ command: options.command ?? runtimeKind,
203
+ args: options.args ?? [source],
204
+ env: options.env,
205
+ cwd: options.cwd,
206
+ hostProfile: options.hostProfile,
207
+ modules: options.modules,
208
+ defaultModuleId: options.defaultModuleId,
209
+ },
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Inspect a WASM module's artifact profile without instantiating it.
215
+ *
216
+ * @param {Uint8Array|ArrayBuffer|WebAssembly.Module} source
217
+ * @returns {Promise<{profile: string, exports: string[], imports: Array}>}
218
+ */
219
+ export async function inspectModule(source) {
220
+ let wasmModule;
221
+ if (source instanceof WebAssembly.Module) {
222
+ wasmModule = source;
223
+ } else {
224
+ const bytes =
225
+ source instanceof ArrayBuffer ? new Uint8Array(source) : source;
226
+ wasmModule = await WebAssembly.compile(bytes);
227
+ }
228
+
229
+ const profile = detectArtifactProfile(wasmModule);
230
+ const exports = WebAssembly.Module.exports(wasmModule).map((e) => e.name);
231
+ const imports = WebAssembly.Module.imports(wasmModule);
232
+
233
+ return { profile, exports, imports };
234
+ }