space-data-module-sdk 0.1.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 (102) hide show
  1. package/README.md +139 -0
  2. package/bin/space-data-module.js +230 -0
  3. package/package.json +40 -0
  4. package/schemas/PluginManifest.fbs +144 -0
  5. package/schemas/TypedArenaBuffer.fbs +78 -0
  6. package/src/auth/canonicalize.js +72 -0
  7. package/src/auth/index.js +10 -0
  8. package/src/auth/permissions.js +190 -0
  9. package/src/compiler/compileModule.js +237 -0
  10. package/src/compiler/index.js +7 -0
  11. package/src/compliance/index.js +56 -0
  12. package/src/compliance/pluginCompliance.js +574 -0
  13. package/src/embeddedManifest.js +124 -0
  14. package/src/generated/orbpro/manifest/accepted-type-set.d.ts +45 -0
  15. package/src/generated/orbpro/manifest/accepted-type-set.d.ts.map +1 -0
  16. package/src/generated/orbpro/manifest/accepted-type-set.js +100 -0
  17. package/src/generated/orbpro/manifest/accepted-type-set.js.map +1 -0
  18. package/src/generated/orbpro/manifest/accepted-type-set.ts +200 -0
  19. package/src/generated/orbpro/manifest/build-artifact.d.ts +41 -0
  20. package/src/generated/orbpro/manifest/build-artifact.d.ts.map +1 -0
  21. package/src/generated/orbpro/manifest/build-artifact.js +105 -0
  22. package/src/generated/orbpro/manifest/build-artifact.js.map +1 -0
  23. package/src/generated/orbpro/manifest/build-artifact.ts +193 -0
  24. package/src/generated/orbpro/manifest/capability-kind.d.ts +17 -0
  25. package/src/generated/orbpro/manifest/capability-kind.d.ts.map +1 -0
  26. package/src/generated/orbpro/manifest/capability-kind.js +20 -0
  27. package/src/generated/orbpro/manifest/capability-kind.js.map +1 -0
  28. package/src/generated/orbpro/manifest/capability-kind.ts +20 -0
  29. package/src/generated/orbpro/manifest/drain-policy.d.ts +9 -0
  30. package/src/generated/orbpro/manifest/drain-policy.d.ts.map +1 -0
  31. package/src/generated/orbpro/manifest/drain-policy.js +12 -0
  32. package/src/generated/orbpro/manifest/drain-policy.js.map +1 -0
  33. package/src/generated/orbpro/manifest/drain-policy.ts +12 -0
  34. package/src/generated/orbpro/manifest/host-capability.d.ts +36 -0
  35. package/src/generated/orbpro/manifest/host-capability.d.ts.map +1 -0
  36. package/src/generated/orbpro/manifest/host-capability.js +91 -0
  37. package/src/generated/orbpro/manifest/host-capability.js.map +1 -0
  38. package/src/generated/orbpro/manifest/host-capability.ts +161 -0
  39. package/src/generated/orbpro/manifest/method-manifest.d.ts +53 -0
  40. package/src/generated/orbpro/manifest/method-manifest.d.ts.map +1 -0
  41. package/src/generated/orbpro/manifest/method-manifest.js +154 -0
  42. package/src/generated/orbpro/manifest/method-manifest.js.map +1 -0
  43. package/src/generated/orbpro/manifest/method-manifest.ts +306 -0
  44. package/src/generated/orbpro/manifest/plugin-family.d.ts +17 -0
  45. package/src/generated/orbpro/manifest/plugin-family.d.ts.map +1 -0
  46. package/src/generated/orbpro/manifest/plugin-family.js +20 -0
  47. package/src/generated/orbpro/manifest/plugin-family.js.map +1 -0
  48. package/src/generated/orbpro/manifest/plugin-family.ts +20 -0
  49. package/src/generated/orbpro/manifest/plugin-manifest.d.ts +85 -0
  50. package/src/generated/orbpro/manifest/plugin-manifest.d.ts.map +1 -0
  51. package/src/generated/orbpro/manifest/plugin-manifest.js +268 -0
  52. package/src/generated/orbpro/manifest/plugin-manifest.js.map +1 -0
  53. package/src/generated/orbpro/manifest/plugin-manifest.ts +562 -0
  54. package/src/generated/orbpro/manifest/port-manifest.d.ts +70 -0
  55. package/src/generated/orbpro/manifest/port-manifest.d.ts.map +1 -0
  56. package/src/generated/orbpro/manifest/port-manifest.js +150 -0
  57. package/src/generated/orbpro/manifest/port-manifest.js.map +1 -0
  58. package/src/generated/orbpro/manifest/port-manifest.ts +284 -0
  59. package/src/generated/orbpro/manifest/protocol-spec.d.ts +41 -0
  60. package/src/generated/orbpro/manifest/protocol-spec.d.ts.map +1 -0
  61. package/src/generated/orbpro/manifest/protocol-spec.js +105 -0
  62. package/src/generated/orbpro/manifest/protocol-spec.js.map +1 -0
  63. package/src/generated/orbpro/manifest/protocol-spec.ts +205 -0
  64. package/src/generated/orbpro/manifest/timer-spec.d.ts +40 -0
  65. package/src/generated/orbpro/manifest/timer-spec.d.ts.map +1 -0
  66. package/src/generated/orbpro/manifest/timer-spec.js +104 -0
  67. package/src/generated/orbpro/manifest/timer-spec.js.map +1 -0
  68. package/src/generated/orbpro/manifest/timer-spec.ts +195 -0
  69. package/src/generated/orbpro/manifest.js +14 -0
  70. package/src/generated/orbpro/stream/buffer-mutability.d.ts +9 -0
  71. package/src/generated/orbpro/stream/buffer-mutability.d.ts.map +1 -0
  72. package/src/generated/orbpro/stream/buffer-mutability.js +12 -0
  73. package/src/generated/orbpro/stream/buffer-mutability.js.map +1 -0
  74. package/src/generated/orbpro/stream/buffer-mutability.ts +12 -0
  75. package/src/generated/orbpro/stream/buffer-ownership.d.ts +10 -0
  76. package/src/generated/orbpro/stream/buffer-ownership.d.ts.map +1 -0
  77. package/src/generated/orbpro/stream/buffer-ownership.js +13 -0
  78. package/src/generated/orbpro/stream/buffer-ownership.js.map +1 -0
  79. package/src/generated/orbpro/stream/buffer-ownership.ts +13 -0
  80. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts +51 -0
  81. package/src/generated/orbpro/stream/flat-buffer-type-ref.d.ts.map +1 -0
  82. package/src/generated/orbpro/stream/flat-buffer-type-ref.js +115 -0
  83. package/src/generated/orbpro/stream/flat-buffer-type-ref.js.map +1 -0
  84. package/src/generated/orbpro/stream/flat-buffer-type-ref.ts +222 -0
  85. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts +100 -0
  86. package/src/generated/orbpro/stream/typed-arena-buffer.d.ts.map +1 -0
  87. package/src/generated/orbpro/stream/typed-arena-buffer.js +215 -0
  88. package/src/generated/orbpro/stream/typed-arena-buffer.js.map +1 -0
  89. package/src/generated/orbpro/stream/typed-arena-buffer.ts +344 -0
  90. package/src/index.js +8 -0
  91. package/src/manifest/codec.js +40 -0
  92. package/src/manifest/index.js +9 -0
  93. package/src/manifest/normalize.js +275 -0
  94. package/src/runtime/bufferLike.js +28 -0
  95. package/src/runtime/constants.js +34 -0
  96. package/src/standards/index.js +153 -0
  97. package/src/standards/sharedCatalog.js +196 -0
  98. package/src/transport/index.js +8 -0
  99. package/src/transport/pki.js +140 -0
  100. package/src/utils/crypto.js +8 -0
  101. package/src/utils/encoding.js +54 -0
  102. package/src/utils/wasmCrypto.js +70 -0
@@ -0,0 +1,574 @@
1
+ import { access, readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ DefaultManifestExports,
6
+ DrainPolicy,
7
+ ExternalInterfaceDirection,
8
+ ExternalInterfaceKind,
9
+ } from "../runtime/constants.js";
10
+
11
+ export const RecommendedCapabilityIds = Object.freeze([
12
+ "clock",
13
+ "random",
14
+ "timers",
15
+ "http",
16
+ "network",
17
+ "filesystem",
18
+ "pipe",
19
+ "pubsub",
20
+ "protocol_handle",
21
+ "protocol_dial",
22
+ "database",
23
+ "storage_adapter",
24
+ "storage_query",
25
+ "storage_write",
26
+ "wallet_sign",
27
+ "ipfs",
28
+ "scene_access",
29
+ "render_hooks",
30
+ ]);
31
+
32
+ const RecommendedCapabilitySet = new Set(RecommendedCapabilityIds);
33
+ const DrainPolicySet = new Set(Object.values(DrainPolicy));
34
+ const ExternalInterfaceDirectionSet = new Set(
35
+ Object.values(ExternalInterfaceDirection),
36
+ );
37
+ const ExternalInterfaceKindSet = new Set(Object.values(ExternalInterfaceKind));
38
+ const IgnoredDirectoryNames = new Set([
39
+ ".git",
40
+ ".hg",
41
+ ".svn",
42
+ ".next",
43
+ ".turbo",
44
+ "build",
45
+ "Build",
46
+ "dist",
47
+ "coverage",
48
+ "node_modules",
49
+ "vendor",
50
+ "docs-html",
51
+ "out",
52
+ ]);
53
+
54
+ function createIssue(severity, code, message, location) {
55
+ return { severity, code, message, location };
56
+ }
57
+
58
+ function pushIssue(issues, severity, code, message, location) {
59
+ issues.push(createIssue(severity, code, message, location));
60
+ }
61
+
62
+ function isNonEmptyString(value) {
63
+ return typeof value === "string" && value.trim().length > 0;
64
+ }
65
+
66
+ function validateStringField(issues, value, location, label) {
67
+ if (!isNonEmptyString(value)) {
68
+ pushIssue(issues, "error", "missing-string", `${label} must be a non-empty string.`, location);
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ function validateIntegerField(issues, value, location, label, { min = null } = {}) {
75
+ if (!Number.isInteger(value)) {
76
+ pushIssue(issues, "error", "invalid-integer", `${label} must be an integer.`, location);
77
+ return false;
78
+ }
79
+ if (min !== null && value < min) {
80
+ pushIssue(
81
+ issues,
82
+ "error",
83
+ "integer-range",
84
+ `${label} must be greater than or equal to ${min}.`,
85
+ location,
86
+ );
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+
92
+ function validateAllowedType(type, issues, location) {
93
+ if (!type || typeof type !== "object" || Array.isArray(type)) {
94
+ pushIssue(issues, "error", "invalid-type-record", "Allowed type entries must be objects.", location);
95
+ return;
96
+ }
97
+ if (type.acceptsAnyFlatbuffer === true) {
98
+ return;
99
+ }
100
+ if (
101
+ !isNonEmptyString(type.schemaName) &&
102
+ !isNonEmptyString(type.fileIdentifier) &&
103
+ !isNonEmptyString(type.schemaHash)
104
+ ) {
105
+ pushIssue(
106
+ issues,
107
+ "error",
108
+ "missing-type-identity",
109
+ "Allowed type must declare at least one stable identity field: schemaName, fileIdentifier, or schemaHash.",
110
+ location,
111
+ );
112
+ }
113
+ }
114
+
115
+ function validateAcceptedTypeSet(typeSet, issues, location) {
116
+ if (!typeSet || typeof typeSet !== "object" || Array.isArray(typeSet)) {
117
+ pushIssue(issues, "error", "invalid-type-set", "Accepted type sets must be objects.", location);
118
+ return;
119
+ }
120
+ validateStringField(issues, typeSet.setId, `${location}.setId`, "Accepted type set setId");
121
+ if (!Array.isArray(typeSet.allowedTypes) || typeSet.allowedTypes.length === 0) {
122
+ pushIssue(
123
+ issues,
124
+ "error",
125
+ "missing-allowed-types",
126
+ "Accepted type sets must declare one or more allowedTypes.",
127
+ `${location}.allowedTypes`,
128
+ );
129
+ return;
130
+ }
131
+ typeSet.allowedTypes.forEach((allowedType, index) => {
132
+ validateAllowedType(allowedType, issues, `${location}.allowedTypes[${index}]`);
133
+ });
134
+ }
135
+
136
+ function validatePort(port, issues, location, label) {
137
+ if (!port || typeof port !== "object" || Array.isArray(port)) {
138
+ pushIssue(issues, "error", "invalid-port", `${label} entries must be objects.`, location);
139
+ return;
140
+ }
141
+ validateStringField(issues, port.portId, `${location}.portId`, `${label} portId`);
142
+ if (!Array.isArray(port.acceptedTypeSets) || port.acceptedTypeSets.length === 0) {
143
+ pushIssue(
144
+ issues,
145
+ "error",
146
+ "missing-accepted-type-sets",
147
+ `${label} must declare one or more acceptedTypeSets.`,
148
+ `${location}.acceptedTypeSets`,
149
+ );
150
+ } else {
151
+ port.acceptedTypeSets.forEach((typeSet, index) => {
152
+ validateAcceptedTypeSet(typeSet, issues, `${location}.acceptedTypeSets[${index}]`);
153
+ });
154
+ }
155
+ const minStreamsValid = validateIntegerField(
156
+ issues,
157
+ port.minStreams,
158
+ `${location}.minStreams`,
159
+ `${label} minStreams`,
160
+ { min: 0 },
161
+ );
162
+ const maxStreamsValid = validateIntegerField(
163
+ issues,
164
+ port.maxStreams,
165
+ `${location}.maxStreams`,
166
+ `${label} maxStreams`,
167
+ { min: 0 },
168
+ );
169
+ if (minStreamsValid && maxStreamsValid && port.maxStreams < port.minStreams) {
170
+ pushIssue(
171
+ issues,
172
+ "error",
173
+ "stream-range",
174
+ `${label} maxStreams must be greater than or equal to minStreams.`,
175
+ location,
176
+ );
177
+ }
178
+ if (typeof port.required !== "boolean") {
179
+ pushIssue(issues, "error", "invalid-required-flag", `${label} required must be a boolean.`, `${location}.required`);
180
+ }
181
+ }
182
+
183
+ function validateExternalInterface(externalInterface, issues, location, declaredCapabilities) {
184
+ if (!externalInterface || typeof externalInterface !== "object" || Array.isArray(externalInterface)) {
185
+ pushIssue(
186
+ issues,
187
+ "error",
188
+ "invalid-external-interface",
189
+ "externalInterfaces entries must be objects.",
190
+ location,
191
+ );
192
+ return;
193
+ }
194
+ validateStringField(
195
+ issues,
196
+ externalInterface.interfaceId,
197
+ `${location}.interfaceId`,
198
+ "External interface interfaceId",
199
+ );
200
+ if (!isNonEmptyString(externalInterface.kind)) {
201
+ pushIssue(issues, "error", "missing-interface-kind", "External interface kind must be a non-empty string.", `${location}.kind`);
202
+ } else if (!ExternalInterfaceKindSet.has(externalInterface.kind)) {
203
+ pushIssue(
204
+ issues,
205
+ "warning",
206
+ "unknown-interface-kind",
207
+ `External interface kind "${externalInterface.kind}" is not in the canonical SDN interface kind set.`,
208
+ `${location}.kind`,
209
+ );
210
+ }
211
+ if (!isNonEmptyString(externalInterface.direction)) {
212
+ pushIssue(
213
+ issues,
214
+ "error",
215
+ "missing-interface-direction",
216
+ "External interface direction must be a non-empty string.",
217
+ `${location}.direction`,
218
+ );
219
+ } else if (!ExternalInterfaceDirectionSet.has(externalInterface.direction)) {
220
+ pushIssue(
221
+ issues,
222
+ "error",
223
+ "invalid-interface-direction",
224
+ `External interface direction "${externalInterface.direction}" is invalid.`,
225
+ `${location}.direction`,
226
+ );
227
+ }
228
+ if (!isNonEmptyString(externalInterface.capability)) {
229
+ pushIssue(
230
+ issues,
231
+ "warning",
232
+ "missing-interface-capability",
233
+ "External interface should declare the coarse capability it consumes.",
234
+ `${location}.capability`,
235
+ );
236
+ } else if (Array.isArray(declaredCapabilities) && !declaredCapabilities.includes(externalInterface.capability)) {
237
+ pushIssue(
238
+ issues,
239
+ "error",
240
+ "undeclared-interface-capability",
241
+ `External interface capability "${externalInterface.capability}" is not declared in manifest.capabilities.`,
242
+ `${location}.capability`,
243
+ );
244
+ }
245
+ }
246
+
247
+ export function validatePluginManifest(manifest, options = {}) {
248
+ const { sourceName = "manifest" } = options;
249
+ const issues = [];
250
+
251
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
252
+ pushIssue(issues, "error", "invalid-manifest", "Manifest must be a JSON object.", sourceName);
253
+ return buildComplianceReport({
254
+ sourceName,
255
+ manifest,
256
+ issues,
257
+ exportNames: [],
258
+ checkedArtifact: false,
259
+ });
260
+ }
261
+
262
+ validateStringField(issues, manifest.pluginId, `${sourceName}.pluginId`, "pluginId");
263
+ validateStringField(issues, manifest.name, `${sourceName}.name`, "name");
264
+ validateStringField(issues, manifest.version, `${sourceName}.version`, "version");
265
+ validateStringField(issues, manifest.pluginFamily, `${sourceName}.pluginFamily`, "pluginFamily");
266
+
267
+ const declaredCapabilities = manifest.capabilities;
268
+ if (!Array.isArray(declaredCapabilities)) {
269
+ pushIssue(
270
+ issues,
271
+ "warning",
272
+ "missing-capabilities-array",
273
+ "manifest.capabilities should be present as an explicit array, even when empty.",
274
+ `${sourceName}.capabilities`,
275
+ );
276
+ } else {
277
+ const seenCapabilities = new Set();
278
+ for (const capability of declaredCapabilities) {
279
+ if (!isNonEmptyString(capability)) {
280
+ pushIssue(
281
+ issues,
282
+ "error",
283
+ "invalid-capability",
284
+ "Capability entries must be non-empty strings.",
285
+ `${sourceName}.capabilities`,
286
+ );
287
+ continue;
288
+ }
289
+ if (seenCapabilities.has(capability)) {
290
+ pushIssue(
291
+ issues,
292
+ "warning",
293
+ "duplicate-capability",
294
+ `Capability "${capability}" is declared more than once.`,
295
+ `${sourceName}.capabilities`,
296
+ );
297
+ }
298
+ seenCapabilities.add(capability);
299
+ if (!RecommendedCapabilitySet.has(capability)) {
300
+ pushIssue(
301
+ issues,
302
+ "warning",
303
+ "noncanonical-capability",
304
+ `Capability "${capability}" is not in the current canonical SDN coarse capability set.`,
305
+ `${sourceName}.capabilities`,
306
+ );
307
+ }
308
+ }
309
+ }
310
+
311
+ if (!Array.isArray(manifest.externalInterfaces)) {
312
+ pushIssue(
313
+ issues,
314
+ "warning",
315
+ "missing-external-interfaces-array",
316
+ "manifest.externalInterfaces should be present as an explicit array, even when empty.",
317
+ `${sourceName}.externalInterfaces`,
318
+ );
319
+ } else {
320
+ manifest.externalInterfaces.forEach((externalInterface, index) => {
321
+ validateExternalInterface(
322
+ externalInterface,
323
+ issues,
324
+ `${sourceName}.externalInterfaces[${index}]`,
325
+ declaredCapabilities,
326
+ );
327
+ });
328
+ }
329
+
330
+ if (!Array.isArray(manifest.methods) || manifest.methods.length === 0) {
331
+ pushIssue(
332
+ issues,
333
+ "error",
334
+ "missing-methods",
335
+ "manifest.methods must declare at least one method.",
336
+ `${sourceName}.methods`,
337
+ );
338
+ } else {
339
+ const seenMethodIds = new Set();
340
+ manifest.methods.forEach((method, index) => {
341
+ const location = `${sourceName}.methods[${index}]`;
342
+ if (!method || typeof method !== "object" || Array.isArray(method)) {
343
+ pushIssue(issues, "error", "invalid-method", "Method entries must be objects.", location);
344
+ return;
345
+ }
346
+ const methodIdValid = validateStringField(issues, method.methodId, `${location}.methodId`, "methodId");
347
+ if (methodIdValid) {
348
+ if (seenMethodIds.has(method.methodId)) {
349
+ pushIssue(
350
+ issues,
351
+ "error",
352
+ "duplicate-method-id",
353
+ `Method "${method.methodId}" is declared more than once.`,
354
+ `${location}.methodId`,
355
+ );
356
+ }
357
+ seenMethodIds.add(method.methodId);
358
+ }
359
+ if (!Array.isArray(method.inputPorts) || method.inputPorts.length === 0) {
360
+ pushIssue(
361
+ issues,
362
+ "error",
363
+ "missing-input-ports",
364
+ "Methods must declare one or more inputPorts.",
365
+ `${location}.inputPorts`,
366
+ );
367
+ } else {
368
+ method.inputPorts.forEach((port, portIndex) => {
369
+ validatePort(port, issues, `${location}.inputPorts[${portIndex}]`, "Input port");
370
+ });
371
+ }
372
+ if (!Array.isArray(method.outputPorts)) {
373
+ pushIssue(
374
+ issues,
375
+ "error",
376
+ "missing-output-ports",
377
+ "Methods must declare outputPorts as an array.",
378
+ `${location}.outputPorts`,
379
+ );
380
+ } else {
381
+ method.outputPorts.forEach((port, portIndex) => {
382
+ validatePort(port, issues, `${location}.outputPorts[${portIndex}]`, "Output port");
383
+ });
384
+ }
385
+ validateIntegerField(issues, method.maxBatch, `${location}.maxBatch`, "maxBatch", {
386
+ min: 1,
387
+ });
388
+ if (!isNonEmptyString(method.drainPolicy)) {
389
+ pushIssue(
390
+ issues,
391
+ "error",
392
+ "missing-drain-policy",
393
+ "Methods must declare drainPolicy.",
394
+ `${location}.drainPolicy`,
395
+ );
396
+ } else if (!DrainPolicySet.has(method.drainPolicy)) {
397
+ pushIssue(
398
+ issues,
399
+ "error",
400
+ "invalid-drain-policy",
401
+ `Drain policy "${method.drainPolicy}" is invalid.`,
402
+ `${location}.drainPolicy`,
403
+ );
404
+ }
405
+ });
406
+ }
407
+
408
+ return buildComplianceReport({
409
+ sourceName,
410
+ manifest,
411
+ issues,
412
+ exportNames: [],
413
+ checkedArtifact: false,
414
+ });
415
+ }
416
+
417
+ export async function loadManifestFromFile(manifestPath) {
418
+ const contents = await readFile(manifestPath, "utf8");
419
+ return JSON.parse(contents);
420
+ }
421
+
422
+ export function getWasmExportNames(wasmBytes) {
423
+ const module = new WebAssembly.Module(wasmBytes);
424
+ return WebAssembly.Module.exports(module).map((entry) => entry.name).sort();
425
+ }
426
+
427
+ export async function getWasmExportNamesFromFile(wasmPath) {
428
+ const wasmBytes = await readFile(wasmPath);
429
+ return getWasmExportNames(wasmBytes);
430
+ }
431
+
432
+ export async function validatePluginArtifact(options) {
433
+ const {
434
+ manifest,
435
+ manifestPath = null,
436
+ wasmPath = null,
437
+ exportNames = null,
438
+ sourceName = manifestPath ?? "manifest",
439
+ } = options;
440
+ const report = validatePluginManifest(manifest, { sourceName });
441
+ const issues = [...report.issues];
442
+ let resolvedExportNames = [];
443
+ let checkedArtifact = false;
444
+
445
+ if (Array.isArray(exportNames)) {
446
+ resolvedExportNames = [...exportNames];
447
+ checkedArtifact = true;
448
+ } else if (isNonEmptyString(wasmPath)) {
449
+ resolvedExportNames = await getWasmExportNamesFromFile(wasmPath);
450
+ checkedArtifact = true;
451
+ }
452
+
453
+ if (checkedArtifact) {
454
+ for (const symbol of [
455
+ DefaultManifestExports.pluginBytesSymbol,
456
+ DefaultManifestExports.pluginSizeSymbol,
457
+ ]) {
458
+ if (!resolvedExportNames.includes(symbol)) {
459
+ pushIssue(
460
+ issues,
461
+ "error",
462
+ "missing-plugin-manifest-export",
463
+ `Plugin artifact is missing required export "${symbol}".`,
464
+ wasmPath ?? sourceName,
465
+ );
466
+ }
467
+ }
468
+ } else {
469
+ pushIssue(
470
+ issues,
471
+ "warning",
472
+ "artifact-abi-not-checked",
473
+ "No WASM artifact or export list was provided, so ABI export checks were skipped.",
474
+ sourceName,
475
+ );
476
+ }
477
+
478
+ return buildComplianceReport({
479
+ sourceName,
480
+ manifest,
481
+ issues,
482
+ exportNames: resolvedExportNames,
483
+ checkedArtifact,
484
+ });
485
+ }
486
+
487
+ function buildComplianceReport({
488
+ sourceName,
489
+ manifest,
490
+ issues,
491
+ exportNames,
492
+ checkedArtifact,
493
+ }) {
494
+ const errors = issues.filter((issue) => issue.severity === "error");
495
+ const warnings = issues.filter((issue) => issue.severity === "warning");
496
+ return {
497
+ ok: errors.length === 0,
498
+ sourceName,
499
+ manifest,
500
+ issues,
501
+ errors,
502
+ warnings,
503
+ checkedArtifact,
504
+ exportNames,
505
+ };
506
+ }
507
+
508
+ export async function findManifestFiles(rootDirectory) {
509
+ const manifestPaths = [];
510
+ await walkDirectory(rootDirectory, manifestPaths);
511
+ manifestPaths.sort();
512
+ return manifestPaths;
513
+ }
514
+
515
+ export async function loadComplianceConfig(rootDirectory) {
516
+ for (const candidate of [
517
+ path.join(rootDirectory, "sdn-plugin-compliance.json"),
518
+ path.join(rootDirectory, ".claude", "sdn-plugin-compliance.json"),
519
+ ]) {
520
+ try {
521
+ await access(candidate);
522
+ return {
523
+ path: candidate,
524
+ config: JSON.parse(await readFile(candidate, "utf8")),
525
+ };
526
+ } catch (error) {
527
+ if (error && error.code !== "ENOENT") {
528
+ throw error;
529
+ }
530
+ }
531
+ }
532
+ return null;
533
+ }
534
+
535
+ export async function resolveManifestFiles(rootDirectory) {
536
+ const loadedConfig = await loadComplianceConfig(rootDirectory);
537
+ if (!loadedConfig) {
538
+ return findManifestFiles(rootDirectory);
539
+ }
540
+
541
+ const { config } = loadedConfig;
542
+ const resolvedPaths = new Set();
543
+ if (Array.isArray(config.manifestPaths)) {
544
+ for (const relativePath of config.manifestPaths) {
545
+ resolvedPaths.add(path.resolve(rootDirectory, relativePath));
546
+ }
547
+ }
548
+ if (Array.isArray(config.scanDirectories)) {
549
+ for (const relativeDirectory of config.scanDirectories) {
550
+ const scanRoot = path.resolve(rootDirectory, relativeDirectory);
551
+ const discoveredPaths = await findManifestFiles(scanRoot);
552
+ for (const discoveredPath of discoveredPaths) {
553
+ resolvedPaths.add(discoveredPath);
554
+ }
555
+ }
556
+ }
557
+ return [...resolvedPaths].sort();
558
+ }
559
+
560
+ async function walkDirectory(currentDirectory, manifestPaths) {
561
+ const entries = await readdir(currentDirectory, { withFileTypes: true });
562
+ for (const entry of entries) {
563
+ const resolvedPath = path.join(currentDirectory, entry.name);
564
+ if (entry.isDirectory()) {
565
+ if (!IgnoredDirectoryNames.has(entry.name)) {
566
+ await walkDirectory(resolvedPath, manifestPaths);
567
+ }
568
+ continue;
569
+ }
570
+ if (entry.isFile() && entry.name === "manifest.json") {
571
+ manifestPaths.push(resolvedPath);
572
+ }
573
+ }
574
+ }
@@ -0,0 +1,124 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { encodePluginManifest } from "./manifest/index.js";
5
+
6
+ function toManifestBytes(manifest) {
7
+ if (manifest instanceof Uint8Array) {
8
+ return manifest;
9
+ }
10
+ if (manifest instanceof ArrayBuffer) {
11
+ return new Uint8Array(manifest);
12
+ }
13
+ if (ArrayBuffer.isView(manifest)) {
14
+ return new Uint8Array(
15
+ manifest.buffer,
16
+ manifest.byteOffset,
17
+ manifest.byteLength,
18
+ );
19
+ }
20
+ return encodePluginManifest(manifest);
21
+ }
22
+
23
+ function renderByteRows(bytes) {
24
+ const rows = [];
25
+ for (let index = 0; index < bytes.length; index += 12) {
26
+ const slice = bytes.subarray(index, index + 12);
27
+ rows.push(
28
+ ` ${Array.from(slice, (byte) => `0x${byte
29
+ .toString(16)
30
+ .padStart(2, "0")}`).join(", ")}`,
31
+ );
32
+ }
33
+ return rows.join(",\n");
34
+ }
35
+
36
+ export function generateEmbeddedManifestSource(options = {}) {
37
+ const manifestBytes = toManifestBytes(options.manifest);
38
+ if (manifestBytes.length === 0) {
39
+ throw new Error("generateEmbeddedManifestSource requires manifest bytes.");
40
+ }
41
+
42
+ const bytesSymbol = options.bytesSymbol ?? "plugin_get_manifest_flatbuffer";
43
+ const sizeSymbol =
44
+ options.sizeSymbol ?? "plugin_get_manifest_flatbuffer_size";
45
+ const bufferSymbol = options.bufferSymbol ?? "g_module_manifest";
46
+
47
+ return `#include <stddef.h>
48
+ #include <stdint.h>
49
+
50
+ #ifdef __EMSCRIPTEN__
51
+ #include <emscripten.h>
52
+ #define MODULE_MANIFEST_EXPORT EMSCRIPTEN_KEEPALIVE
53
+ #else
54
+ #define MODULE_MANIFEST_EXPORT __attribute__((visibility("default")))
55
+ #endif
56
+
57
+ static const uint8_t ${bufferSymbol}[] = {
58
+ ${renderByteRows(manifestBytes)}
59
+ };
60
+
61
+ #ifdef __cplusplus
62
+ extern "C" {
63
+ #endif
64
+
65
+ MODULE_MANIFEST_EXPORT const uint8_t* ${bytesSymbol}(void) {
66
+ return ${bufferSymbol};
67
+ }
68
+
69
+ MODULE_MANIFEST_EXPORT uint32_t ${sizeSymbol}(void) {
70
+ return (uint32_t)sizeof(${bufferSymbol});
71
+ }
72
+
73
+ #ifdef __cplusplus
74
+ }
75
+ #endif
76
+ `;
77
+ }
78
+
79
+ export function writeEmbeddedManifestArtifacts(options = {}) {
80
+ const {
81
+ manifest,
82
+ outputDir,
83
+ sourceFileName = "plugin-manifest-exports.c",
84
+ binaryFileName = "plugin-manifest.fb",
85
+ moduleFileName = "plugin-manifest.js",
86
+ bytesSymbol = "plugin_get_manifest_flatbuffer",
87
+ sizeSymbol = "plugin_get_manifest_flatbuffer_size",
88
+ } = options;
89
+
90
+ if (!outputDir) {
91
+ throw new Error("writeEmbeddedManifestArtifacts requires outputDir.");
92
+ }
93
+
94
+ const manifestBytes = toManifestBytes(manifest);
95
+ fs.mkdirSync(outputDir, { recursive: true });
96
+
97
+ const binaryPath = path.join(outputDir, binaryFileName);
98
+ fs.writeFileSync(binaryPath, manifestBytes);
99
+
100
+ const source = generateEmbeddedManifestSource({
101
+ manifest: manifestBytes,
102
+ bytesSymbol,
103
+ sizeSymbol,
104
+ });
105
+ const sourcePath = path.join(outputDir, sourceFileName);
106
+ fs.writeFileSync(sourcePath, source);
107
+
108
+ const moduleSource = `export const manifestBase64 = "${Buffer.from(
109
+ manifestBytes,
110
+ ).toString("base64")}";\n`;
111
+ const modulePath = path.join(outputDir, moduleFileName);
112
+ fs.writeFileSync(modulePath, moduleSource);
113
+
114
+ return {
115
+ manifestBytes,
116
+ binaryPath,
117
+ source,
118
+ sourcePath,
119
+ modulePath,
120
+ bytesSymbol,
121
+ sizeSymbol,
122
+ };
123
+ }
124
+