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.
- package/LICENSE +190 -0
- package/README.md +174 -83
- package/bin/space-data-module.js +24 -0
- package/package.json +8 -3
- package/schemas/ModuleBundle.fbs +108 -0
- package/schemas/PluginManifest.fbs +26 -1
- package/src/bundle/codec.js +244 -0
- package/src/bundle/constants.js +8 -0
- package/src/bundle/index.js +3 -0
- package/src/bundle/wasm.js +447 -0
- package/src/compiler/compileModule.js +189 -41
- package/src/compliance/pluginCompliance.js +334 -0
- package/src/generated/orbpro/manifest/capability-kind.d.ts +27 -2
- package/src/generated/orbpro/manifest/capability-kind.js +26 -1
- package/src/generated/orbpro/manifest/capability-kind.ts +25 -0
- package/src/generated/orbpro/module/canonicalization-rule.d.ts +48 -0
- package/src/generated/orbpro/module/canonicalization-rule.js +95 -0
- package/src/generated/orbpro/module/canonicalization-rule.ts +142 -0
- package/src/generated/orbpro/module/module-bundle-entry-role.d.ts +11 -0
- package/src/generated/orbpro/module/module-bundle-entry-role.js +14 -0
- package/src/generated/orbpro/module/module-bundle-entry-role.ts +15 -0
- package/src/generated/orbpro/module/module-bundle-entry.d.ts +97 -0
- package/src/generated/orbpro/module/module-bundle-entry.js +219 -0
- package/src/generated/orbpro/module/module-bundle-entry.ts +287 -0
- package/src/generated/orbpro/module/module-bundle.d.ts +86 -0
- package/src/generated/orbpro/module/module-bundle.js +213 -0
- package/src/generated/orbpro/module/module-bundle.ts +277 -0
- package/src/generated/orbpro/module/module-payload-encoding.d.ts +9 -0
- package/src/generated/orbpro/module/module-payload-encoding.js +12 -0
- package/src/generated/orbpro/module/module-payload-encoding.ts +13 -0
- package/src/generated/orbpro/module.d.ts +5 -0
- package/src/generated/orbpro/module.js +7 -0
- package/src/generated/orbpro/module.ts +9 -0
- package/src/host/abi.js +282 -0
- package/src/host/cron.js +247 -0
- package/src/host/index.js +3 -0
- package/src/host/nodeHost.js +2165 -0
- package/src/index.d.ts +880 -0
- package/src/index.js +9 -2
- package/src/manifest/normalize.js +32 -1
- package/src/runtime/constants.js +18 -1
- package/src/transport/pki.js +0 -5
- package/src/utils/encoding.js +9 -1
- 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
|
|
72
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
const report =
|
|
119
|
-
|
|
120
|
-
|
|
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:
|
|
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
|
|