space-data-module-sdk 0.2.5 → 0.2.7

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.
@@ -0,0 +1,414 @@
1
+ import { encodePluginInvokeRequest } from "../invoke/codec.js";
2
+ import { normalizeInvokeSurfaces } from "../invoke/index.js";
3
+ import { selectPreferredPayloadTypeRef } from "../manifest/typeRefs.js";
4
+
5
+ const CapabilitySurfaceMatrix = Object.freeze({
6
+ logging: Object.freeze({
7
+ capability: "logging",
8
+ wasi: true,
9
+ syncHostcall: false,
10
+ nodeHostApi: true,
11
+ notes: [
12
+ "Portable guests can emit diagnostics through stdout/stderr.",
13
+ "The current sync hostcall ABI does not expose a structured logging API.",
14
+ ],
15
+ }),
16
+ clock: Object.freeze({
17
+ capability: "clock",
18
+ wasi: true,
19
+ syncHostcall: true,
20
+ nodeHostApi: true,
21
+ notes: [
22
+ "WASI runtimes can expose clock/time directly to standalone guests.",
23
+ "The SDK sync hostcall bridge also exposes clock.now/clock.nowIso/clock.monotonicNow.",
24
+ ],
25
+ }),
26
+ random: Object.freeze({
27
+ capability: "random",
28
+ wasi: true,
29
+ syncHostcall: false,
30
+ nodeHostApi: true,
31
+ notes: [
32
+ "WASI random_get is available to standalone guests.",
33
+ "The current sync hostcall ABI does not expose random.bytes.",
34
+ ],
35
+ }),
36
+ timers: Object.freeze({
37
+ capability: "timers",
38
+ wasi: false,
39
+ syncHostcall: false,
40
+ nodeHostApi: true,
41
+ notes: [
42
+ "Timers are async host services today.",
43
+ "They are not reachable through the current sync JSON hostcall ABI.",
44
+ ],
45
+ }),
46
+ schedule_cron: Object.freeze({
47
+ capability: "schedule_cron",
48
+ wasi: false,
49
+ syncHostcall: true,
50
+ nodeHostApi: true,
51
+ notes: [
52
+ "Schedule parsing/matching is available through sync hostcalls.",
53
+ ],
54
+ }),
55
+ filesystem: Object.freeze({
56
+ capability: "filesystem",
57
+ wasi: true,
58
+ syncHostcall: true,
59
+ nodeHostApi: true,
60
+ notes: [
61
+ "WASI preopens are the preferred cross-runtime filesystem surface.",
62
+ "The sync hostcall ABI currently exposes filesystem.resolvePath only.",
63
+ ],
64
+ }),
65
+ pipe: Object.freeze({
66
+ capability: "pipe",
67
+ wasi: true,
68
+ syncHostcall: false,
69
+ nodeHostApi: false,
70
+ notes: [
71
+ "Portable WASI guests can rely on stdio descriptors as the current pipe surface.",
72
+ "Named or ad hoc pipe services are not exposed through the current host APIs.",
73
+ ],
74
+ }),
75
+ network: Object.freeze({
76
+ capability: "network",
77
+ wasi: false,
78
+ syncHostcall: false,
79
+ nodeHostApi: true,
80
+ notes: [
81
+ "The coarse network capability maps to host-side HTTP/TCP/UDP/TLS/WebSocket services today.",
82
+ "Pure WASI guests cannot reach that surface through the current sync hostcall ABI.",
83
+ ],
84
+ }),
85
+ http: Object.freeze({
86
+ capability: "http",
87
+ wasi: false,
88
+ syncHostcall: false,
89
+ nodeHostApi: true,
90
+ notes: [
91
+ "HTTP is currently available from the Node host API only.",
92
+ "Pure WASM guests cannot reach it through the current sync hostcall ABI.",
93
+ ],
94
+ }),
95
+ websocket: Object.freeze({
96
+ capability: "websocket",
97
+ wasi: false,
98
+ syncHostcall: false,
99
+ nodeHostApi: true,
100
+ notes: [
101
+ "WebSocket exchange is async and host-only today.",
102
+ ],
103
+ }),
104
+ mqtt: Object.freeze({
105
+ capability: "mqtt",
106
+ wasi: false,
107
+ syncHostcall: false,
108
+ nodeHostApi: true,
109
+ notes: [
110
+ "MQTT publish/subscribe is async and host-only today.",
111
+ ],
112
+ }),
113
+ tcp: Object.freeze({
114
+ capability: "tcp",
115
+ wasi: false,
116
+ syncHostcall: false,
117
+ nodeHostApi: true,
118
+ notes: [
119
+ "TCP request support exists in the Node host API only today.",
120
+ ],
121
+ }),
122
+ udp: Object.freeze({
123
+ capability: "udp",
124
+ wasi: false,
125
+ syncHostcall: false,
126
+ nodeHostApi: true,
127
+ notes: [
128
+ "UDP request support exists in the Node host API only today.",
129
+ ],
130
+ }),
131
+ tls: Object.freeze({
132
+ capability: "tls",
133
+ wasi: false,
134
+ syncHostcall: false,
135
+ nodeHostApi: true,
136
+ notes: [
137
+ "TLS request support exists in the Node host API only today.",
138
+ ],
139
+ }),
140
+ context_read: Object.freeze({
141
+ capability: "context_read",
142
+ wasi: false,
143
+ syncHostcall: false,
144
+ nodeHostApi: true,
145
+ notes: [
146
+ "Context storage is not exposed through the sync hostcall ABI today.",
147
+ ],
148
+ }),
149
+ context_write: Object.freeze({
150
+ capability: "context_write",
151
+ wasi: false,
152
+ syncHostcall: false,
153
+ nodeHostApi: true,
154
+ notes: [
155
+ "Context storage is not exposed through the sync hostcall ABI today.",
156
+ ],
157
+ }),
158
+ crypto_hash: Object.freeze({
159
+ capability: "crypto_hash",
160
+ wasi: false,
161
+ syncHostcall: false,
162
+ nodeHostApi: true,
163
+ notes: [
164
+ "Hashing exists in the Node host API, but not the sync hostcall ABI.",
165
+ ],
166
+ }),
167
+ crypto_sign: Object.freeze({
168
+ capability: "crypto_sign",
169
+ wasi: false,
170
+ syncHostcall: false,
171
+ nodeHostApi: true,
172
+ notes: [
173
+ "Signing exists in the Node host API, but not the sync hostcall ABI.",
174
+ ],
175
+ }),
176
+ crypto_verify: Object.freeze({
177
+ capability: "crypto_verify",
178
+ wasi: false,
179
+ syncHostcall: false,
180
+ nodeHostApi: true,
181
+ notes: [
182
+ "Verification exists in the Node host API, but not the sync hostcall ABI.",
183
+ ],
184
+ }),
185
+ crypto_encrypt: Object.freeze({
186
+ capability: "crypto_encrypt",
187
+ wasi: false,
188
+ syncHostcall: false,
189
+ nodeHostApi: true,
190
+ notes: [
191
+ "Encryption exists in the Node host API, but not the sync hostcall ABI.",
192
+ ],
193
+ }),
194
+ crypto_decrypt: Object.freeze({
195
+ capability: "crypto_decrypt",
196
+ wasi: false,
197
+ syncHostcall: false,
198
+ nodeHostApi: true,
199
+ notes: [
200
+ "Decryption exists in the Node host API, but not the sync hostcall ABI.",
201
+ ],
202
+ }),
203
+ crypto_key_agreement: Object.freeze({
204
+ capability: "crypto_key_agreement",
205
+ wasi: false,
206
+ syncHostcall: false,
207
+ nodeHostApi: true,
208
+ notes: [
209
+ "Key agreement exists in the Node host API, but not the sync hostcall ABI.",
210
+ ],
211
+ }),
212
+ crypto_kdf: Object.freeze({
213
+ capability: "crypto_kdf",
214
+ wasi: false,
215
+ syncHostcall: false,
216
+ nodeHostApi: true,
217
+ notes: [
218
+ "KDF support exists in the Node host API, but not the sync hostcall ABI.",
219
+ ],
220
+ }),
221
+ process_exec: Object.freeze({
222
+ capability: "process_exec",
223
+ wasi: false,
224
+ syncHostcall: false,
225
+ nodeHostApi: true,
226
+ notes: [
227
+ "Process execution is host-only today.",
228
+ ],
229
+ }),
230
+ });
231
+
232
+ function findDefaultTypeRef(port = {}, options = {}) {
233
+ return selectPreferredPayloadTypeRef(port, {
234
+ preferredWireFormat: options.preferredWireFormat,
235
+ });
236
+ }
237
+
238
+ function normalizePayloadBytes(value) {
239
+ if (value instanceof Uint8Array) {
240
+ return new Uint8Array(value);
241
+ }
242
+ if (value instanceof ArrayBuffer) {
243
+ return new Uint8Array(value);
244
+ }
245
+ if (ArrayBuffer.isView(value)) {
246
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
247
+ }
248
+ if (typeof value === "string") {
249
+ return new TextEncoder().encode(value);
250
+ }
251
+ return new Uint8Array();
252
+ }
253
+
254
+ function buildDefaultInputs(method = {}, options = {}) {
255
+ const inputs = [];
256
+ for (const port of Array.isArray(method.inputPorts) ? method.inputPorts : []) {
257
+ const required = port.required !== false;
258
+ if (!required && options.includeOptionalInputs !== true) {
259
+ continue;
260
+ }
261
+ const typeRef = findDefaultTypeRef(port, options);
262
+ const payload = options.payloadForPort
263
+ ? options.payloadForPort({
264
+ methodId: method.methodId ?? null,
265
+ portId: port.portId ?? null,
266
+ port,
267
+ required,
268
+ typeRef,
269
+ })
270
+ : null;
271
+ inputs.push({
272
+ portId: port.portId ?? null,
273
+ typeRef,
274
+ payload: normalizePayloadBytes(payload),
275
+ });
276
+ }
277
+ return inputs;
278
+ }
279
+
280
+ function buildMethodCase(method = {}, surface, options = {}) {
281
+ const methodId = String(method.methodId ?? "").trim();
282
+ return {
283
+ id: `${surface}:${methodId}`,
284
+ kind: "invoke",
285
+ surface,
286
+ methodId,
287
+ displayName: method.displayName ?? methodId,
288
+ inputs: buildDefaultInputs(method, options),
289
+ requiredPortIds: (Array.isArray(method.inputPorts) ? method.inputPorts : [])
290
+ .filter((port) => port.required !== false)
291
+ .map((port) => port.portId ?? null)
292
+ .filter(Boolean),
293
+ expectedStatusCode:
294
+ Number.isInteger(options.expectedStatusCode) ? options.expectedStatusCode : 0,
295
+ notes: [
296
+ `Generated smoke case for ${surface} surface.`,
297
+ "Semantic assertions still require scenario-specific validators.",
298
+ ],
299
+ };
300
+ }
301
+
302
+ function encodeValue(value) {
303
+ if (value instanceof Uint8Array) {
304
+ return {
305
+ type: "bytes",
306
+ base64: Buffer.from(value).toString("base64"),
307
+ };
308
+ }
309
+ if (Array.isArray(value)) {
310
+ return value.map((entry) => encodeValue(entry));
311
+ }
312
+ if (value && typeof value === "object") {
313
+ return Object.fromEntries(
314
+ Object.entries(value).map(([key, entry]) => [key, encodeValue(entry)]),
315
+ );
316
+ }
317
+ return value;
318
+ }
319
+
320
+ export function describeCapabilityRuntimeSurface(capability) {
321
+ const normalized = String(capability ?? "").trim();
322
+ const known = CapabilitySurfaceMatrix[normalized];
323
+ if (known) {
324
+ return {
325
+ capability: known.capability,
326
+ wasi: known.wasi,
327
+ syncHostcall: known.syncHostcall,
328
+ nodeHostApi: known.nodeHostApi,
329
+ notes: [...known.notes],
330
+ };
331
+ }
332
+ return {
333
+ capability: normalized,
334
+ wasi: false,
335
+ syncHostcall: false,
336
+ nodeHostApi: false,
337
+ notes: [
338
+ "Capability is unknown to the SDK testing matrix.",
339
+ ],
340
+ };
341
+ }
342
+
343
+ export function generateManifestHarnessPlan(options = {}) {
344
+ const manifest = options.manifest ?? {};
345
+ const methods = Array.isArray(manifest.methods) ? manifest.methods : [];
346
+ const invokeSurfaces = normalizeInvokeSurfaces(manifest.invokeSurfaces ?? ["direct"]);
347
+ const generatedCases = [];
348
+ for (const method of methods) {
349
+ if (!method?.methodId) {
350
+ continue;
351
+ }
352
+ for (const surface of invokeSurfaces) {
353
+ generatedCases.push(buildMethodCase(method, surface, options));
354
+ }
355
+ }
356
+
357
+ const customCases = Array.isArray(options.scenarios) ? options.scenarios : [];
358
+ return {
359
+ moduleKind:
360
+ String(manifest.pluginFamily ?? "").trim().toLowerCase() === "flow"
361
+ ? "flow"
362
+ : "module",
363
+ pluginId: manifest.pluginId ?? null,
364
+ name: manifest.name ?? null,
365
+ version: manifest.version ?? null,
366
+ invokeSurfaces,
367
+ methods: methods.map((method) => ({
368
+ methodId: method.methodId ?? null,
369
+ displayName: method.displayName ?? null,
370
+ inputPorts: Array.isArray(method.inputPorts) ? method.inputPorts.length : 0,
371
+ outputPorts: Array.isArray(method.outputPorts) ? method.outputPorts.length : 0,
372
+ })),
373
+ capabilities: (Array.isArray(manifest.capabilities) ? manifest.capabilities : []).map(
374
+ (capability) => describeCapabilityRuntimeSurface(capability),
375
+ ),
376
+ generatedCases,
377
+ scenarios: [...generatedCases, ...customCases],
378
+ };
379
+ }
380
+
381
+ export function materializeHarnessScenario(scenario = {}) {
382
+ if (scenario?.kind !== "invoke") {
383
+ return {
384
+ ...scenario,
385
+ stdinBytes: normalizePayloadBytes(scenario.stdinBytes ?? scenario.stdin ?? null),
386
+ };
387
+ }
388
+
389
+ const requestBytes = encodePluginInvokeRequest({
390
+ methodId: scenario.methodId,
391
+ inputs: (Array.isArray(scenario.inputs) ? scenario.inputs : []).map((input) => ({
392
+ portId: input.portId ?? null,
393
+ typeRef: input.typeRef ?? null,
394
+ payload: normalizePayloadBytes(input.payload),
395
+ })),
396
+ });
397
+
398
+ if (scenario.surface === "command") {
399
+ return {
400
+ ...scenario,
401
+ stdinBytes: requestBytes,
402
+ requestBytes,
403
+ };
404
+ }
405
+
406
+ return {
407
+ ...scenario,
408
+ requestBytes,
409
+ };
410
+ }
411
+
412
+ export function serializeHarnessPlan(plan = {}) {
413
+ return encodeValue(plan);
414
+ }