space-data-module-sdk 0.3.1 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
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",
@@ -9,3 +9,8 @@ export const SDS_DEPLOYMENT_SECTION_NAME = "sds.deployment";
9
9
  export const SDS_DEPLOYMENT_ENTRY_ID = "deployment-plan";
10
10
  export const SDS_DEPLOYMENT_MEDIA_TYPE =
11
11
  "application/vnd.space-data.module.deployment+json";
12
+ export const SDS_GUEST_LINK_OBJECT_ENTRY_ID = "guest-link-object";
13
+ export const SDS_GUEST_LINK_METADATA_ENTRY_ID = "guest-link-metadata";
14
+ export const SDS_GUEST_LINK_SECTION_NAME = "sds.guest-link";
15
+ export const SDS_GUEST_LINK_MEDIA_TYPE =
16
+ "application/vnd.space-data.module.guest-link+json";
@@ -30,6 +30,16 @@ import {
30
30
  generateX25519Keypair,
31
31
  } from "../transport/index.js";
32
32
  import { createSingleFileBundle } from "../bundle/index.js";
33
+ import {
34
+ SDS_GUEST_LINK_MEDIA_TYPE,
35
+ SDS_GUEST_LINK_METADATA_ENTRY_ID,
36
+ SDS_GUEST_LINK_OBJECT_ENTRY_ID,
37
+ SDS_GUEST_LINK_SECTION_NAME,
38
+ } from "../bundle/constants.js";
39
+ import {
40
+ decodeUnsignedLeb128,
41
+ parseWasmModuleSections,
42
+ } from "../bundle/wasm.js";
33
43
  import {
34
44
  base64ToBytes,
35
45
  bytesToBase64,
@@ -76,6 +86,201 @@ function buildCompilerArgs(exportedSymbols, options = {}) {
76
86
  return args;
77
87
  }
78
88
 
89
+ function guestLinkSymbolPrefix(pluginId) {
90
+ const normalized = String(pluginId ?? "module");
91
+ const ascii = Array.from(new TextEncoder().encode(normalized))
92
+ .map((byte) => byte.toString(16).padStart(2, "0"))
93
+ .join("")
94
+ .slice(0, 24);
95
+ return `sdm_guest_${ascii}_`;
96
+ }
97
+
98
+ function parseStrongDefinedIdentifiers(nmOutput = "") {
99
+ const identifiers = new Set();
100
+ for (const line of String(nmOutput).split(/\r?\n/)) {
101
+ const match = line.match(/^\S+\s+([A-Za-z])\s+(.+)$/);
102
+ if (!match) {
103
+ continue;
104
+ }
105
+ const symbolType = match[1];
106
+ if (!"TDBCGRSV".includes(symbolType)) {
107
+ continue;
108
+ }
109
+ const demangled = match[2].trim();
110
+ const baseName = demangled
111
+ .replace(/\(.*$/, "")
112
+ .split("::")
113
+ .at(-1)
114
+ ?.trim();
115
+ if (!C_IDENTIFIER.test(baseName ?? "")) {
116
+ continue;
117
+ }
118
+ identifiers.add(baseName);
119
+ }
120
+ return Array.from(identifiers).sort();
121
+ }
122
+
123
+ const WASM_SYM_BINDING_WEAK = 0x01;
124
+ const WASM_SYM_BINDING_LOCAL = 0x02;
125
+ const WASM_SYM_UNDEFINED = 0x10;
126
+ const WASM_SYM_EXPLICIT_NAME = 0x40;
127
+ const WASM_SYMBOL_KIND_FUNCTION = 0;
128
+ const WASM_SYMBOL_KIND_DATA = 1;
129
+ const WASM_SYMBOL_KIND_GLOBAL = 2;
130
+ const WASM_SYMBOL_KIND_SECTION = 3;
131
+ const WASM_SYMBOL_KIND_EVENT = 4;
132
+ const WASM_SYMBOL_KIND_TABLE = 5;
133
+ const LINKING_SYMBOL_TABLE_SUBSECTION_ID = 8;
134
+ const textDecoder = new TextDecoder();
135
+
136
+ function decodeWasmName(bytes, offset) {
137
+ const lengthInfo = decodeUnsignedLeb128(bytes, offset);
138
+ const nameStart = lengthInfo.nextOffset;
139
+ const nameEnd = nameStart + lengthInfo.value;
140
+ if (nameEnd > bytes.length) {
141
+ throw new Error("WASM name extends past end of symbol payload.");
142
+ }
143
+ return {
144
+ value: textDecoder.decode(bytes.subarray(nameStart, nameEnd)),
145
+ nextOffset: nameEnd,
146
+ };
147
+ }
148
+
149
+ function parseDefinedLinkSymbolsFromObjectBytes(objectBytes) {
150
+ const parsed = parseWasmModuleSections(objectBytes);
151
+ const linkingSection = parsed.sections.find(
152
+ (section) => section.id === 0 && section.name === "linking",
153
+ );
154
+ if (!linkingSection) {
155
+ throw new Error("Guest-link object is missing a linking custom section.");
156
+ }
157
+ const payload = linkingSection.dataBytes;
158
+ let offset = decodeUnsignedLeb128(payload, 0).nextOffset;
159
+ const identifiers = new Set();
160
+ while (offset < payload.length) {
161
+ const subsectionId = payload[offset++];
162
+ const sizeInfo = decodeUnsignedLeb128(payload, offset);
163
+ offset = sizeInfo.nextOffset;
164
+ const subsectionEnd = offset + sizeInfo.value;
165
+ if (subsectionEnd > payload.length) {
166
+ throw new Error("Linking subsection extends past end of payload.");
167
+ }
168
+ if (subsectionId !== LINKING_SYMBOL_TABLE_SUBSECTION_ID) {
169
+ offset = subsectionEnd;
170
+ continue;
171
+ }
172
+ let cursor = offset;
173
+ const countInfo = decodeUnsignedLeb128(payload, cursor);
174
+ cursor = countInfo.nextOffset;
175
+ for (let symbolIndex = 0; symbolIndex < countInfo.value; symbolIndex += 1) {
176
+ const kind = payload[cursor++];
177
+ const flagsInfo = decodeUnsignedLeb128(payload, cursor);
178
+ const flags = flagsInfo.value;
179
+ cursor = flagsInfo.nextOffset;
180
+ let name = "";
181
+ if (
182
+ kind === WASM_SYMBOL_KIND_FUNCTION ||
183
+ kind === WASM_SYMBOL_KIND_GLOBAL ||
184
+ kind === WASM_SYMBOL_KIND_EVENT ||
185
+ kind === WASM_SYMBOL_KIND_TABLE
186
+ ) {
187
+ cursor = decodeUnsignedLeb128(payload, cursor).nextOffset;
188
+ if (
189
+ (flags & WASM_SYM_UNDEFINED) === 0 ||
190
+ (flags & WASM_SYM_EXPLICIT_NAME) !== 0
191
+ ) {
192
+ const nameInfo = decodeWasmName(payload, cursor);
193
+ name = nameInfo.value;
194
+ cursor = nameInfo.nextOffset;
195
+ }
196
+ } else if (kind === WASM_SYMBOL_KIND_DATA) {
197
+ const nameInfo = decodeWasmName(payload, cursor);
198
+ name = nameInfo.value;
199
+ cursor = nameInfo.nextOffset;
200
+ if ((flags & WASM_SYM_UNDEFINED) === 0) {
201
+ cursor = decodeUnsignedLeb128(payload, cursor).nextOffset;
202
+ cursor = decodeUnsignedLeb128(payload, cursor).nextOffset;
203
+ cursor = decodeUnsignedLeb128(payload, cursor).nextOffset;
204
+ }
205
+ } else if (kind === WASM_SYMBOL_KIND_SECTION) {
206
+ cursor = decodeUnsignedLeb128(payload, cursor).nextOffset;
207
+ } else {
208
+ throw new Error(`Unsupported WASM linking symbol kind: ${kind}`);
209
+ }
210
+ if ((flags & WASM_SYM_UNDEFINED) !== 0) {
211
+ continue;
212
+ }
213
+ if ((flags & WASM_SYM_BINDING_LOCAL) !== 0) {
214
+ continue;
215
+ }
216
+ if ((flags & WASM_SYM_BINDING_WEAK) !== 0) {
217
+ continue;
218
+ }
219
+ if (!C_IDENTIFIER.test(name)) {
220
+ continue;
221
+ }
222
+ identifiers.add(name);
223
+ }
224
+ return Array.from(identifiers).sort();
225
+ }
226
+ throw new Error("Guest-link object is missing a symbol table subsection.");
227
+ }
228
+
229
+ function deriveGuestLinkRenameArgs({
230
+ objectBytes,
231
+ pluginId,
232
+ methodIds = [],
233
+ } = {}) {
234
+ const identifiers = parseDefinedLinkSymbolsFromObjectBytes(objectBytes);
235
+ const prefix = guestLinkSymbolPrefix(pluginId);
236
+ const renamedIdentifiers = Array.from(
237
+ new Set([...identifiers, ...methodIds.filter((value) => C_IDENTIFIER.test(value))]),
238
+ ).sort();
239
+ return {
240
+ prefix,
241
+ identifiers: renamedIdentifiers,
242
+ renameArgs: renamedIdentifiers.map(
243
+ (identifier) => `-D${identifier}=${prefix}${identifier}`,
244
+ ),
245
+ methodSymbols: Object.fromEntries(
246
+ methodIds.map((methodId) => [methodId, `${prefix}${methodId}`]),
247
+ ),
248
+ };
249
+ }
250
+
251
+ function createGuestLinkBundleEntries(guestLink) {
252
+ if (!guestLink?.objectBytes || guestLink.objectBytes.length === 0) {
253
+ return [];
254
+ }
255
+ return [
256
+ {
257
+ entryId: SDS_GUEST_LINK_OBJECT_ENTRY_ID,
258
+ role: "auxiliary",
259
+ sectionName: SDS_GUEST_LINK_SECTION_NAME,
260
+ payloadEncoding: "raw-bytes",
261
+ mediaType: "application/wasm",
262
+ payload: guestLink.objectBytes,
263
+ description: "Prefixed guest-link wasm object for monolithic flow linking.",
264
+ },
265
+ {
266
+ entryId: SDS_GUEST_LINK_METADATA_ENTRY_ID,
267
+ role: "auxiliary",
268
+ sectionName: SDS_GUEST_LINK_SECTION_NAME,
269
+ payloadEncoding: "json-utf8",
270
+ mediaType: SDS_GUEST_LINK_MEDIA_TYPE,
271
+ payload: {
272
+ version: 1,
273
+ format: "wasm-object",
274
+ symbolPrefix: guestLink.symbolPrefix,
275
+ methodSymbols: guestLink.methodSymbols,
276
+ methodIds: Object.keys(guestLink.methodSymbols ?? {}),
277
+ language: guestLink.language,
278
+ },
279
+ description: "Guest-link metadata for monolithic flow linking.",
280
+ },
281
+ ];
282
+ }
283
+
79
284
  async function getInvokeCppSupportFiles() {
80
285
  const [runtimeHeaders, schemaHeaders] = await Promise.all([
81
286
  getFlatbuffersCppRuntimeHeaders(),
@@ -114,6 +319,8 @@ function removeEmceptionDirectory(emception, directoryPath) {
114
319
 
115
320
  async function compileWithEmception(options = {}) {
116
321
  const {
322
+ manifest,
323
+ language,
117
324
  sourceCompilerCommand,
118
325
  sourceExtension,
119
326
  sourceCode,
@@ -140,6 +347,7 @@ async function compileWithEmception(options = {}) {
140
347
  const invokeHeaderPath = path.posix.join(workDir, "space_data_module_invoke.h");
141
348
  const invokeSourcePath = path.posix.join(workDir, "plugin-invoke-bridge.cpp");
142
349
  const sourceObjectPath = path.posix.join(workDir, "module.o");
350
+ const linkObjectPath = path.posix.join(workDir, "module-link.o");
143
351
  const manifestObjectPath = path.posix.join(workDir, "plugin-manifest-exports.o");
144
352
  const invokeObjectPath = path.posix.join(workDir, "plugin-invoke-bridge.o");
145
353
  const wasmOutputPath = path.posix.join(workDir, "module.wasm");
@@ -165,6 +373,43 @@ async function compileWithEmception(options = {}) {
165
373
  "-o",
166
374
  sourceObjectPath,
167
375
  ],
376
+ ];
377
+
378
+ for (const command of commands) {
379
+ const result = emception.run(command.join(" "));
380
+ if (result.returncode !== 0) {
381
+ throw new Error(
382
+ `Compilation failed with ${command[0]} (emception): ${result.stderr || result.stdout}`,
383
+ );
384
+ }
385
+ }
386
+
387
+ const sourceObjectBytes = new Uint8Array(emception.readFile(sourceObjectPath));
388
+ const guestLink = deriveGuestLinkRenameArgs({
389
+ objectBytes: sourceObjectBytes,
390
+ pluginId: manifest?.pluginId,
391
+ methodIds: Array.isArray(manifest?.methods)
392
+ ? manifest.methods.map((method) => String(method?.methodId ?? ""))
393
+ : [],
394
+ });
395
+
396
+ const linkCompileCommand = [
397
+ sourceCompilerCommand,
398
+ "-c",
399
+ sourcePath,
400
+ `-I${workDir}`,
401
+ ...guestLink.renameArgs,
402
+ "-o",
403
+ linkObjectPath,
404
+ ];
405
+ const linkCompileResult = emception.run(linkCompileCommand.join(" "));
406
+ if (linkCompileResult.returncode !== 0) {
407
+ throw new Error(
408
+ `Compilation failed with ${linkCompileCommand[0]} (emception): ${linkCompileResult.stderr || linkCompileResult.stdout}`,
409
+ );
410
+ }
411
+
412
+ const remainingCommands = [
168
413
  [
169
414
  "em++",
170
415
  "-c",
@@ -195,8 +440,7 @@ async function compileWithEmception(options = {}) {
195
440
  wasmOutputPath,
196
441
  ],
197
442
  ];
198
-
199
- for (const command of commands) {
443
+ for (const command of remainingCommands) {
200
444
  const result = emception.run(command.join(" "));
201
445
  if (result.returncode !== 0) {
202
446
  throw new Error(
@@ -206,8 +450,20 @@ async function compileWithEmception(options = {}) {
206
450
  }
207
451
 
208
452
  const wasmBytes = new Uint8Array(emception.readFile(wasmOutputPath));
453
+ const linkObjectBytes = new Uint8Array(emception.readFile(linkObjectPath));
209
454
  await writeFile(resolvedOutputPath, wasmBytes);
210
- return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
455
+ return {
456
+ wasmBytes,
457
+ outputPath: resolvedOutputPath,
458
+ tempDir,
459
+ guestLink: {
460
+ format: "wasm-object",
461
+ language,
462
+ symbolPrefix: guestLink.prefix,
463
+ methodSymbols: guestLink.methodSymbols,
464
+ objectBytes: linkObjectBytes,
465
+ },
466
+ };
211
467
  } finally {
212
468
  try {
213
469
  removeEmceptionDirectory(emception, workDir);
@@ -275,6 +531,8 @@ export async function compileModuleFromSource(options = {}) {
275
531
  noEntry: includeCommandMain !== true,
276
532
  };
277
533
  const result = await compileWithEmception({
534
+ manifest,
535
+ language: compiler.language,
278
536
  sourceCompilerCommand: compiler.command,
279
537
  sourceExtension: compiler.extension,
280
538
  sourceCode,
@@ -301,6 +559,7 @@ export async function compileModuleFromSource(options = {}) {
301
559
  outputPath: resolvedOutputPath,
302
560
  tempDir,
303
561
  wasmBytes,
562
+ guestLink: result.guestLink,
304
563
  manifestWarnings: warnings,
305
564
  report,
306
565
  };
@@ -394,12 +653,16 @@ export async function protectModuleArtifact(options = {}) {
394
653
 
395
654
  let singleFileBundle = null;
396
655
  if (options.singleFileBundle === true) {
656
+ const additionalEntries = [
657
+ ...(Array.isArray(options.bundleEntries) ? options.bundleEntries : []),
658
+ ...createGuestLinkBundleEntries(options.guestLink),
659
+ ];
397
660
  singleFileBundle = await createSingleFileBundle({
398
661
  wasmBytes,
399
662
  manifest,
400
663
  authorization: signedAuthorization,
401
664
  transportEnvelope: encryptedEnvelope,
402
- entries: options.bundleEntries,
665
+ entries: additionalEntries,
403
666
  });
404
667
  }
405
668
 
package/src/index.d.ts CHANGED
@@ -424,10 +424,19 @@ export interface CompilationResult {
424
424
  outputPath: string | null;
425
425
  tempDir: string | null;
426
426
  wasmBytes: Uint8Array;
427
+ guestLink: GuestLinkArtifact | null;
427
428
  manifestWarnings: string[];
428
429
  report: ComplianceReport;
429
430
  }
430
431
 
432
+ export interface GuestLinkArtifact {
433
+ format: "wasm-object";
434
+ language: string;
435
+ symbolPrefix: string;
436
+ methodSymbols: Record<string, string>;
437
+ objectBytes: Uint8Array;
438
+ }
439
+
431
440
  export interface ProtectedArtifact {
432
441
  mnemonic: string;
433
442
  signingPublicKeyHex: string;
@@ -473,7 +482,8 @@ export function protectModuleArtifact(options: {
473
482
  targetUrl?: string;
474
483
  capabilities?: string[];
475
484
  singleFileBundle?: boolean;
476
- bundleEntries?: Record<string, unknown>;
485
+ bundleEntries?: Array<Record<string, unknown>>;
486
+ guestLink?: GuestLinkArtifact | null;
477
487
  }): Promise<ProtectedArtifact>;
478
488
 
479
489
  export function createRecipientKeypairHex(): Promise<{
@@ -567,6 +577,10 @@ export function validateManifestAgainstStandardsCatalog(
567
577
 
568
578
  export const SDS_BUNDLE_SECTION_NAME: string;
569
579
  export const DEFAULT_HASH_ALGORITHM: string;
580
+ export const SDS_GUEST_LINK_OBJECT_ENTRY_ID: string;
581
+ export const SDS_GUEST_LINK_METADATA_ENTRY_ID: string;
582
+ export const SDS_GUEST_LINK_SECTION_NAME: string;
583
+ export const SDS_GUEST_LINK_MEDIA_TYPE: string;
570
584
 
571
585
  export function createSingleFileBundle(options: {
572
586
  wasmBytes: Uint8Array;