space-data-module-sdk 0.1.0 → 0.2.0

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.
Files changed (44) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +174 -83
  3. package/bin/space-data-module.js +24 -0
  4. package/package.json +8 -3
  5. package/schemas/ModuleBundle.fbs +108 -0
  6. package/schemas/PluginManifest.fbs +26 -1
  7. package/src/bundle/codec.js +244 -0
  8. package/src/bundle/constants.js +8 -0
  9. package/src/bundle/index.js +3 -0
  10. package/src/bundle/wasm.js +447 -0
  11. package/src/compiler/compileModule.js +189 -41
  12. package/src/compliance/pluginCompliance.js +334 -0
  13. package/src/generated/orbpro/manifest/capability-kind.d.ts +27 -2
  14. package/src/generated/orbpro/manifest/capability-kind.js +26 -1
  15. package/src/generated/orbpro/manifest/capability-kind.ts +25 -0
  16. package/src/generated/orbpro/module/canonicalization-rule.d.ts +48 -0
  17. package/src/generated/orbpro/module/canonicalization-rule.js +95 -0
  18. package/src/generated/orbpro/module/canonicalization-rule.ts +142 -0
  19. package/src/generated/orbpro/module/module-bundle-entry-role.d.ts +11 -0
  20. package/src/generated/orbpro/module/module-bundle-entry-role.js +14 -0
  21. package/src/generated/orbpro/module/module-bundle-entry-role.ts +15 -0
  22. package/src/generated/orbpro/module/module-bundle-entry.d.ts +97 -0
  23. package/src/generated/orbpro/module/module-bundle-entry.js +219 -0
  24. package/src/generated/orbpro/module/module-bundle-entry.ts +287 -0
  25. package/src/generated/orbpro/module/module-bundle.d.ts +86 -0
  26. package/src/generated/orbpro/module/module-bundle.js +213 -0
  27. package/src/generated/orbpro/module/module-bundle.ts +277 -0
  28. package/src/generated/orbpro/module/module-payload-encoding.d.ts +9 -0
  29. package/src/generated/orbpro/module/module-payload-encoding.js +12 -0
  30. package/src/generated/orbpro/module/module-payload-encoding.ts +13 -0
  31. package/src/generated/orbpro/module.d.ts +5 -0
  32. package/src/generated/orbpro/module.js +7 -0
  33. package/src/generated/orbpro/module.ts +9 -0
  34. package/src/host/abi.js +282 -0
  35. package/src/host/cron.js +247 -0
  36. package/src/host/index.js +3 -0
  37. package/src/host/nodeHost.js +2165 -0
  38. package/src/index.d.ts +880 -0
  39. package/src/index.js +9 -2
  40. package/src/manifest/normalize.js +32 -1
  41. package/src/runtime/constants.js +18 -1
  42. package/src/transport/pki.js +0 -5
  43. package/src/utils/encoding.js +9 -1
  44. package/src/utils/wasmCrypto.js +49 -1
@@ -16,6 +16,7 @@ import {
16
16
  encryptJsonForRecipient,
17
17
  generateX25519Keypair,
18
18
  } from "../transport/index.js";
19
+ import { createSingleFileBundle } from "../bundle/index.js";
19
20
  import {
20
21
  base64ToBytes,
21
22
  bytesToBase64,
@@ -48,6 +49,133 @@ function ensureExportableMethodIds(manifest) {
48
49
  }
49
50
  }
50
51
 
52
+ function buildCompilerArgs(compiler, exportedSymbols, options = {}) {
53
+ const linkerExports = exportedSymbols.map(
54
+ (symbol) => "-Wl,--export=" + symbol,
55
+ );
56
+ const extraArgs = [];
57
+ if (options.allowUndefinedImports === true) {
58
+ extraArgs.push("-s", "ERROR_ON_UNDEFINED_SYMBOLS=0", "-Wl,--allow-undefined");
59
+ }
60
+ return [
61
+ "-O2",
62
+ "--no-entry",
63
+ "-s",
64
+ "STANDALONE_WASM=1",
65
+ ...extraArgs,
66
+ ...linkerExports,
67
+ ];
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Emception — in-process WASM-based Emscripten (preferred)
72
+ // ---------------------------------------------------------------------------
73
+
74
+ let emceptionInstance = null;
75
+ let emceptionLoadAttempted = false;
76
+
77
+ async function loadEmception() {
78
+ if (emceptionLoadAttempted) return emceptionInstance;
79
+ emceptionLoadAttempted = true;
80
+ try {
81
+ const { default: Emception } = await import(
82
+ "sdn-emception"
83
+ );
84
+ emceptionInstance = new Emception();
85
+ await emceptionInstance.init();
86
+ return emceptionInstance;
87
+ } catch {
88
+ // emception not available or init failed — fall back to system emcc
89
+ }
90
+ return null;
91
+ }
92
+
93
+ async function compileWithEmception(
94
+ emception,
95
+ compiler,
96
+ sourceCode,
97
+ manifestSource,
98
+ exportedSymbols,
99
+ compileOptions,
100
+ ) {
101
+ const ext = compiler.extension;
102
+ const inputPath = `/working/module.${ext}`;
103
+ const manifestPath = "/working/plugin-manifest-exports.c";
104
+ const outputPath = "/working/module.wasm";
105
+
106
+ emception.writeFile(inputPath, sourceCode);
107
+ emception.writeFile(manifestPath, manifestSource);
108
+
109
+ const args = buildCompilerArgs(compiler, exportedSymbols, compileOptions);
110
+ const cmd = [
111
+ compiler.command,
112
+ inputPath,
113
+ manifestPath,
114
+ ...args,
115
+ "-o",
116
+ outputPath,
117
+ ].join(" ");
118
+
119
+ const result = emception.run(cmd);
120
+ if (result.returncode !== 0) {
121
+ throw new Error(
122
+ `Compilation failed with ${compiler.command} (emception): ${result.stderr || result.stdout}`,
123
+ );
124
+ }
125
+
126
+ const wasmBytes = emception.readFile(outputPath);
127
+ return new Uint8Array(wasmBytes);
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // System Emscripten — fallback to emcc/em++ on PATH
132
+ // ---------------------------------------------------------------------------
133
+
134
+ async function compileWithSystemEmcc(
135
+ compiler,
136
+ sourceCode,
137
+ manifestSource,
138
+ exportedSymbols,
139
+ outputPath,
140
+ compileOptions,
141
+ ) {
142
+ const tempDir = await mkdtemp(
143
+ path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
144
+ );
145
+ const sourcePath = path.join(tempDir, `module.${compiler.extension}`);
146
+ const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.c");
147
+ const resolvedOutputPath = path.resolve(
148
+ outputPath ?? path.join(tempDir, "module.wasm"),
149
+ );
150
+
151
+ await writeFile(sourcePath, sourceCode, "utf8");
152
+ await writeFile(manifestSourcePath, manifestSource, "utf8");
153
+
154
+ const args = buildCompilerArgs(compiler, exportedSymbols, compileOptions);
155
+
156
+ try {
157
+ await execFile(compiler.command, [
158
+ sourcePath,
159
+ manifestSourcePath,
160
+ ...args,
161
+ "-o",
162
+ resolvedOutputPath,
163
+ ], { timeout: 120_000 });
164
+ } catch (error) {
165
+ error.message =
166
+ `Compilation failed with ${compiler.command}: ` +
167
+ (error.stderr || error.message);
168
+ throw error;
169
+ }
170
+
171
+ const wasmBytes = await readFile(resolvedOutputPath);
172
+ return { wasmBytes, outputPath: resolvedOutputPath, tempDir };
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Public API
177
+ // ---------------------------------------------------------------------------
178
+
51
179
  export async function compileModuleFromSource(options = {}) {
52
180
  const manifest = options.manifest ?? {};
53
181
  const sourceCode = String(options.sourceCode ?? "");
@@ -68,19 +196,9 @@ export async function compileModuleFromSource(options = {}) {
68
196
  const { manifest: embeddedManifest, warnings } = toEmbeddedPluginManifest(
69
197
  manifest,
70
198
  );
71
- const tempDir = await mkdtemp(
72
- path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
73
- );
74
- const sourcePath = path.join(tempDir, `module.${compiler.extension}`);
75
- const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.c");
76
- const outputPath = path.resolve(options.outputPath ?? path.join(tempDir, "module.wasm"));
77
-
78
- await writeFile(sourcePath, sourceCode, "utf8");
79
- await writeFile(
80
- manifestSourcePath,
81
- generateEmbeddedManifestSource({ manifest: embeddedManifest }),
82
- "utf8",
83
- );
199
+ const manifestSource = generateEmbeddedManifestSource({
200
+ manifest: embeddedManifest,
201
+ });
84
202
 
85
203
  const exportedSymbols = [
86
204
  "plugin_get_manifest_flatbuffer",
@@ -91,39 +209,57 @@ export async function compileModuleFromSource(options = {}) {
91
209
  .filter(Boolean),
92
210
  ),
93
211
  ];
94
- const linkerExports = exportedSymbols.flatMap((symbol) => [
95
- "-Wl,--export=" + symbol,
96
- ]);
97
212
 
98
- try {
99
- await execFile(compiler.command, [
100
- sourcePath,
101
- manifestSourcePath,
102
- "-O2",
103
- "--no-entry",
104
- "-s",
105
- "STANDALONE_WASM=1",
106
- ...linkerExports,
107
- "-o",
108
- outputPath,
109
- ]);
110
- } catch (error) {
111
- error.message =
112
- `Compilation failed with ${compiler.command}: ` +
113
- (error.stderr || error.message);
114
- throw error;
213
+ // Try emception first, fall back to system emcc/em++
214
+ const emception = await loadEmception();
215
+
216
+ let wasmBytes;
217
+ let resolvedOutputPath = null;
218
+ let tempDir = null;
219
+ let compilerBackend;
220
+
221
+ if (emception) {
222
+ wasmBytes = await compileWithEmception(
223
+ emception,
224
+ compiler,
225
+ sourceCode,
226
+ manifestSource,
227
+ exportedSymbols,
228
+ options,
229
+ );
230
+ compilerBackend = `${compiler.command} (emception)`;
231
+ } else {
232
+ const result = await compileWithSystemEmcc(
233
+ compiler,
234
+ sourceCode,
235
+ manifestSource,
236
+ exportedSymbols,
237
+ options.outputPath,
238
+ options,
239
+ );
240
+ wasmBytes = result.wasmBytes;
241
+ resolvedOutputPath = result.outputPath;
242
+ tempDir = result.tempDir;
243
+ compilerBackend = `${compiler.command} (system)`;
115
244
  }
116
245
 
117
- const wasmBytes = await readFile(outputPath);
118
- const report = await validateArtifactWithStandards({
119
- manifest,
120
- wasmPath: outputPath,
121
- });
246
+ // Validate the compiled artifact
247
+ const report = emception
248
+ ? await validateArtifactWithStandards({
249
+ manifest,
250
+ exportNames: WebAssembly.Module.exports(
251
+ new WebAssembly.Module(wasmBytes),
252
+ ).map((e) => e.name),
253
+ })
254
+ : await validateArtifactWithStandards({
255
+ manifest,
256
+ wasmPath: resolvedOutputPath,
257
+ });
122
258
 
123
259
  return {
124
- compiler: compiler.command,
260
+ compiler: compilerBackend,
125
261
  language: compiler.language,
126
- outputPath,
262
+ outputPath: resolvedOutputPath,
127
263
  tempDir,
128
264
  wasmBytes,
129
265
  manifestWarnings: warnings,
@@ -217,6 +353,17 @@ export async function protectModuleArtifact(options = {}) {
217
353
  });
218
354
  }
219
355
 
356
+ let singleFileBundle = null;
357
+ if (options.singleFileBundle === true) {
358
+ singleFileBundle = await createSingleFileBundle({
359
+ wasmBytes,
360
+ manifest,
361
+ authorization: signedAuthorization,
362
+ transportEnvelope: encryptedEnvelope,
363
+ entries: options.bundleEntries,
364
+ });
365
+ }
366
+
220
367
  return {
221
368
  mnemonic: identity.mnemonic,
222
369
  signingPublicKeyHex: bytesToHex(identity.signingKey.publicKey),
@@ -224,6 +371,8 @@ export async function protectModuleArtifact(options = {}) {
224
371
  payload,
225
372
  encrypted: Boolean(encryptedEnvelope),
226
373
  encryptedEnvelope,
374
+ singleFileBundle,
375
+ bundledWasmBytes: singleFileBundle?.wasmBytes ?? null,
227
376
  };
228
377
  }
229
378
 
@@ -234,4 +383,3 @@ export async function createRecipientKeypairHex() {
234
383
  privateKeyHex: bytesToHex(keypair.privateKey),
235
384
  };
236
385
  }
237
-
@@ -6,13 +6,21 @@ import {
6
6
  DrainPolicy,
7
7
  ExternalInterfaceDirection,
8
8
  ExternalInterfaceKind,
9
+ RuntimeTarget,
9
10
  } from "../runtime/constants.js";
10
11
 
11
12
  export const RecommendedCapabilityIds = Object.freeze([
12
13
  "clock",
13
14
  "random",
15
+ "logging",
14
16
  "timers",
17
+ "schedule_cron",
15
18
  "http",
19
+ "tls",
20
+ "websocket",
21
+ "mqtt",
22
+ "tcp",
23
+ "udp",
16
24
  "network",
17
25
  "filesystem",
18
26
  "pipe",
@@ -23,18 +31,51 @@ export const RecommendedCapabilityIds = Object.freeze([
23
31
  "storage_adapter",
24
32
  "storage_query",
25
33
  "storage_write",
34
+ "context_read",
35
+ "context_write",
36
+ "process_exec",
37
+ "crypto_hash",
38
+ "crypto_sign",
39
+ "crypto_verify",
40
+ "crypto_encrypt",
41
+ "crypto_decrypt",
42
+ "crypto_key_agreement",
43
+ "crypto_kdf",
26
44
  "wallet_sign",
27
45
  "ipfs",
28
46
  "scene_access",
47
+ "entity_access",
29
48
  "render_hooks",
30
49
  ]);
31
50
 
32
51
  const RecommendedCapabilitySet = new Set(RecommendedCapabilityIds);
52
+ const RecommendedRuntimeTargets = Object.freeze(Object.values(RuntimeTarget));
53
+ const RecommendedRuntimeTargetSet = new Set(RecommendedRuntimeTargets);
33
54
  const DrainPolicySet = new Set(Object.values(DrainPolicy));
34
55
  const ExternalInterfaceDirectionSet = new Set(
35
56
  Object.values(ExternalInterfaceDirection),
36
57
  );
37
58
  const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
59
+ const BrowserIncompatibleCapabilitySet = new Set([
60
+ "filesystem",
61
+ "pipe",
62
+ "network",
63
+ "tcp",
64
+ "udp",
65
+ "mqtt",
66
+ "tls",
67
+ "database",
68
+ "storage_adapter",
69
+ "storage_write",
70
+ "protocol_dial",
71
+ "protocol_handle",
72
+ "process_exec",
73
+ "wallet_sign",
74
+ "ipfs",
75
+ "scene_access",
76
+ "entity_access",
77
+ "render_hooks",
78
+ ]);
38
79
  const IgnoredDirectoryNames = new Set([
39
80
  ".git",
40
81
  ".hg",
@@ -244,6 +285,244 @@ function validateExternalInterface(externalInterface, issues, location, declared
244
285
  }
245
286
  }
246
287
 
288
+ function validateTimer(timer, issues, location, methodLookup, declaredCapabilities) {
289
+ if (!timer || typeof timer !== "object" || Array.isArray(timer)) {
290
+ pushIssue(issues, "error", "invalid-timer", "Timer entries must be objects.", location);
291
+ return;
292
+ }
293
+ const timerIdValid = validateStringField(
294
+ issues,
295
+ timer.timerId,
296
+ `${location}.timerId`,
297
+ "Timer timerId",
298
+ );
299
+ const methodIdValid = validateStringField(
300
+ issues,
301
+ timer.methodId,
302
+ `${location}.methodId`,
303
+ "Timer methodId",
304
+ );
305
+ let method = null;
306
+ if (methodIdValid) {
307
+ method = methodLookup.get(timer.methodId) ?? null;
308
+ if (!method) {
309
+ pushIssue(
310
+ issues,
311
+ "error",
312
+ "unknown-timer-method",
313
+ `Timer "${timer.timerId ?? "timer"}" references unknown method "${timer.methodId}".`,
314
+ `${location}.methodId`,
315
+ );
316
+ }
317
+ }
318
+ if (timer.inputPortId !== undefined && timer.inputPortId !== null) {
319
+ if (!isNonEmptyString(timer.inputPortId)) {
320
+ pushIssue(
321
+ issues,
322
+ "error",
323
+ "invalid-timer-input-port",
324
+ "Timer inputPortId must be a non-empty string when present.",
325
+ `${location}.inputPortId`,
326
+ );
327
+ } else if (
328
+ method &&
329
+ !method.inputPorts.some((port) => port?.portId === timer.inputPortId)
330
+ ) {
331
+ pushIssue(
332
+ issues,
333
+ "error",
334
+ "unknown-timer-input-port",
335
+ `Timer "${timer.timerId ?? "timer"}" references unknown input port "${timer.inputPortId}" on method "${timer.methodId}".`,
336
+ `${location}.inputPortId`,
337
+ );
338
+ }
339
+ }
340
+ if (timer.defaultIntervalMs !== undefined) {
341
+ validateIntegerField(
342
+ issues,
343
+ timer.defaultIntervalMs,
344
+ `${location}.defaultIntervalMs`,
345
+ "Timer defaultIntervalMs",
346
+ { min: 0 },
347
+ );
348
+ }
349
+ if (
350
+ timerIdValid &&
351
+ Array.isArray(declaredCapabilities) &&
352
+ !declaredCapabilities.includes("timers")
353
+ ) {
354
+ pushIssue(
355
+ issues,
356
+ "error",
357
+ "undeclared-timer-capability",
358
+ `Timer "${timer.timerId}" requires the "timers" capability to be declared in manifest.capabilities.`,
359
+ location,
360
+ );
361
+ }
362
+ }
363
+
364
+ function validateProtocol(protocol, issues, location, methodLookup, declaredCapabilities) {
365
+ if (!protocol || typeof protocol !== "object" || Array.isArray(protocol)) {
366
+ pushIssue(
367
+ issues,
368
+ "error",
369
+ "invalid-protocol",
370
+ "Protocol entries must be objects.",
371
+ location,
372
+ );
373
+ return;
374
+ }
375
+ const protocolIdValid = validateStringField(
376
+ issues,
377
+ protocol.protocolId,
378
+ `${location}.protocolId`,
379
+ "Protocol protocolId",
380
+ );
381
+ const methodIdValid = validateStringField(
382
+ issues,
383
+ protocol.methodId,
384
+ `${location}.methodId`,
385
+ "Protocol methodId",
386
+ );
387
+ let method = null;
388
+ if (methodIdValid) {
389
+ method = methodLookup.get(protocol.methodId) ?? null;
390
+ if (!method) {
391
+ pushIssue(
392
+ issues,
393
+ "error",
394
+ "unknown-protocol-method",
395
+ `Protocol "${protocol.protocolId ?? "protocol"}" references unknown method "${protocol.methodId}".`,
396
+ `${location}.methodId`,
397
+ );
398
+ }
399
+ }
400
+ if (protocol.inputPortId !== undefined && protocol.inputPortId !== null) {
401
+ if (!isNonEmptyString(protocol.inputPortId)) {
402
+ pushIssue(
403
+ issues,
404
+ "error",
405
+ "invalid-protocol-input-port",
406
+ "Protocol inputPortId must be a non-empty string when present.",
407
+ `${location}.inputPortId`,
408
+ );
409
+ } else if (
410
+ method &&
411
+ !method.inputPorts.some((port) => port?.portId === protocol.inputPortId)
412
+ ) {
413
+ pushIssue(
414
+ issues,
415
+ "error",
416
+ "unknown-protocol-input-port",
417
+ `Protocol "${protocol.protocolId ?? "protocol"}" references unknown input port "${protocol.inputPortId}" on method "${protocol.methodId}".`,
418
+ `${location}.inputPortId`,
419
+ );
420
+ }
421
+ }
422
+ if (protocol.outputPortId !== undefined && protocol.outputPortId !== null) {
423
+ if (!isNonEmptyString(protocol.outputPortId)) {
424
+ pushIssue(
425
+ issues,
426
+ "error",
427
+ "invalid-protocol-output-port",
428
+ "Protocol outputPortId must be a non-empty string when present.",
429
+ `${location}.outputPortId`,
430
+ );
431
+ } else if (
432
+ method &&
433
+ !method.outputPorts.some((port) => port?.portId === protocol.outputPortId)
434
+ ) {
435
+ pushIssue(
436
+ issues,
437
+ "error",
438
+ "unknown-protocol-output-port",
439
+ `Protocol "${protocol.protocolId ?? "protocol"}" references unknown output port "${protocol.outputPortId}" on method "${protocol.methodId}".`,
440
+ `${location}.outputPortId`,
441
+ );
442
+ }
443
+ }
444
+ if (
445
+ protocolIdValid &&
446
+ Array.isArray(declaredCapabilities) &&
447
+ !declaredCapabilities.includes("protocol_handle") &&
448
+ !declaredCapabilities.includes("protocol_dial")
449
+ ) {
450
+ pushIssue(
451
+ issues,
452
+ "error",
453
+ "undeclared-protocol-capability",
454
+ `Protocol "${protocol.protocolId}" requires "protocol_handle" or "protocol_dial" to be declared in manifest.capabilities.`,
455
+ location,
456
+ );
457
+ }
458
+ }
459
+
460
+ function validateRuntimeTargets(runtimeTargets, declaredCapabilities, issues, sourceName) {
461
+ if (runtimeTargets === undefined) {
462
+ return;
463
+ }
464
+ if (!Array.isArray(runtimeTargets)) {
465
+ pushIssue(
466
+ issues,
467
+ "error",
468
+ "invalid-runtime-targets",
469
+ "manifest.runtimeTargets must be an array of non-empty strings when present.",
470
+ `${sourceName}.runtimeTargets`,
471
+ );
472
+ return;
473
+ }
474
+ const seenTargets = new Set();
475
+ for (const target of runtimeTargets) {
476
+ if (!isNonEmptyString(target)) {
477
+ pushIssue(
478
+ issues,
479
+ "error",
480
+ "invalid-runtime-target",
481
+ "Runtime target entries must be non-empty strings.",
482
+ `${sourceName}.runtimeTargets`,
483
+ );
484
+ continue;
485
+ }
486
+ if (seenTargets.has(target)) {
487
+ pushIssue(
488
+ issues,
489
+ "warning",
490
+ "duplicate-runtime-target",
491
+ `Runtime target "${target}" is declared more than once.`,
492
+ `${sourceName}.runtimeTargets`,
493
+ );
494
+ continue;
495
+ }
496
+ seenTargets.add(target);
497
+ if (!RecommendedRuntimeTargetSet.has(target)) {
498
+ pushIssue(
499
+ issues,
500
+ "warning",
501
+ "noncanonical-runtime-target",
502
+ `Runtime target "${target}" is not in the current canonical runtime target set.`,
503
+ `${sourceName}.runtimeTargets`,
504
+ );
505
+ }
506
+ }
507
+ if (
508
+ seenTargets.has(RuntimeTarget.BROWSER) &&
509
+ Array.isArray(declaredCapabilities)
510
+ ) {
511
+ for (const capability of declaredCapabilities) {
512
+ if (!BrowserIncompatibleCapabilitySet.has(capability)) {
513
+ continue;
514
+ }
515
+ pushIssue(
516
+ issues,
517
+ "error",
518
+ "capability-runtime-conflict",
519
+ `Capability "${capability}" is not available in the canonical browser runtime target.`,
520
+ `${sourceName}.capabilities`,
521
+ );
522
+ }
523
+ }
524
+ }
525
+
247
526
  export function validatePluginManifest(manifest, options = {}) {
248
527
  const { sourceName = "manifest" } = options;
249
528
  const issues = [];
@@ -307,6 +586,12 @@ export function validatePluginManifest(manifest, options = {}) {
307
586
  }
308
587
  }
309
588
  }
589
+ validateRuntimeTargets(
590
+ manifest.runtimeTargets,
591
+ declaredCapabilities,
592
+ issues,
593
+ sourceName,
594
+ );
310
595
 
311
596
  if (!Array.isArray(manifest.externalInterfaces)) {
312
597
  pushIssue(
@@ -337,6 +622,7 @@ export function validatePluginManifest(manifest, options = {}) {
337
622
  );
338
623
  } else {
339
624
  const seenMethodIds = new Set();
625
+ const methodLookup = new Map();
340
626
  manifest.methods.forEach((method, index) => {
341
627
  const location = `${sourceName}.methods[${index}]`;
342
628
  if (!method || typeof method !== "object" || Array.isArray(method)) {
@@ -355,6 +641,7 @@ export function validatePluginManifest(manifest, options = {}) {
355
641
  );
356
642
  }
357
643
  seenMethodIds.add(method.methodId);
644
+ methodLookup.set(method.methodId, method);
358
645
  }
359
646
  if (!Array.isArray(method.inputPorts) || method.inputPorts.length === 0) {
360
647
  pushIssue(
@@ -403,6 +690,46 @@ export function validatePluginManifest(manifest, options = {}) {
403
690
  );
404
691
  }
405
692
  });
693
+
694
+ if (manifest.timers !== undefined && !Array.isArray(manifest.timers)) {
695
+ pushIssue(
696
+ issues,
697
+ "error",
698
+ "invalid-timers-array",
699
+ "manifest.timers must be an array when present.",
700
+ `${sourceName}.timers`,
701
+ );
702
+ } else if (Array.isArray(manifest.timers)) {
703
+ manifest.timers.forEach((timer, index) => {
704
+ validateTimer(
705
+ timer,
706
+ issues,
707
+ `${sourceName}.timers[${index}]`,
708
+ methodLookup,
709
+ declaredCapabilities,
710
+ );
711
+ });
712
+ }
713
+
714
+ if (manifest.protocols !== undefined && !Array.isArray(manifest.protocols)) {
715
+ pushIssue(
716
+ issues,
717
+ "error",
718
+ "invalid-protocols-array",
719
+ "manifest.protocols must be an array when present.",
720
+ `${sourceName}.protocols`,
721
+ );
722
+ } else if (Array.isArray(manifest.protocols)) {
723
+ manifest.protocols.forEach((protocol, index) => {
724
+ validateProtocol(
725
+ protocol,
726
+ issues,
727
+ `${sourceName}.protocols[${index}]`,
728
+ methodLookup,
729
+ declaredCapabilities,
730
+ );
731
+ });
732
+ }
406
733
  }
407
734
 
408
735
  return buildComplianceReport({
@@ -414,8 +741,15 @@ export function validatePluginManifest(manifest, options = {}) {
414
741
  });
415
742
  }
416
743
 
744
+ const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
745
+
417
746
  export async function loadManifestFromFile(manifestPath) {
418
747
  const contents = await readFile(manifestPath, "utf8");
748
+ if (contents.length > MAX_MANIFEST_BYTES) {
749
+ throw new Error(
750
+ `Manifest at ${manifestPath} exceeds ${MAX_MANIFEST_BYTES} byte limit.`,
751
+ );
752
+ }
419
753
  return JSON.parse(contents);
420
754
  }
421
755