space-data-module-sdk 0.1.0 → 0.2.5

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 (136) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +236 -73
  3. package/bin/space-data-module.js +24 -0
  4. package/package.json +16 -4
  5. package/schemas/ModuleBundle.fbs +108 -0
  6. package/schemas/PluginInvokeRequest.fbs +18 -0
  7. package/schemas/PluginInvokeResponse.fbs +30 -0
  8. package/schemas/PluginManifest.fbs +33 -1
  9. package/schemas/TypedArenaBuffer.fbs +23 -2
  10. package/src/bundle/codec.js +268 -0
  11. package/src/bundle/constants.js +8 -0
  12. package/src/bundle/index.js +3 -0
  13. package/src/bundle/wasm.js +447 -0
  14. package/src/compiler/compileModule.js +353 -37
  15. package/src/compiler/emceptionNode.js +217 -0
  16. package/src/compiler/flatcSupport.js +66 -0
  17. package/src/compiler/invokeGlue.js +884 -0
  18. package/src/compliance/pluginCompliance.js +575 -1
  19. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts +51 -0
  20. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts.map +1 -0
  21. package/src/generated/orbpro/invoke/plugin-invoke-request.js +131 -0
  22. package/src/generated/orbpro/invoke/plugin-invoke-request.js.map +1 -0
  23. package/src/generated/orbpro/invoke/plugin-invoke-request.ts +173 -0
  24. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts +76 -0
  25. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts.map +1 -0
  26. package/src/generated/orbpro/invoke/plugin-invoke-response.js +184 -0
  27. package/src/generated/orbpro/invoke/plugin-invoke-response.js.map +1 -0
  28. package/src/generated/orbpro/invoke/plugin-invoke-response.ts +243 -0
  29. package/src/generated/orbpro/invoke.d.ts +3 -0
  30. package/src/generated/orbpro/invoke.d.ts.map +1 -0
  31. package/src/generated/orbpro/invoke.js +5 -0
  32. package/src/generated/orbpro/invoke.js.map +1 -0
  33. package/src/generated/orbpro/invoke.ts +6 -0
  34. package/src/generated/orbpro/manifest/accepted-type-set.d.ts +4 -4
  35. package/src/generated/orbpro/manifest/accepted-type-set.d.ts.map +1 -1
  36. package/src/generated/orbpro/manifest/accepted-type-set.js +18 -11
  37. package/src/generated/orbpro/manifest/accepted-type-set.js.map +1 -1
  38. package/src/generated/orbpro/manifest/build-artifact.d.ts +1 -1
  39. package/src/generated/orbpro/manifest/build-artifact.d.ts.map +1 -1
  40. package/src/generated/orbpro/manifest/build-artifact.js +28 -15
  41. package/src/generated/orbpro/manifest/build-artifact.js.map +1 -1
  42. package/src/generated/orbpro/manifest/capability-kind.d.ts +26 -1
  43. package/src/generated/orbpro/manifest/capability-kind.d.ts.map +1 -1
  44. package/src/generated/orbpro/manifest/capability-kind.js +25 -0
  45. package/src/generated/orbpro/manifest/capability-kind.js.map +1 -1
  46. package/src/generated/orbpro/manifest/capability-kind.ts +25 -0
  47. package/src/generated/orbpro/manifest/drain-policy.d.ts.map +1 -1
  48. package/src/generated/orbpro/manifest/drain-policy.js.map +1 -1
  49. package/src/generated/orbpro/manifest/host-capability.d.ts +2 -2
  50. package/src/generated/orbpro/manifest/host-capability.d.ts.map +1 -1
  51. package/src/generated/orbpro/manifest/host-capability.js +19 -11
  52. package/src/generated/orbpro/manifest/host-capability.js.map +1 -1
  53. package/src/generated/orbpro/manifest/invoke-surface.d.ts +8 -0
  54. package/src/generated/orbpro/manifest/invoke-surface.d.ts.map +1 -0
  55. package/src/generated/orbpro/manifest/invoke-surface.js +11 -0
  56. package/src/generated/orbpro/manifest/invoke-surface.js.map +1 -0
  57. package/src/generated/orbpro/manifest/invoke-surface.ts +11 -0
  58. package/src/generated/orbpro/manifest/method-manifest.d.ts +6 -6
  59. package/src/generated/orbpro/manifest/method-manifest.d.ts.map +1 -1
  60. package/src/generated/orbpro/manifest/method-manifest.js +33 -16
  61. package/src/generated/orbpro/manifest/method-manifest.js.map +1 -1
  62. package/src/generated/orbpro/manifest/plugin-family.d.ts.map +1 -1
  63. package/src/generated/orbpro/manifest/plugin-family.js.map +1 -1
  64. package/src/generated/orbpro/manifest/plugin-manifest.d.ts +10 -2
  65. package/src/generated/orbpro/manifest/plugin-manifest.d.ts.map +1 -1
  66. package/src/generated/orbpro/manifest/plugin-manifest.js +48 -9
  67. package/src/generated/orbpro/manifest/plugin-manifest.js.map +1 -1
  68. package/src/generated/orbpro/manifest/plugin-manifest.ts +322 -491
  69. package/src/generated/orbpro/manifest/port-manifest.d.ts +4 -4
  70. package/src/generated/orbpro/manifest/port-manifest.d.ts.map +1 -1
  71. package/src/generated/orbpro/manifest/port-manifest.js +26 -13
  72. package/src/generated/orbpro/manifest/port-manifest.js.map +1 -1
  73. package/src/generated/orbpro/manifest/protocol-spec.d.ts +1 -1
  74. package/src/generated/orbpro/manifest/protocol-spec.d.ts.map +1 -1
  75. package/src/generated/orbpro/manifest/protocol-spec.js +28 -15
  76. package/src/generated/orbpro/manifest/protocol-spec.js.map +1 -1
  77. package/src/generated/orbpro/manifest/timer-spec.d.ts +1 -1
  78. package/src/generated/orbpro/manifest/timer-spec.d.ts.map +1 -1
  79. package/src/generated/orbpro/manifest/timer-spec.js +27 -16
  80. package/src/generated/orbpro/manifest/timer-spec.js.map +1 -1
  81. package/src/generated/orbpro/manifest.d.ts +13 -0
  82. package/src/generated/orbpro/manifest.d.ts.map +1 -0
  83. package/src/generated/orbpro/manifest.js +1 -0
  84. package/src/generated/orbpro/manifest.js.map +1 -0
  85. package/src/generated/orbpro/manifest.ts +16 -0
  86. package/src/generated/orbpro/module/canonicalization-rule.d.ts +48 -0
  87. package/src/generated/orbpro/module/canonicalization-rule.js +95 -0
  88. package/src/generated/orbpro/module/canonicalization-rule.ts +142 -0
  89. package/src/generated/orbpro/module/module-bundle-entry-role.d.ts +11 -0
  90. package/src/generated/orbpro/module/module-bundle-entry-role.js +14 -0
  91. package/src/generated/orbpro/module/module-bundle-entry-role.ts +15 -0
  92. package/src/generated/orbpro/module/module-bundle-entry.d.ts +97 -0
  93. package/src/generated/orbpro/module/module-bundle-entry.js +219 -0
  94. package/src/generated/orbpro/module/module-bundle-entry.ts +287 -0
  95. package/src/generated/orbpro/module/module-bundle.d.ts +86 -0
  96. package/src/generated/orbpro/module/module-bundle.js +213 -0
  97. package/src/generated/orbpro/module/module-bundle.ts +277 -0
  98. package/src/generated/orbpro/module/module-payload-encoding.d.ts +9 -0
  99. package/src/generated/orbpro/module/module-payload-encoding.js +12 -0
  100. package/src/generated/orbpro/module/module-payload-encoding.ts +13 -0
  101. package/src/generated/orbpro/module.d.ts +5 -0
  102. package/src/generated/orbpro/module.js +7 -0
  103. package/src/generated/orbpro/module.ts +9 -0
  104. package/src/generated/orbpro/stream/buffer-mutability.d.ts.map +1 -1
  105. package/src/generated/orbpro/stream/buffer-mutability.js.map +1 -1
  106. package/src/generated/orbpro/stream/buffer-ownership.d.ts.map +1 -1
  107. package/src/generated/orbpro/stream/buffer-ownership.js.map +1 -1
  108. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts +22 -5
  109. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts.map +1 -1
  110. package/src/generated/orbpro/stream/flat-buffer-type-ref.js +107 -17
  111. package/src/generated/orbpro/stream/flat-buffer-type-ref.js.map +1 -1
  112. package/src/generated/orbpro/stream/flat-buffer-type-ref.ts +126 -2
  113. package/src/generated/orbpro/stream/payload-wire-format.d.ts +8 -0
  114. package/src/generated/orbpro/stream/payload-wire-format.d.ts.map +1 -0
  115. package/src/generated/orbpro/stream/payload-wire-format.js +11 -0
  116. package/src/generated/orbpro/stream/payload-wire-format.js.map +1 -0
  117. package/src/generated/orbpro/stream/payload-wire-format.ts +11 -0
  118. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts +4 -4
  119. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts.map +1 -1
  120. package/src/generated/orbpro/stream/typed-arena-buffer.js +42 -24
  121. package/src/generated/orbpro/stream/typed-arena-buffer.js.map +1 -1
  122. package/src/host/abi.js +282 -0
  123. package/src/host/cron.js +247 -0
  124. package/src/host/index.js +3 -0
  125. package/src/host/nodeHost.js +2165 -0
  126. package/src/index.d.ts +958 -0
  127. package/src/index.js +12 -2
  128. package/src/invoke/codec.js +278 -0
  129. package/src/invoke/index.js +9 -0
  130. package/src/manifest/codec.js +10 -2
  131. package/src/manifest/index.js +5 -2
  132. package/src/manifest/normalize.js +90 -1
  133. package/src/runtime/constants.js +29 -0
  134. package/src/transport/pki.js +0 -5
  135. package/src/utils/encoding.js +9 -1
  136. package/src/utils/wasmCrypto.js +49 -1
@@ -2,17 +2,27 @@ import { access, readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import {
5
+ DefaultInvokeExports,
5
6
  DefaultManifestExports,
6
7
  DrainPolicy,
7
8
  ExternalInterfaceDirection,
8
9
  ExternalInterfaceKind,
10
+ InvokeSurface,
11
+ RuntimeTarget,
9
12
  } from "../runtime/constants.js";
10
13
 
11
14
  export const RecommendedCapabilityIds = Object.freeze([
12
15
  "clock",
13
16
  "random",
17
+ "logging",
14
18
  "timers",
19
+ "schedule_cron",
15
20
  "http",
21
+ "tls",
22
+ "websocket",
23
+ "mqtt",
24
+ "tcp",
25
+ "udp",
16
26
  "network",
17
27
  "filesystem",
18
28
  "pipe",
@@ -23,18 +33,52 @@ export const RecommendedCapabilityIds = Object.freeze([
23
33
  "storage_adapter",
24
34
  "storage_query",
25
35
  "storage_write",
36
+ "context_read",
37
+ "context_write",
38
+ "process_exec",
39
+ "crypto_hash",
40
+ "crypto_sign",
41
+ "crypto_verify",
42
+ "crypto_encrypt",
43
+ "crypto_decrypt",
44
+ "crypto_key_agreement",
45
+ "crypto_kdf",
26
46
  "wallet_sign",
27
47
  "ipfs",
28
48
  "scene_access",
49
+ "entity_access",
29
50
  "render_hooks",
30
51
  ]);
31
52
 
32
53
  const RecommendedCapabilitySet = new Set(RecommendedCapabilityIds);
54
+ const RecommendedRuntimeTargets = Object.freeze(Object.values(RuntimeTarget));
55
+ const RecommendedRuntimeTargetSet = new Set(RecommendedRuntimeTargets);
33
56
  const DrainPolicySet = new Set(Object.values(DrainPolicy));
57
+ const InvokeSurfaceSet = new Set(Object.values(InvokeSurface));
34
58
  const ExternalInterfaceDirectionSet = new Set(
35
59
  Object.values(ExternalInterfaceDirection),
36
60
  );
37
61
  const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
62
+ const BrowserIncompatibleCapabilitySet = new Set([
63
+ "filesystem",
64
+ "pipe",
65
+ "network",
66
+ "tcp",
67
+ "udp",
68
+ "mqtt",
69
+ "tls",
70
+ "database",
71
+ "storage_adapter",
72
+ "storage_write",
73
+ "protocol_dial",
74
+ "protocol_handle",
75
+ "process_exec",
76
+ "wallet_sign",
77
+ "ipfs",
78
+ "scene_access",
79
+ "entity_access",
80
+ "render_hooks",
81
+ ]);
38
82
  const IgnoredDirectoryNames = new Set([
39
83
  ".git",
40
84
  ".hg",
@@ -63,6 +107,42 @@ function isNonEmptyString(value) {
63
107
  return typeof value === "string" && value.trim().length > 0;
64
108
  }
65
109
 
110
+ function hasNonEmptyByteSequence(value) {
111
+ if (typeof value === "string") {
112
+ return value.trim().length > 0;
113
+ }
114
+ if (Array.isArray(value)) {
115
+ return value.length > 0;
116
+ }
117
+ if (ArrayBuffer.isView(value)) {
118
+ return value.byteLength > 0;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ function normalizePayloadWireFormatName(value) {
124
+ if (value === undefined || value === null || value === "") {
125
+ return "flatbuffer";
126
+ }
127
+ if (value === 0) {
128
+ return "flatbuffer";
129
+ }
130
+ if (value === 1) {
131
+ return "aligned-binary";
132
+ }
133
+ const normalized = String(value)
134
+ .trim()
135
+ .toLowerCase()
136
+ .replace(/_/g, "-");
137
+ if (normalized === "flatbuffer") {
138
+ return "flatbuffer";
139
+ }
140
+ if (normalized === "aligned-binary") {
141
+ return "aligned-binary";
142
+ }
143
+ return null;
144
+ }
145
+
66
146
  function validateStringField(issues, value, location, label) {
67
147
  if (!isNonEmptyString(value)) {
68
148
  pushIssue(issues, "error", "missing-string", `${label} must be a non-empty string.`, location);
@@ -89,18 +169,93 @@ function validateIntegerField(issues, value, location, label, { min = null } = {
89
169
  return true;
90
170
  }
91
171
 
172
+ function validateOptionalIntegerField(
173
+ issues,
174
+ value,
175
+ location,
176
+ label,
177
+ options = {},
178
+ ) {
179
+ if (value === undefined || value === null) {
180
+ return true;
181
+ }
182
+ return validateIntegerField(issues, value, location, label, options);
183
+ }
184
+
92
185
  function validateAllowedType(type, issues, location) {
93
186
  if (!type || typeof type !== "object" || Array.isArray(type)) {
94
187
  pushIssue(issues, "error", "invalid-type-record", "Allowed type entries must be objects.", location);
95
188
  return;
96
189
  }
190
+ const wireFormat = normalizePayloadWireFormatName(type.wireFormat);
191
+ if (wireFormat === null) {
192
+ pushIssue(
193
+ issues,
194
+ "error",
195
+ "invalid-wire-format",
196
+ 'Allowed type wireFormat must be "flatbuffer" or "aligned-binary".',
197
+ `${location}.wireFormat`,
198
+ );
199
+ return;
200
+ }
201
+ if (wireFormat === "aligned-binary") {
202
+ if (type.acceptsAnyFlatbuffer === true) {
203
+ pushIssue(
204
+ issues,
205
+ "error",
206
+ "accepts-any-flatbuffer-format-conflict",
207
+ "acceptsAnyFlatbuffer can only be used with flatbuffer wireFormat.",
208
+ `${location}.acceptsAnyFlatbuffer`,
209
+ );
210
+ }
211
+ if (!isNonEmptyString(type.schemaName)) {
212
+ pushIssue(
213
+ issues,
214
+ "error",
215
+ "missing-aligned-schema-name",
216
+ "Aligned-binary allowed types must declare schemaName.",
217
+ `${location}.schemaName`,
218
+ );
219
+ }
220
+ if (!isNonEmptyString(type.rootTypeName)) {
221
+ pushIssue(
222
+ issues,
223
+ "error",
224
+ "missing-aligned-root-type-name",
225
+ "Aligned-binary allowed types must declare rootTypeName.",
226
+ `${location}.rootTypeName`,
227
+ );
228
+ }
229
+ validateOptionalIntegerField(
230
+ issues,
231
+ type.fixedStringLength,
232
+ `${location}.fixedStringLength`,
233
+ "Allowed type fixedStringLength",
234
+ { min: 0 },
235
+ );
236
+ validateIntegerField(
237
+ issues,
238
+ type.byteLength,
239
+ `${location}.byteLength`,
240
+ "Aligned-binary allowed type byteLength",
241
+ { min: 1 },
242
+ );
243
+ validateIntegerField(
244
+ issues,
245
+ type.requiredAlignment,
246
+ `${location}.requiredAlignment`,
247
+ "Aligned-binary allowed type requiredAlignment",
248
+ { min: 1 },
249
+ );
250
+ return;
251
+ }
97
252
  if (type.acceptsAnyFlatbuffer === true) {
98
253
  return;
99
254
  }
100
255
  if (
101
256
  !isNonEmptyString(type.schemaName) &&
102
257
  !isNonEmptyString(type.fileIdentifier) &&
103
- !isNonEmptyString(type.schemaHash)
258
+ !hasNonEmptyByteSequence(type.schemaHash)
104
259
  ) {
105
260
  pushIssue(
106
261
  issues,
@@ -110,6 +265,27 @@ function validateAllowedType(type, issues, location) {
110
265
  location,
111
266
  );
112
267
  }
268
+ validateOptionalIntegerField(
269
+ issues,
270
+ type.fixedStringLength,
271
+ `${location}.fixedStringLength`,
272
+ "Allowed type fixedStringLength",
273
+ { min: 0 },
274
+ );
275
+ validateOptionalIntegerField(
276
+ issues,
277
+ type.byteLength,
278
+ `${location}.byteLength`,
279
+ "Allowed type byteLength",
280
+ { min: 0 },
281
+ );
282
+ validateOptionalIntegerField(
283
+ issues,
284
+ type.requiredAlignment,
285
+ `${location}.requiredAlignment`,
286
+ "Allowed type requiredAlignment",
287
+ { min: 0 },
288
+ );
113
289
  }
114
290
 
115
291
  function validateAcceptedTypeSet(typeSet, issues, location) {
@@ -244,6 +420,298 @@ function validateExternalInterface(externalInterface, issues, location, declared
244
420
  }
245
421
  }
246
422
 
423
+ function validateTimer(timer, issues, location, methodLookup, declaredCapabilities) {
424
+ if (!timer || typeof timer !== "object" || Array.isArray(timer)) {
425
+ pushIssue(issues, "error", "invalid-timer", "Timer entries must be objects.", location);
426
+ return;
427
+ }
428
+ const timerIdValid = validateStringField(
429
+ issues,
430
+ timer.timerId,
431
+ `${location}.timerId`,
432
+ "Timer timerId",
433
+ );
434
+ const methodIdValid = validateStringField(
435
+ issues,
436
+ timer.methodId,
437
+ `${location}.methodId`,
438
+ "Timer methodId",
439
+ );
440
+ let method = null;
441
+ if (methodIdValid) {
442
+ method = methodLookup.get(timer.methodId) ?? null;
443
+ if (!method) {
444
+ pushIssue(
445
+ issues,
446
+ "error",
447
+ "unknown-timer-method",
448
+ `Timer "${timer.timerId ?? "timer"}" references unknown method "${timer.methodId}".`,
449
+ `${location}.methodId`,
450
+ );
451
+ }
452
+ }
453
+ if (timer.inputPortId !== undefined && timer.inputPortId !== null) {
454
+ if (!isNonEmptyString(timer.inputPortId)) {
455
+ pushIssue(
456
+ issues,
457
+ "error",
458
+ "invalid-timer-input-port",
459
+ "Timer inputPortId must be a non-empty string when present.",
460
+ `${location}.inputPortId`,
461
+ );
462
+ } else if (
463
+ method &&
464
+ !method.inputPorts.some((port) => port?.portId === timer.inputPortId)
465
+ ) {
466
+ pushIssue(
467
+ issues,
468
+ "error",
469
+ "unknown-timer-input-port",
470
+ `Timer "${timer.timerId ?? "timer"}" references unknown input port "${timer.inputPortId}" on method "${timer.methodId}".`,
471
+ `${location}.inputPortId`,
472
+ );
473
+ }
474
+ }
475
+ if (timer.defaultIntervalMs !== undefined) {
476
+ validateIntegerField(
477
+ issues,
478
+ timer.defaultIntervalMs,
479
+ `${location}.defaultIntervalMs`,
480
+ "Timer defaultIntervalMs",
481
+ { min: 0 },
482
+ );
483
+ }
484
+ if (
485
+ timerIdValid &&
486
+ Array.isArray(declaredCapabilities) &&
487
+ !declaredCapabilities.includes("timers")
488
+ ) {
489
+ pushIssue(
490
+ issues,
491
+ "error",
492
+ "undeclared-timer-capability",
493
+ `Timer "${timer.timerId}" requires the "timers" capability to be declared in manifest.capabilities.`,
494
+ location,
495
+ );
496
+ }
497
+ }
498
+
499
+ function validateProtocol(protocol, issues, location, methodLookup, declaredCapabilities) {
500
+ if (!protocol || typeof protocol !== "object" || Array.isArray(protocol)) {
501
+ pushIssue(
502
+ issues,
503
+ "error",
504
+ "invalid-protocol",
505
+ "Protocol entries must be objects.",
506
+ location,
507
+ );
508
+ return;
509
+ }
510
+ const protocolIdValid = validateStringField(
511
+ issues,
512
+ protocol.protocolId,
513
+ `${location}.protocolId`,
514
+ "Protocol protocolId",
515
+ );
516
+ const methodIdValid = validateStringField(
517
+ issues,
518
+ protocol.methodId,
519
+ `${location}.methodId`,
520
+ "Protocol methodId",
521
+ );
522
+ let method = null;
523
+ if (methodIdValid) {
524
+ method = methodLookup.get(protocol.methodId) ?? null;
525
+ if (!method) {
526
+ pushIssue(
527
+ issues,
528
+ "error",
529
+ "unknown-protocol-method",
530
+ `Protocol "${protocol.protocolId ?? "protocol"}" references unknown method "${protocol.methodId}".`,
531
+ `${location}.methodId`,
532
+ );
533
+ }
534
+ }
535
+ if (protocol.inputPortId !== undefined && protocol.inputPortId !== null) {
536
+ if (!isNonEmptyString(protocol.inputPortId)) {
537
+ pushIssue(
538
+ issues,
539
+ "error",
540
+ "invalid-protocol-input-port",
541
+ "Protocol inputPortId must be a non-empty string when present.",
542
+ `${location}.inputPortId`,
543
+ );
544
+ } else if (
545
+ method &&
546
+ !method.inputPorts.some((port) => port?.portId === protocol.inputPortId)
547
+ ) {
548
+ pushIssue(
549
+ issues,
550
+ "error",
551
+ "unknown-protocol-input-port",
552
+ `Protocol "${protocol.protocolId ?? "protocol"}" references unknown input port "${protocol.inputPortId}" on method "${protocol.methodId}".`,
553
+ `${location}.inputPortId`,
554
+ );
555
+ }
556
+ }
557
+ if (protocol.outputPortId !== undefined && protocol.outputPortId !== null) {
558
+ if (!isNonEmptyString(protocol.outputPortId)) {
559
+ pushIssue(
560
+ issues,
561
+ "error",
562
+ "invalid-protocol-output-port",
563
+ "Protocol outputPortId must be a non-empty string when present.",
564
+ `${location}.outputPortId`,
565
+ );
566
+ } else if (
567
+ method &&
568
+ !method.outputPorts.some((port) => port?.portId === protocol.outputPortId)
569
+ ) {
570
+ pushIssue(
571
+ issues,
572
+ "error",
573
+ "unknown-protocol-output-port",
574
+ `Protocol "${protocol.protocolId ?? "protocol"}" references unknown output port "${protocol.outputPortId}" on method "${protocol.methodId}".`,
575
+ `${location}.outputPortId`,
576
+ );
577
+ }
578
+ }
579
+ if (
580
+ protocolIdValid &&
581
+ Array.isArray(declaredCapabilities) &&
582
+ !declaredCapabilities.includes("protocol_handle") &&
583
+ !declaredCapabilities.includes("protocol_dial")
584
+ ) {
585
+ pushIssue(
586
+ issues,
587
+ "error",
588
+ "undeclared-protocol-capability",
589
+ `Protocol "${protocol.protocolId}" requires "protocol_handle" or "protocol_dial" to be declared in manifest.capabilities.`,
590
+ location,
591
+ );
592
+ }
593
+ }
594
+
595
+ function validateRuntimeTargets(runtimeTargets, declaredCapabilities, issues, sourceName) {
596
+ if (runtimeTargets === undefined) {
597
+ return;
598
+ }
599
+ if (!Array.isArray(runtimeTargets)) {
600
+ pushIssue(
601
+ issues,
602
+ "error",
603
+ "invalid-runtime-targets",
604
+ "manifest.runtimeTargets must be an array of non-empty strings when present.",
605
+ `${sourceName}.runtimeTargets`,
606
+ );
607
+ return;
608
+ }
609
+ const seenTargets = new Set();
610
+ for (const target of runtimeTargets) {
611
+ if (!isNonEmptyString(target)) {
612
+ pushIssue(
613
+ issues,
614
+ "error",
615
+ "invalid-runtime-target",
616
+ "Runtime target entries must be non-empty strings.",
617
+ `${sourceName}.runtimeTargets`,
618
+ );
619
+ continue;
620
+ }
621
+ if (seenTargets.has(target)) {
622
+ pushIssue(
623
+ issues,
624
+ "warning",
625
+ "duplicate-runtime-target",
626
+ `Runtime target "${target}" is declared more than once.`,
627
+ `${sourceName}.runtimeTargets`,
628
+ );
629
+ continue;
630
+ }
631
+ seenTargets.add(target);
632
+ if (!RecommendedRuntimeTargetSet.has(target)) {
633
+ pushIssue(
634
+ issues,
635
+ "warning",
636
+ "noncanonical-runtime-target",
637
+ `Runtime target "${target}" is not in the current canonical runtime target set.`,
638
+ `${sourceName}.runtimeTargets`,
639
+ );
640
+ }
641
+ }
642
+ if (
643
+ seenTargets.has(RuntimeTarget.BROWSER) &&
644
+ Array.isArray(declaredCapabilities)
645
+ ) {
646
+ for (const capability of declaredCapabilities) {
647
+ if (!BrowserIncompatibleCapabilitySet.has(capability)) {
648
+ continue;
649
+ }
650
+ pushIssue(
651
+ issues,
652
+ "error",
653
+ "capability-runtime-conflict",
654
+ `Capability "${capability}" is not available in the canonical browser runtime target.`,
655
+ `${sourceName}.capabilities`,
656
+ );
657
+ }
658
+ }
659
+ }
660
+
661
+ function validateInvokeSurfaces(invokeSurfaces, issues, sourceName) {
662
+ if (invokeSurfaces === undefined) {
663
+ return [];
664
+ }
665
+ if (!Array.isArray(invokeSurfaces)) {
666
+ pushIssue(
667
+ issues,
668
+ "error",
669
+ "invalid-invoke-surfaces",
670
+ "manifest.invokeSurfaces must be an array of non-empty strings when present.",
671
+ `${sourceName}.invokeSurfaces`,
672
+ );
673
+ return [];
674
+ }
675
+
676
+ const seen = new Set();
677
+ const normalized = [];
678
+ for (const surface of invokeSurfaces) {
679
+ if (!isNonEmptyString(surface)) {
680
+ pushIssue(
681
+ issues,
682
+ "error",
683
+ "invalid-invoke-surface",
684
+ "Invoke surface entries must be non-empty strings.",
685
+ `${sourceName}.invokeSurfaces`,
686
+ );
687
+ continue;
688
+ }
689
+ if (!InvokeSurfaceSet.has(surface)) {
690
+ pushIssue(
691
+ issues,
692
+ "error",
693
+ "unknown-invoke-surface",
694
+ `Invoke surface "${surface}" is invalid.`,
695
+ `${sourceName}.invokeSurfaces`,
696
+ );
697
+ continue;
698
+ }
699
+ if (seen.has(surface)) {
700
+ pushIssue(
701
+ issues,
702
+ "warning",
703
+ "duplicate-invoke-surface",
704
+ `Invoke surface "${surface}" is declared more than once.`,
705
+ `${sourceName}.invokeSurfaces`,
706
+ );
707
+ continue;
708
+ }
709
+ seen.add(surface);
710
+ normalized.push(surface);
711
+ }
712
+ return normalized;
713
+ }
714
+
247
715
  export function validatePluginManifest(manifest, options = {}) {
248
716
  const { sourceName = "manifest" } = options;
249
717
  const issues = [];
@@ -307,6 +775,13 @@ export function validatePluginManifest(manifest, options = {}) {
307
775
  }
308
776
  }
309
777
  }
778
+ validateRuntimeTargets(
779
+ manifest.runtimeTargets,
780
+ declaredCapabilities,
781
+ issues,
782
+ sourceName,
783
+ );
784
+ validateInvokeSurfaces(manifest.invokeSurfaces, issues, sourceName);
310
785
 
311
786
  if (!Array.isArray(manifest.externalInterfaces)) {
312
787
  pushIssue(
@@ -337,6 +812,7 @@ export function validatePluginManifest(manifest, options = {}) {
337
812
  );
338
813
  } else {
339
814
  const seenMethodIds = new Set();
815
+ const methodLookup = new Map();
340
816
  manifest.methods.forEach((method, index) => {
341
817
  const location = `${sourceName}.methods[${index}]`;
342
818
  if (!method || typeof method !== "object" || Array.isArray(method)) {
@@ -355,6 +831,7 @@ export function validatePluginManifest(manifest, options = {}) {
355
831
  );
356
832
  }
357
833
  seenMethodIds.add(method.methodId);
834
+ methodLookup.set(method.methodId, method);
358
835
  }
359
836
  if (!Array.isArray(method.inputPorts) || method.inputPorts.length === 0) {
360
837
  pushIssue(
@@ -403,6 +880,64 @@ export function validatePluginManifest(manifest, options = {}) {
403
880
  );
404
881
  }
405
882
  });
883
+
884
+ if (manifest.timers !== undefined && !Array.isArray(manifest.timers)) {
885
+ pushIssue(
886
+ issues,
887
+ "error",
888
+ "invalid-timers-array",
889
+ "manifest.timers must be an array when present.",
890
+ `${sourceName}.timers`,
891
+ );
892
+ } else if (Array.isArray(manifest.timers)) {
893
+ manifest.timers.forEach((timer, index) => {
894
+ validateTimer(
895
+ timer,
896
+ issues,
897
+ `${sourceName}.timers[${index}]`,
898
+ methodLookup,
899
+ declaredCapabilities,
900
+ );
901
+ });
902
+ }
903
+
904
+ if (manifest.protocols !== undefined && !Array.isArray(manifest.protocols)) {
905
+ pushIssue(
906
+ issues,
907
+ "error",
908
+ "invalid-protocols-array",
909
+ "manifest.protocols must be an array when present.",
910
+ `${sourceName}.protocols`,
911
+ );
912
+ } else if (Array.isArray(manifest.protocols)) {
913
+ manifest.protocols.forEach((protocol, index) => {
914
+ validateProtocol(
915
+ protocol,
916
+ issues,
917
+ `${sourceName}.protocols[${index}]`,
918
+ methodLookup,
919
+ declaredCapabilities,
920
+ );
921
+ });
922
+ }
923
+ }
924
+
925
+ if (manifest.schemasUsed !== undefined && !Array.isArray(manifest.schemasUsed)) {
926
+ pushIssue(
927
+ issues,
928
+ "error",
929
+ "invalid-schemas-used-array",
930
+ "manifest.schemasUsed must be an array when present.",
931
+ `${sourceName}.schemasUsed`,
932
+ );
933
+ } else if (Array.isArray(manifest.schemasUsed)) {
934
+ manifest.schemasUsed.forEach((typeRef, index) => {
935
+ validateAllowedType(
936
+ typeRef,
937
+ issues,
938
+ `${sourceName}.schemasUsed[${index}]`,
939
+ );
940
+ });
406
941
  }
407
942
 
408
943
  return buildComplianceReport({
@@ -414,8 +949,15 @@ export function validatePluginManifest(manifest, options = {}) {
414
949
  });
415
950
  }
416
951
 
952
+ const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
953
+
417
954
  export async function loadManifestFromFile(manifestPath) {
418
955
  const contents = await readFile(manifestPath, "utf8");
956
+ if (contents.length > MAX_MANIFEST_BYTES) {
957
+ throw new Error(
958
+ `Manifest at ${manifestPath} exceeds ${MAX_MANIFEST_BYTES} byte limit.`,
959
+ );
960
+ }
419
961
  return JSON.parse(contents);
420
962
  }
421
963
 
@@ -441,6 +983,9 @@ export async function validatePluginArtifact(options) {
441
983
  const issues = [...report.issues];
442
984
  let resolvedExportNames = [];
443
985
  let checkedArtifact = false;
986
+ const declaredInvokeSurfaces = Array.isArray(manifest?.invokeSurfaces)
987
+ ? manifest.invokeSurfaces.filter((surface) => InvokeSurfaceSet.has(surface))
988
+ : [];
444
989
 
445
990
  if (Array.isArray(exportNames)) {
446
991
  resolvedExportNames = [...exportNames];
@@ -465,6 +1010,35 @@ export async function validatePluginArtifact(options) {
465
1010
  );
466
1011
  }
467
1012
  }
1013
+ if (declaredInvokeSurfaces.includes(InvokeSurface.DIRECT)) {
1014
+ for (const symbol of [
1015
+ DefaultInvokeExports.invokeSymbol,
1016
+ DefaultInvokeExports.allocSymbol,
1017
+ DefaultInvokeExports.freeSymbol,
1018
+ ]) {
1019
+ if (!resolvedExportNames.includes(symbol)) {
1020
+ pushIssue(
1021
+ issues,
1022
+ "error",
1023
+ "missing-plugin-invoke-export",
1024
+ `Plugin artifact is missing required direct invoke export "${symbol}".`,
1025
+ wasmPath ?? sourceName,
1026
+ );
1027
+ }
1028
+ }
1029
+ }
1030
+ if (
1031
+ declaredInvokeSurfaces.includes(InvokeSurface.COMMAND) &&
1032
+ !resolvedExportNames.includes(DefaultInvokeExports.commandSymbol)
1033
+ ) {
1034
+ pushIssue(
1035
+ issues,
1036
+ "error",
1037
+ "missing-plugin-command-export",
1038
+ `Plugin artifact is missing required command export "${DefaultInvokeExports.commandSymbol}".`,
1039
+ wasmPath ?? sourceName,
1040
+ );
1041
+ }
468
1042
  } else {
469
1043
  pushIssue(
470
1044
  issues,