space-data-module-sdk 0.5.4 → 0.5.8

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 CHANGED
@@ -159,6 +159,26 @@ environments with socket/TLS extensions, while plain `wasi` remains the strict
159
159
  no-wrapper baseline. The Node-RED-oriented parity map lives in
160
160
  [`docs/node-red-default-node-parity.md`](./docs/node-red-default-node-parity.md).
161
161
 
162
+ ## WasmEdge Pthreads
163
+
164
+ `space-data-module-sdk` is the source of truth for module thread-model
165
+ selection.
166
+
167
+ - `compileModuleFromSource({ threadModel })` accepts an explicit thread model.
168
+ - If `threadModel` is omitted, the SDK resolves it from `manifest.runtimeTargets`.
169
+ - `runtimeTargets: ["wasmedge"]` defaults to `emscripten-pthreads`.
170
+ - Other targets currently default to `single-thread`.
171
+
172
+ WasmEdge-targeted pthread builds do not use the embedded `sdn-emception`
173
+ toolchain. They require a real system Emscripten installation on `PATH`, and
174
+ the compiler result plus guest-link bundle metadata preserve the selected
175
+ `threadModel`.
176
+
177
+ This SDK does not treat Cesium `TaskProcessor`, ad hoc JS worker pools, or
178
+ host-side fake orchestration as a substitute for guest pthread support. If a
179
+ runtime cannot interoperate with the guest contract directly, document that as a
180
+ wrapper requirement instead of changing the guest artifact semantics.
181
+
162
182
  ## Install
163
183
 
164
184
  ```bash
@@ -186,6 +206,8 @@ const compilation = await compileModuleFromSource({
186
206
  manifest,
187
207
  sourceCode,
188
208
  language: "c",
209
+ // Optional. Defaults from manifest.runtimeTargets.
210
+ // threadModel: "emscripten-pthreads",
189
211
  });
190
212
 
191
213
  const bundle = await createSingleFileBundle({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.5.4",
3
+ "version": "0.5.8",
4
4
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
5
5
  "type": "module",
6
6
  "types": "./src/index.d.ts",
@@ -1,7 +1,11 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
3
5
  import {
4
6
  mkdtemp,
7
+ mkdir,
8
+ readFile,
5
9
  rm,
6
10
  writeFile,
7
11
  } from "node:fs/promises";
@@ -24,7 +28,11 @@ import {
24
28
  } from "./flatcSupport.js";
25
29
  import { runWithEmceptionLock } from "./emceptionNode.js";
26
30
  import { encodePluginManifest, toEmbeddedPluginManifest } from "../manifest/index.js";
27
- import { DefaultInvokeExports, InvokeSurface } from "../runtime/constants.js";
31
+ import {
32
+ DefaultInvokeExports,
33
+ InvokeSurface,
34
+ RuntimeTarget,
35
+ } from "../runtime/constants.js";
28
36
  import {
29
37
  appendPublicationRecordCollection,
30
38
  createEncryptedEnvelopePayload,
@@ -55,6 +63,12 @@ import { sha256Bytes } from "../utils/crypto.js";
55
63
  import { getWasmWallet } from "../utils/wasmCrypto.js";
56
64
 
57
65
  const C_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
66
+ const execFileAsync = promisify(execFile);
67
+
68
+ export const ModuleThreadModel = Object.freeze({
69
+ SINGLE_THREAD: "single-thread",
70
+ EMSCRIPTEN_PTHREADS: "emscripten-pthreads",
71
+ });
58
72
 
59
73
  function selectCompiler(language) {
60
74
  const normalized = String(language ?? "c").trim().toLowerCase();
@@ -81,16 +95,63 @@ function buildCompilerArgs(exportedSymbols, options = {}) {
81
95
  (symbol) => "-Wl,--export=" + symbol,
82
96
  );
83
97
  const extraArgs = [];
98
+ const threadArgs = [];
99
+ if (options.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS) {
100
+ threadArgs.push("-pthread");
101
+ }
84
102
  if (options.allowUndefinedImports === true) {
85
103
  extraArgs.push("-s", "ERROR_ON_UNDEFINED_SYMBOLS=0", "-Wl,--allow-undefined");
86
104
  }
87
- const args = ["-O2", "-s", "STANDALONE_WASM=1", ...extraArgs, ...linkerExports];
105
+ const args = [
106
+ "-O2",
107
+ ...threadArgs,
108
+ "-s",
109
+ "STANDALONE_WASM=1",
110
+ ...extraArgs,
111
+ ...linkerExports,
112
+ ];
88
113
  if (options.noEntry === true) {
89
114
  args.splice(1, 0, "--no-entry");
90
115
  }
91
116
  return args;
92
117
  }
93
118
 
119
+ function normalizeThreadModel(value) {
120
+ if (value === undefined || value === null || value === "") {
121
+ return null;
122
+ }
123
+ const normalized = String(value).trim().toLowerCase();
124
+ if (normalized === ModuleThreadModel.SINGLE_THREAD) {
125
+ return ModuleThreadModel.SINGLE_THREAD;
126
+ }
127
+ if (normalized === ModuleThreadModel.EMSCRIPTEN_PTHREADS) {
128
+ return ModuleThreadModel.EMSCRIPTEN_PTHREADS;
129
+ }
130
+ throw new Error(
131
+ `Unsupported threadModel "${value}". Expected "${ModuleThreadModel.SINGLE_THREAD}" or "${ModuleThreadModel.EMSCRIPTEN_PTHREADS}".`,
132
+ );
133
+ }
134
+
135
+ function resolveThreadModel({ manifest, threadModel } = {}) {
136
+ const explicit = normalizeThreadModel(threadModel);
137
+ if (explicit) {
138
+ return explicit;
139
+ }
140
+ const runtimeTargets = Array.isArray(manifest?.runtimeTargets)
141
+ ? manifest.runtimeTargets
142
+ .map((target) => String(target ?? "").trim().toLowerCase())
143
+ .filter(Boolean)
144
+ : [];
145
+ if (runtimeTargets.includes(RuntimeTarget.WASMEDGE)) {
146
+ return ModuleThreadModel.EMSCRIPTEN_PTHREADS;
147
+ }
148
+ return ModuleThreadModel.SINGLE_THREAD;
149
+ }
150
+
151
+ function requiresSystemEmscripten(threadModel) {
152
+ return threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS;
153
+ }
154
+
94
155
  function guestLinkSymbolPrefix(pluginId) {
95
156
  const normalized = String(pluginId ?? "module");
96
157
  const ascii = Array.from(new TextEncoder().encode(normalized))
@@ -280,6 +341,7 @@ function createGuestLinkBundleEntries(guestLink) {
280
341
  methodSymbols: guestLink.methodSymbols,
281
342
  methodIds: Object.keys(guestLink.methodSymbols ?? {}),
282
343
  language: guestLink.language,
344
+ threadModel: guestLink.threadModel ?? ModuleThreadModel.SINGLE_THREAD,
283
345
  },
284
346
  description: "Guest-link metadata for monolithic flow linking.",
285
347
  },
@@ -302,6 +364,14 @@ async function writeFilesToEmception(emception, rootDir, files) {
302
364
  }
303
365
  }
304
366
 
367
+ async function writeFilesToDirectory(rootDir, files) {
368
+ for (const [relativePath, content] of Object.entries(files)) {
369
+ const filePath = path.join(rootDir, relativePath);
370
+ await mkdir(path.dirname(filePath), { recursive: true });
371
+ await writeFile(filePath, content);
372
+ }
373
+ }
374
+
305
375
  function removeEmceptionDirectory(emception, directoryPath) {
306
376
  if (!emception.FS.analyzePath(directoryPath).exists) {
307
377
  return;
@@ -466,6 +536,8 @@ async function compileWithEmception(options = {}) {
466
536
  language,
467
537
  symbolPrefix: guestLink.prefix,
468
538
  methodSymbols: guestLink.methodSymbols,
539
+ threadModel:
540
+ compileOptions.threadModel ?? ModuleThreadModel.SINGLE_THREAD,
469
541
  objectBytes: linkObjectBytes,
470
542
  },
471
543
  };
@@ -483,6 +555,166 @@ async function compileWithEmception(options = {}) {
483
555
  }
484
556
  }
485
557
 
558
+ async function ensureSystemCompilerAvailable(command) {
559
+ try {
560
+ await execFileAsync(command, ["--version"]);
561
+ } catch (error) {
562
+ if (error?.code === "ENOENT") {
563
+ throw new Error(
564
+ `System Emscripten toolchain is required for "${ModuleThreadModel.EMSCRIPTEN_PTHREADS}" builds, but "${command}" was not found on PATH.`,
565
+ );
566
+ }
567
+ throw error;
568
+ }
569
+ }
570
+
571
+ async function runSystemCompiler(command, args, options = {}) {
572
+ try {
573
+ await execFileAsync(command, args, {
574
+ cwd: options.cwd,
575
+ maxBuffer: 32 * 1024 * 1024,
576
+ });
577
+ } catch (error) {
578
+ const stderr = typeof error?.stderr === "string" ? error.stderr.trim() : "";
579
+ const stdout = typeof error?.stdout === "string" ? error.stdout.trim() : "";
580
+ const detail = stderr || stdout || error?.message || "unknown error";
581
+ throw new Error(
582
+ `Compilation failed with ${command} (system emscripten): ${detail}`,
583
+ );
584
+ }
585
+ }
586
+
587
+ async function compileWithSystemEmscripten(options = {}) {
588
+ const {
589
+ manifest,
590
+ language,
591
+ sourceCompilerCommand,
592
+ sourceExtension,
593
+ sourceCode,
594
+ manifestSource,
595
+ invokeHeaderSource,
596
+ invokeSource,
597
+ exportedSymbols,
598
+ outputPath,
599
+ compileOptions,
600
+ } = options;
601
+ const tempDir = await mkdtemp(
602
+ path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
603
+ );
604
+ const resolvedOutputPath = path.resolve(
605
+ outputPath ?? path.join(tempDir, "module.wasm"),
606
+ );
607
+ const runtimeIncludeDir = path.join(tempDir, "flatbuffers-runtime");
608
+ const sourcePath = path.join(tempDir, `module.${sourceExtension}`);
609
+ const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.cpp");
610
+ const invokeHeaderPath = path.join(tempDir, "space_data_module_invoke.h");
611
+ const invokeSourcePath = path.join(tempDir, "plugin-invoke-bridge.cpp");
612
+ const sourceObjectPath = path.join(tempDir, "module.o");
613
+ const linkObjectPath = path.join(tempDir, "module-link.o");
614
+ const manifestObjectPath = path.join(tempDir, "plugin-manifest-exports.o");
615
+ const invokeObjectPath = path.join(tempDir, "plugin-invoke-bridge.o");
616
+ const wasmOutputPath = path.join(tempDir, "module.wasm");
617
+
618
+ try {
619
+ await ensureSystemCompilerAvailable(sourceCompilerCommand);
620
+ await ensureSystemCompilerAvailable("em++");
621
+ const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
622
+ const args = buildCompilerArgs(exportedSymbols, compileOptions);
623
+ await writeFilesToDirectory(runtimeIncludeDir, runtimeHeaders);
624
+ await writeFilesToDirectory(tempDir, schemaHeaders);
625
+ await writeFile(sourcePath, sourceCode);
626
+ await writeFile(manifestSourcePath, manifestSource);
627
+ await writeFile(invokeHeaderPath, invokeHeaderSource);
628
+ await writeFile(invokeSourcePath, invokeSource);
629
+
630
+ const sourceCompileArgs = [
631
+ "-c",
632
+ sourcePath,
633
+ `-I${tempDir}`,
634
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
635
+ ? ["-pthread"]
636
+ : []),
637
+ "-o",
638
+ sourceObjectPath,
639
+ ];
640
+ await runSystemCompiler(sourceCompilerCommand, sourceCompileArgs);
641
+
642
+ const sourceObjectBytes = new Uint8Array(await readFile(sourceObjectPath));
643
+ const guestLink = deriveGuestLinkRenameArgs({
644
+ objectBytes: sourceObjectBytes,
645
+ pluginId: manifest?.pluginId,
646
+ methodIds: Array.isArray(manifest?.methods)
647
+ ? manifest.methods.map((method) => String(method?.methodId ?? ""))
648
+ : [],
649
+ });
650
+
651
+ const linkCompileArgs = [
652
+ "-c",
653
+ sourcePath,
654
+ `-I${tempDir}`,
655
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
656
+ ? ["-pthread"]
657
+ : []),
658
+ ...guestLink.renameArgs,
659
+ "-o",
660
+ linkObjectPath,
661
+ ];
662
+ await runSystemCompiler(sourceCompilerCommand, linkCompileArgs);
663
+ await runSystemCompiler("em++", [
664
+ "-c",
665
+ manifestSourcePath,
666
+ "-std=c++17",
667
+ `-I${tempDir}`,
668
+ `-I${runtimeIncludeDir}`,
669
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
670
+ ? ["-pthread"]
671
+ : []),
672
+ "-o",
673
+ manifestObjectPath,
674
+ ]);
675
+ await runSystemCompiler("em++", [
676
+ "-c",
677
+ invokeSourcePath,
678
+ "-std=c++17",
679
+ `-I${tempDir}`,
680
+ `-I${runtimeIncludeDir}`,
681
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
682
+ ? ["-pthread"]
683
+ : []),
684
+ "-o",
685
+ invokeObjectPath,
686
+ ]);
687
+ await runSystemCompiler("em++", [
688
+ sourceObjectPath,
689
+ manifestObjectPath,
690
+ invokeObjectPath,
691
+ ...args,
692
+ "-o",
693
+ wasmOutputPath,
694
+ ]);
695
+
696
+ const wasmBytes = new Uint8Array(await readFile(wasmOutputPath));
697
+ const linkObjectBytes = new Uint8Array(await readFile(linkObjectPath));
698
+ await writeFile(resolvedOutputPath, wasmBytes);
699
+ return {
700
+ wasmBytes,
701
+ outputPath: resolvedOutputPath,
702
+ tempDir,
703
+ guestLink: {
704
+ format: "wasm-object",
705
+ language,
706
+ symbolPrefix: guestLink.prefix,
707
+ methodSymbols: guestLink.methodSymbols,
708
+ threadModel: compileOptions.threadModel,
709
+ objectBytes: linkObjectBytes,
710
+ },
711
+ };
712
+ } catch (error) {
713
+ await rm(tempDir, { recursive: true, force: true });
714
+ throw error;
715
+ }
716
+ }
717
+
486
718
  export async function compileModuleFromSource(options = {}) {
487
719
  const manifest = options.manifest ?? {};
488
720
  const sourceCode = String(options.sourceCode ?? "");
@@ -531,11 +763,19 @@ export async function compileModuleFromSource(options = {}) {
531
763
  let wasmBytes;
532
764
  let resolvedOutputPath = null;
533
765
  let tempDir = null;
766
+ const threadModel = resolveThreadModel({
767
+ manifest,
768
+ threadModel: options.threadModel,
769
+ });
534
770
  const compileOptions = {
535
771
  ...options,
536
772
  noEntry: includeCommandMain !== true,
773
+ threadModel,
537
774
  };
538
- const result = await compileWithEmception({
775
+ const compileFunction = requiresSystemEmscripten(threadModel)
776
+ ? compileWithSystemEmscripten
777
+ : compileWithEmception;
778
+ const result = await compileFunction({
539
779
  manifest,
540
780
  language: compiler.language,
541
781
  sourceCompilerCommand: compiler.command,
@@ -559,8 +799,11 @@ export async function compileModuleFromSource(options = {}) {
559
799
  });
560
800
 
561
801
  return {
562
- compiler: "em++ (emception)",
802
+ compiler: requiresSystemEmscripten(threadModel)
803
+ ? "em++ (system emscripten pthreads)"
804
+ : "em++ (emception)",
563
805
  language: compiler.language,
806
+ threadModel,
564
807
  outputPath: resolvedOutputPath,
565
808
  tempDir,
566
809
  wasmBytes,
@@ -54,6 +54,7 @@ export interface SharedEmceptionSession {
54
54
  }
55
55
 
56
56
  export function createSharedEmceptionSession(): SharedEmceptionSession;
57
+ export function createIsolatedEmceptionSession(): SharedEmceptionSession;
57
58
  export function loadSharedEmception(): Promise<unknown>;
58
59
  export function withSharedEmception<T>(
59
60
  task: (handle: SharedEmceptionHandle) => T | Promise<T>,
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import {
4
+ createEmceptionController,
4
5
  getSharedEmceptionController,
5
6
  loadEmception,
6
7
  runWithEmceptionLock,
@@ -180,6 +181,10 @@ export function createSharedEmceptionSession() {
180
181
  return new SharedEmceptionSession();
181
182
  }
182
183
 
184
+ export function createIsolatedEmceptionSession() {
185
+ return new SharedEmceptionSession(createEmceptionController());
186
+ }
187
+
183
188
  export async function loadSharedEmception() {
184
189
  return loadEmception();
185
190
  }
@@ -225,6 +225,10 @@ export function getSharedEmceptionController() {
225
225
  return sharedEmceptionController;
226
226
  }
227
227
 
228
+ export function createEmceptionController() {
229
+ return new EmceptionController();
230
+ }
231
+
228
232
  export async function loadEmception() {
229
233
  return sharedEmceptionController.load();
230
234
  }
@@ -1,5 +1,7 @@
1
1
  export type {
2
2
  CompilationResult,
3
+ GuestLinkArtifact,
4
+ ModuleThreadModelName,
3
5
  ProtectedArtifact,
4
6
  } from "../index.js";
5
7
 
@@ -7,6 +9,7 @@ export {
7
9
  cleanupCompilation,
8
10
  compileModuleFromSource,
9
11
  createRecipientKeypairHex,
12
+ ModuleThreadModel,
10
13
  protectModuleArtifact,
11
14
  } from "../index.js";
12
15
 
@@ -18,6 +21,7 @@ export type {
18
21
  } from "./emception.js";
19
22
 
20
23
  export {
24
+ createIsolatedEmceptionSession,
21
25
  createSharedEmceptionSession,
22
26
  loadSharedEmception,
23
27
  withSharedEmception,
@@ -2,10 +2,12 @@ export {
2
2
  cleanupCompilation,
3
3
  compileModuleFromSource,
4
4
  createRecipientKeypairHex,
5
+ ModuleThreadModel,
5
6
  protectModuleArtifact,
6
7
  } from "./compileModule.js";
7
8
 
8
9
  export {
10
+ createIsolatedEmceptionSession,
9
11
  createSharedEmceptionSession,
10
12
  loadSharedEmception,
11
13
  withSharedEmception,
@@ -175,6 +175,44 @@ function validateStringField(issues, value, location, label) {
175
175
  return true;
176
176
  }
177
177
 
178
+ function validateCapabilityEntry(capability, issues, location) {
179
+ if (isNonEmptyString(capability)) {
180
+ return capability;
181
+ }
182
+ if (!capability || typeof capability !== "object" || Array.isArray(capability)) {
183
+ pushIssue(
184
+ issues,
185
+ "error",
186
+ "invalid-capability",
187
+ "Capability entries must be non-empty strings or host capability records.",
188
+ location,
189
+ );
190
+ return null;
191
+ }
192
+ if (!validateStringField(issues, capability.capability, `${location}.capability`, "Capability id")) {
193
+ return null;
194
+ }
195
+ validateOptionalStringField(
196
+ issues,
197
+ capability.scope,
198
+ `${location}.scope`,
199
+ "Capability scope",
200
+ );
201
+ validateOptionalBooleanField(
202
+ issues,
203
+ capability.required,
204
+ `${location}.required`,
205
+ "Capability required",
206
+ );
207
+ validateOptionalStringField(
208
+ issues,
209
+ capability.description,
210
+ `${location}.description`,
211
+ "Capability description",
212
+ );
213
+ return capability.capability;
214
+ }
215
+
178
216
  function validateIntegerField(
179
217
  issues,
180
218
  value,
@@ -1066,8 +1104,9 @@ export function validatePluginManifest(manifest, options = {}) {
1066
1104
  validateStringField(issues, manifest.version, `${sourceName}.version`, "version");
1067
1105
  validateStringField(issues, manifest.pluginFamily, `${sourceName}.pluginFamily`, "pluginFamily");
1068
1106
 
1069
- const declaredCapabilities = manifest.capabilities;
1070
- if (!Array.isArray(declaredCapabilities)) {
1107
+ const rawDeclaredCapabilities = manifest.capabilities;
1108
+ let declaredCapabilities = null;
1109
+ if (!Array.isArray(rawDeclaredCapabilities)) {
1071
1110
  pushIssue(
1072
1111
  issues,
1073
1112
  "warning",
@@ -1076,38 +1115,39 @@ export function validatePluginManifest(manifest, options = {}) {
1076
1115
  `${sourceName}.capabilities`,
1077
1116
  );
1078
1117
  } else {
1118
+ declaredCapabilities = [];
1079
1119
  const seenCapabilities = new Set();
1080
- for (const capability of declaredCapabilities) {
1081
- if (!isNonEmptyString(capability)) {
1082
- pushIssue(
1083
- issues,
1084
- "error",
1085
- "invalid-capability",
1086
- "Capability entries must be non-empty strings.",
1087
- `${sourceName}.capabilities`,
1088
- );
1089
- continue;
1120
+ rawDeclaredCapabilities.forEach((capability, index) => {
1121
+ const normalizedCapability = validateCapabilityEntry(
1122
+ capability,
1123
+ issues,
1124
+ `${sourceName}.capabilities[${index}]`,
1125
+ );
1126
+ if (!normalizedCapability) {
1127
+ return;
1090
1128
  }
1091
- if (seenCapabilities.has(capability)) {
1129
+ declaredCapabilities.push(normalizedCapability);
1130
+ if (seenCapabilities.has(normalizedCapability)) {
1092
1131
  pushIssue(
1093
1132
  issues,
1094
1133
  "warning",
1095
1134
  "duplicate-capability",
1096
- `Capability "${capability}" is declared more than once.`,
1135
+ `Capability "${normalizedCapability}" is declared more than once.`,
1097
1136
  `${sourceName}.capabilities`,
1098
1137
  );
1138
+ return;
1099
1139
  }
1100
- seenCapabilities.add(capability);
1101
- if (!RecommendedCapabilitySet.has(capability)) {
1140
+ seenCapabilities.add(normalizedCapability);
1141
+ if (!RecommendedCapabilitySet.has(normalizedCapability)) {
1102
1142
  pushIssue(
1103
1143
  issues,
1104
1144
  "warning",
1105
1145
  "noncanonical-capability",
1106
- `Capability "${capability}" is not in the current canonical SDN coarse capability set.`,
1146
+ `Capability "${normalizedCapability}" is not in the current canonical SDN coarse capability set.`,
1107
1147
  `${sourceName}.capabilities`,
1108
1148
  );
1109
1149
  }
1110
- }
1150
+ });
1111
1151
  }
1112
1152
  validateRuntimeTargets(
1113
1153
  manifest.runtimeTargets,
package/src/host/abi.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { bytesToBase64 } from "../utils/encoding.js";
2
+
1
3
  const textDecoder = new TextDecoder();
2
4
  const textEncoder = new TextEncoder();
3
5
 
@@ -14,6 +16,7 @@ export const NodeHostSyncHostcallOperations = Object.freeze([
14
16
  "clock.now",
15
17
  "clock.monotonicNow",
16
18
  "clock.nowIso",
19
+ "random.bytes",
17
20
  "schedule.parse",
18
21
  "schedule.matches",
19
22
  "schedule.next",
@@ -105,6 +108,47 @@ function isPromiseLike(value) {
105
108
  );
106
109
  }
107
110
 
111
+ function encodeHostcallValue(value) {
112
+ if (value === undefined) {
113
+ return undefined;
114
+ }
115
+ if (
116
+ value instanceof Uint8Array ||
117
+ value instanceof ArrayBuffer ||
118
+ ArrayBuffer.isView(value)
119
+ ) {
120
+ const bytes =
121
+ value instanceof Uint8Array
122
+ ? value
123
+ : new Uint8Array(
124
+ value.buffer ?? value,
125
+ value.byteOffset ?? 0,
126
+ value.byteLength ?? value.byteLength,
127
+ );
128
+ return {
129
+ __type: "bytes",
130
+ base64: bytesToBase64(bytes),
131
+ };
132
+ }
133
+ if (value instanceof Date) {
134
+ return value.toISOString();
135
+ }
136
+ if (typeof value === "bigint") {
137
+ return value.toString();
138
+ }
139
+ if (Array.isArray(value)) {
140
+ return value.map((entry) => encodeHostcallValue(entry));
141
+ }
142
+ if (value && typeof value === "object") {
143
+ return Object.fromEntries(
144
+ Object.entries(value)
145
+ .filter(([, entry]) => entry !== undefined)
146
+ .map(([key, entry]) => [key, encodeHostcallValue(entry)]),
147
+ );
148
+ }
149
+ return value;
150
+ }
151
+
108
152
  export function dispatchNodeHostSyncOperation(host, operation, params = null) {
109
153
  const normalized = assertNonEmptyString(operation, "Hostcall operation");
110
154
  switch (normalized) {
@@ -124,6 +168,8 @@ export function dispatchNodeHostSyncOperation(host, operation, params = null) {
124
168
  return host.clock.monotonicNow();
125
169
  case "clock.nowIso":
126
170
  return host.clock.nowIso();
171
+ case "random.bytes":
172
+ return host.random.bytes(params?.length);
127
173
  case "schedule.parse":
128
174
  return host.schedule.parse(params?.expression);
129
175
  case "schedule.matches":
@@ -202,7 +248,7 @@ export function createJsonHostcallBridge(options = {}) {
202
248
  }
203
249
  setEnvelope(HOSTCALL_STATUS_OK, {
204
250
  ok: true,
205
- result,
251
+ result: encodeHostcallValue(result),
206
252
  });
207
253
  return HOSTCALL_STATUS_OK;
208
254
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import dgram from "node:dgram";
2
2
  import net from "node:net";
3
3
  import path from "node:path";
4
+ import { randomBytes as nodeRandomBytes } from "node:crypto";
4
5
  import { spawn } from "node:child_process";
5
6
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
7
  import { performance } from "node:perf_hooks";
@@ -21,7 +22,6 @@ import {
21
22
  ed25519Sign,
22
23
  ed25519Verify,
23
24
  hkdfBytes,
24
- randomBytes,
25
25
  secp256k1PublicKey,
26
26
  secp256k1SignDigest,
27
27
  secp256k1VerifyDigest,
@@ -1422,10 +1422,10 @@ export class NodeHost {
1422
1422
  });
1423
1423
 
1424
1424
  this.random = Object.freeze({
1425
- bytes: async (length) =>
1426
- this.#withCapability("random", "random.bytes", async () => {
1425
+ bytes: (length) =>
1426
+ this.#withCapability("random", "random.bytes", () => {
1427
1427
  const size = assertNonNegativeInteger(length, "Random byte length");
1428
- return randomBytes(size);
1428
+ return nodeRandomBytes(size);
1429
1429
  }),
1430
1430
  });
1431
1431
 
package/src/index.d.ts CHANGED
@@ -556,6 +556,7 @@ export function decodeProtectedBlobBase64(
556
556
  export interface CompilationResult {
557
557
  compiler: string;
558
558
  language: string;
559
+ threadModel: ModuleThreadModelName;
559
560
  outputPath: string | null;
560
561
  tempDir: string | null;
561
562
  wasmBytes: Uint8Array;
@@ -569,9 +570,19 @@ export interface GuestLinkArtifact {
569
570
  language: string;
570
571
  symbolPrefix: string;
571
572
  methodSymbols: Record<string, string>;
573
+ threadModel: ModuleThreadModelName;
572
574
  objectBytes: Uint8Array;
573
575
  }
574
576
 
577
+ export type ModuleThreadModelName =
578
+ | "single-thread"
579
+ | "emscripten-pthreads";
580
+
581
+ export const ModuleThreadModel: {
582
+ readonly SINGLE_THREAD: "single-thread";
583
+ readonly EMSCRIPTEN_PTHREADS: "emscripten-pthreads";
584
+ };
585
+
575
586
  export interface ProtectedArtifact {
576
587
  mnemonic: string;
577
588
  signingPublicKeyHex: string;
@@ -602,6 +613,7 @@ export function compileModuleFromSource(options: {
602
613
  manifest: PluginManifest;
603
614
  sourceCode: string;
604
615
  language?: string;
616
+ threadModel?: ModuleThreadModelName;
605
617
  outputPath?: string;
606
618
  allowUndefinedImports?: boolean;
607
619
  }): Promise<CompilationResult>;
@@ -638,6 +650,7 @@ export type {
638
650
  } from "./compiler/emception.js";
639
651
 
640
652
  export {
653
+ createIsolatedEmceptionSession,
641
654
  createSharedEmceptionSession,
642
655
  loadSharedEmception,
643
656
  withSharedEmception,
@@ -924,7 +937,7 @@ export class NodeHost {
924
937
  nowIso(): string;
925
938
  };
926
939
  random: {
927
- bytes(length: number): Promise<Uint8Array>;
940
+ bytes(length: number): Uint8Array;
928
941
  };
929
942
  timers: {
930
943
  delay(ms: number, options?: { signal?: unknown }): Promise<void>;
@@ -1,6 +1,10 @@
1
1
  import * as flatbuffers from "flatbuffers";
2
2
 
3
3
  import {
4
+ CapabilityKind,
5
+ DrainPolicy,
6
+ HostCapabilityT,
7
+ PluginFamily,
4
8
  PluginManifest,
5
9
  PluginManifestT,
6
10
  } from "../generated/orbpro/manifest.js";
@@ -21,6 +25,88 @@ function toByteBuffer(data) {
21
25
  );
22
26
  }
23
27
 
28
+ function normalizeEnumName(name, { separator = "_", lowercase = true } = {}) {
29
+ if (typeof name !== "string" || name.length === 0) {
30
+ return null;
31
+ }
32
+ const normalized = name.trim();
33
+ if (!normalized) {
34
+ return null;
35
+ }
36
+ const joined = normalized.replace(/_/g, separator);
37
+ return lowercase ? joined.toLowerCase() : joined;
38
+ }
39
+
40
+ function normalizePluginFamilyName(value) {
41
+ if (typeof value === "number" && typeof PluginFamily[value] === "string") {
42
+ return normalizeEnumName(PluginFamily[value], { separator: "_" });
43
+ }
44
+ return normalizeEnumName(value, { separator: "_" });
45
+ }
46
+
47
+ function normalizeDrainPolicyName(value) {
48
+ if (typeof value === "number" && typeof DrainPolicy[value] === "string") {
49
+ return normalizeEnumName(DrainPolicy[value], { separator: "-" });
50
+ }
51
+ return normalizeEnumName(value, { separator: "-" });
52
+ }
53
+
54
+ function normalizeCapabilityName(value) {
55
+ if (
56
+ typeof value === "number" &&
57
+ typeof CapabilityKind[value] === "string"
58
+ ) {
59
+ return normalizeEnumName(CapabilityKind[value], { separator: "_" });
60
+ }
61
+ return normalizeEnumName(value, { separator: "_" });
62
+ }
63
+
64
+ function normalizeDecodedCapabilities(value) {
65
+ if (!Array.isArray(value)) {
66
+ return [];
67
+ }
68
+ return value
69
+ .map((entry) => {
70
+ if (typeof entry === "string") {
71
+ return normalizeCapabilityName(entry);
72
+ }
73
+ if (!(entry instanceof HostCapabilityT) && (!entry || typeof entry !== "object")) {
74
+ return null;
75
+ }
76
+ const capability = normalizeCapabilityName(entry.capability);
77
+ if (!capability) {
78
+ return null;
79
+ }
80
+ const scope =
81
+ typeof entry.scope === "string" && entry.scope.trim().length > 0
82
+ ? entry.scope.trim()
83
+ : null;
84
+ const description =
85
+ typeof entry.description === "string" &&
86
+ entry.description.trim().length > 0
87
+ ? entry.description.trim()
88
+ : null;
89
+ const required = entry.required !== false;
90
+ if (!scope && !description && required) {
91
+ return capability;
92
+ }
93
+ return {
94
+ capability,
95
+ ...(scope ? { scope } : {}),
96
+ ...(required === false ? { required: false } : {}),
97
+ ...(description ? { description } : {}),
98
+ };
99
+ })
100
+ .filter(Boolean);
101
+ }
102
+
103
+ function normalizeDecodedMethod(method = {}) {
104
+ return {
105
+ ...method,
106
+ drainPolicy: normalizeDrainPolicyName(method.drainPolicy),
107
+ };
108
+ }
109
+
24
110
  export function decodePluginManifest(data) {
25
111
  const bb = toByteBuffer(data);
26
112
  if (!PluginManifest.bufferHasIdentifier(bb)) {
@@ -29,6 +115,11 @@ export function decodePluginManifest(data) {
29
115
  const unpacked = PluginManifest.getRootAsPluginManifest(bb).unpack();
30
116
  return {
31
117
  ...unpacked,
118
+ pluginFamily: normalizePluginFamilyName(unpacked.pluginFamily),
119
+ capabilities: normalizeDecodedCapabilities(unpacked.capabilities),
120
+ methods: Array.isArray(unpacked.methods)
121
+ ? unpacked.methods.map((method) => normalizeDecodedMethod(method))
122
+ : [],
32
123
  invokeSurfaces: Array.isArray(unpacked.invokeSurfaces)
33
124
  ? unpacked.invokeSurfaces
34
125
  .map((value) => normalizeInvokeSurfaceName(value))
@@ -32,11 +32,11 @@ const CapabilitySurfaceMatrix = Object.freeze({
32
32
  wasi: true,
33
33
  standaloneWasi: true,
34
34
  wasmedge: true,
35
- syncHostcall: false,
35
+ syncHostcall: true,
36
36
  nodeHostApi: true,
37
37
  notes: [
38
38
  "WASI random_get is available to standalone guests.",
39
- "The current sync hostcall ABI does not expose random.bytes.",
39
+ "The sync hostcall ABI exposes random.bytes through a canonical base64 byte envelope.",
40
40
  ],
41
41
  }),
42
42
  timers: Object.freeze({