sdn-flow 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude/SKILLS.md +7 -0
  2. package/.claude/skills/sdn-plugin-abi-compliance/SKILL.md +56 -0
  3. package/.claude/todo/001-js-host-startup-and-deno.md +85 -0
  4. package/LICENSE +21 -0
  5. package/README.md +223 -0
  6. package/bin/sdn-flow-host.js +169 -0
  7. package/docs/.nojekyll +0 -0
  8. package/docs/ARCHITECTURE.md +200 -0
  9. package/docs/HOST_CAPABILITY_MODEL.md +317 -0
  10. package/docs/PLUGIN_ARCHITECTURE.md +145 -0
  11. package/docs/PLUGIN_COMPATIBILITY.md +61 -0
  12. package/docs/PLUGIN_COMPLIANCE_CHECKS.md +82 -0
  13. package/docs/PLUGIN_MANIFEST.md +94 -0
  14. package/docs/css/style.css +465 -0
  15. package/docs/index.html +218 -0
  16. package/docs/js/app.mjs +751 -0
  17. package/docs/js/editor-panel.mjs +203 -0
  18. package/docs/js/flow-canvas.mjs +515 -0
  19. package/docs/js/flow-model.mjs +391 -0
  20. package/docs/js/workers/emception.worker.js +146 -0
  21. package/docs/js/workers/pyodide.worker.js +134 -0
  22. package/native/flow_source_generator.cpp +1958 -0
  23. package/package.json +67 -0
  24. package/schemas/FlowRuntimeAbi.fbs +91 -0
  25. package/src/auth/canonicalize.js +5 -0
  26. package/src/auth/index.js +11 -0
  27. package/src/auth/permissions.js +8 -0
  28. package/src/compiler/CppFlowSourceGenerator.js +475 -0
  29. package/src/compiler/EmceptionCompilerAdapter.js +244 -0
  30. package/src/compiler/SignedArtifactCatalog.js +152 -0
  31. package/src/compiler/index.js +8 -0
  32. package/src/compiler/nativeFlowSourceGeneratorTool.js +144 -0
  33. package/src/compliance/index.js +13 -0
  34. package/src/compliance/pluginCompliance.js +11 -0
  35. package/src/deploy/FlowDeploymentClient.js +532 -0
  36. package/src/deploy/index.js +8 -0
  37. package/src/designer/FlowDesignerSession.js +158 -0
  38. package/src/designer/index.js +2 -0
  39. package/src/designer/requirements.js +184 -0
  40. package/src/generated/runtimeAbiLayouts.js +544 -0
  41. package/src/host/appHost.js +105 -0
  42. package/src/host/autoHost.js +113 -0
  43. package/src/host/browserHostAdapters.js +108 -0
  44. package/src/host/compiledFlowRuntimeHost.js +703 -0
  45. package/src/host/constants.js +55 -0
  46. package/src/host/dependencyRuntime.js +227 -0
  47. package/src/host/descriptorAbi.js +351 -0
  48. package/src/host/fetchService.js +237 -0
  49. package/src/host/httpHostAdapters.js +280 -0
  50. package/src/host/index.js +91 -0
  51. package/src/host/installedFlowHost.js +885 -0
  52. package/src/host/invocationAbi.js +440 -0
  53. package/src/host/normalize.js +372 -0
  54. package/src/host/packageManagers.js +369 -0
  55. package/src/host/profile.js +134 -0
  56. package/src/host/runtimeAbi.js +106 -0
  57. package/src/host/workspace.js +895 -0
  58. package/src/index.js +8 -0
  59. package/src/runtime/FlowRuntime.js +273 -0
  60. package/src/runtime/MethodRegistry.js +295 -0
  61. package/src/runtime/constants.js +44 -0
  62. package/src/runtime/index.js +19 -0
  63. package/src/runtime/normalize.js +377 -0
  64. package/src/transport/index.js +7 -0
  65. package/src/transport/pki.js +7 -0
  66. package/src/utils/crypto.js +7 -0
  67. package/src/utils/encoding.js +65 -0
  68. package/src/utils/wasmCrypto.js +69 -0
  69. package/tools/run-plugin-compliance-check.mjs +153 -0
@@ -0,0 +1,751 @@
1
+ /**
2
+ * app.mjs — Main application orchestrator for the sdn-flow IDE.
3
+ *
4
+ * Binds the flow model, canvas, Monaco editor, compilation workers,
5
+ * toolbar actions (import/export/CRC/compile/deploy), and VS Code
6
+ * webview communication.
7
+ */
8
+
9
+ import { FlowModel, crc32Hex, canonicalBytes } from "./flow-model.mjs";
10
+ import { FlowCanvas } from "./flow-canvas.mjs";
11
+ import { EditorPanel } from "./editor-panel.mjs";
12
+
13
+ // ── VS Code webview API (if running inside VS Code) ──
14
+ const vscode = (typeof acquireVsCodeApi === "function") ? acquireVsCodeApi() : null;
15
+
16
+ // ── Worker RPC helper ──
17
+ function workerRPC(worker) {
18
+ let idCounter = 0;
19
+ const pending = new Map();
20
+ worker.onmessage = ({ data }) => {
21
+ if (data.log !== undefined) {
22
+ termLog(data.log, data.level || "info");
23
+ return;
24
+ }
25
+ const { id, result, error } = data;
26
+ const p = pending.get(id);
27
+ if (p) {
28
+ pending.delete(id);
29
+ if (error) p.reject(new Error(error));
30
+ else p.resolve(result);
31
+ }
32
+ };
33
+ return (method, args) => new Promise((resolve, reject) => {
34
+ const id = ++idCounter;
35
+ pending.set(id, { resolve, reject });
36
+ worker.postMessage({ id, method, args });
37
+ });
38
+ }
39
+
40
+ // ── Terminal ──
41
+ const termEl = document.getElementById("terminal");
42
+
43
+ function termLog(text, level = "info") {
44
+ const span = document.createElement("span");
45
+ span.className = `term-${level}`;
46
+ span.textContent = text + "\n";
47
+ termEl.appendChild(span);
48
+ termEl.scrollTop = termEl.scrollHeight;
49
+ }
50
+
51
+ function termClear() { termEl.innerHTML = ""; }
52
+
53
+ // ── Status bar ──
54
+ function setStatus(msg, type = "") {
55
+ const bar = document.getElementById("statusbar");
56
+ document.getElementById("status-msg").textContent = msg;
57
+ bar.className = type; // "", "error", "success"
58
+ }
59
+
60
+ function updateCounts() {
61
+ document.getElementById("node-count").textContent = `${model.nodes.size} nodes`;
62
+ document.getElementById("edge-count").textContent = `${model.edges.size} wires`;
63
+ }
64
+
65
+ // ── Model + Canvas + Editor ──
66
+ const model = new FlowModel();
67
+ const svg = document.getElementById("flow-canvas");
68
+ const canvas = new FlowCanvas(svg, model);
69
+ const editorPanel = new EditorPanel("editor-container", "editor-lang");
70
+
71
+ // ── Properties Panel ──
72
+ const propsBody = document.getElementById("props-body");
73
+
74
+ function renderProps(nodeId) {
75
+ if (!nodeId) {
76
+ propsBody.innerHTML = '<div class="empty-state">Select a node to edit</div>';
77
+ return;
78
+ }
79
+ const node = model.nodes.get(nodeId);
80
+ if (!node) return;
81
+
82
+ propsBody.innerHTML = `
83
+ <div class="prop-group">
84
+ <label class="prop-label">Label</label>
85
+ <input class="prop-input" data-field="label" value="${esc(node.label)}">
86
+ </div>
87
+ <div class="prop-group">
88
+ <label class="prop-label">Kind</label>
89
+ <select class="prop-select" data-field="kind">
90
+ ${["trigger","transform","analyzer","publisher","responder","renderer","sink"]
91
+ .map(k => `<option value="${k}" ${k === node.kind ? "selected" : ""}>${k}</option>`).join("")}
92
+ </select>
93
+ </div>
94
+ <div class="prop-group">
95
+ <label class="prop-label">Plugin ID</label>
96
+ <input class="prop-input" data-field="pluginId" value="${esc(node.pluginId)}" placeholder="com.example.plugin">
97
+ </div>
98
+ <div class="prop-group">
99
+ <label class="prop-label">Method ID</label>
100
+ <input class="prop-input" data-field="methodId" value="${esc(node.methodId)}" placeholder="process">
101
+ </div>
102
+ <div class="prop-group">
103
+ <label class="prop-label">Drain Policy</label>
104
+ <select class="prop-select" data-field="drainPolicy">
105
+ ${["single-shot","drain-until-yield","drain-to-empty"]
106
+ .map(d => `<option value="${d}" ${d === node.drainPolicy ? "selected" : ""}>${d}</option>`).join("")}
107
+ </select>
108
+ </div>
109
+ <div class="prop-group">
110
+ <label class="prop-label">Language</label>
111
+ <select class="prop-select" data-field="lang">
112
+ <option value="">None</option>
113
+ ${["cpp","python","typescript","javascript","rust","go","c"]
114
+ .map(l => `<option value="${l}" ${l === node.lang ? "selected" : ""}>${l}</option>`).join("")}
115
+ </select>
116
+ </div>
117
+ <div class="prop-group">
118
+ <label class="prop-label">Input Ports</label>
119
+ <div class="port-list" data-dir="input">
120
+ ${(node.ports?.inputs || []).map(p => `
121
+ <div class="port-row">
122
+ <input class="prop-input port-name" value="${esc(p.id)}" data-port-id="${esc(p.id)}" placeholder="port id">
123
+ <button class="icon-btn remove-port" data-port-id="${esc(p.id)}">-</button>
124
+ </div>
125
+ `).join("")}
126
+ <button class="palette-btn add-port" data-dir="input">+ Add Input</button>
127
+ </div>
128
+ </div>
129
+ <div class="prop-group">
130
+ <label class="prop-label">Output Ports</label>
131
+ <div class="port-list" data-dir="output">
132
+ ${(node.ports?.outputs || []).map(p => `
133
+ <div class="port-row">
134
+ <input class="prop-input port-name" value="${esc(p.id)}" data-port-id="${esc(p.id)}" placeholder="port id">
135
+ <button class="icon-btn remove-port" data-port-id="${esc(p.id)}">-</button>
136
+ </div>
137
+ `).join("")}
138
+ <button class="palette-btn add-port" data-dir="output">+ Add Output</button>
139
+ </div>
140
+ </div>
141
+ `;
142
+
143
+ // Bind property changes
144
+ propsBody.querySelectorAll(".prop-input[data-field], .prop-select[data-field]").forEach(el => {
145
+ el.addEventListener("change", () => {
146
+ model.updateNode(nodeId, { [el.dataset.field]: el.value || null });
147
+ if (el.dataset.field === "lang") {
148
+ editorPanel.setNode(nodeId); // refresh editor language
149
+ }
150
+ });
151
+ });
152
+
153
+ // Add/remove port buttons
154
+ propsBody.querySelectorAll(".add-port").forEach(btn => {
155
+ btn.addEventListener("click", () => {
156
+ const dir = btn.dataset.dir;
157
+ const portId = prompt("Port ID:", `port-${Date.now().toString(36)}`);
158
+ if (portId) {
159
+ model.addPort(nodeId, dir, portId, portId);
160
+ renderProps(nodeId);
161
+ }
162
+ });
163
+ });
164
+ propsBody.querySelectorAll(".remove-port").forEach(btn => {
165
+ btn.addEventListener("click", () => {
166
+ const portId = btn.dataset.portId;
167
+ const dir = btn.closest(".port-list").dataset.dir;
168
+ model.removePort(nodeId, dir, portId);
169
+ renderProps(nodeId);
170
+ });
171
+ });
172
+ }
173
+
174
+ function esc(s) { return (s || "").replace(/"/g, "&quot;").replace(/</g, "&lt;"); }
175
+
176
+ // ── Wire canvas + editor + props ──
177
+ canvas.onNodeSelect((nodeId) => {
178
+ renderProps(nodeId);
179
+ updateGeneratedSource();
180
+ });
181
+
182
+ // Double-click node → load plugin metadata from manifest
183
+ canvas.onNodeDblClick(async (nodeId) => {
184
+ const node = model.nodes.get(nodeId);
185
+ if (!node) return;
186
+ if (!node.pluginId) {
187
+ termLog(`Node ${node.label}: no pluginId set, cannot load metadata.`, "warn");
188
+ return;
189
+ }
190
+ termLog(`Loading metadata for plugin: ${node.pluginId}...`, "info");
191
+
192
+ // Try fetching manifest from examples
193
+ const manifests = [
194
+ `../examples/plugins/${node.pluginId.split(".").pop()}/manifest.json`,
195
+ `../examples/plugins/${node.methodId}/manifest.json`,
196
+ ];
197
+ let found = false;
198
+ for (const url of manifests) {
199
+ try {
200
+ const resp = await fetch(url);
201
+ if (resp.ok) {
202
+ const manifest = await resp.json();
203
+ termLog(`Plugin manifest loaded: ${manifest.pluginId || node.pluginId}`, "success");
204
+ termLog(JSON.stringify(manifest, null, 2), "info");
205
+ // Update node with manifest data
206
+ if (manifest.methods) {
207
+ const methods = Array.isArray(manifest.methods) ? manifest.methods : Object.values(manifest.methods);
208
+ const method = methods.find(m => m.methodId === node.methodId) || methods[0];
209
+ if (method) {
210
+ const ports = { inputs: [], outputs: [] };
211
+ if (method.inputs) method.inputs.forEach(p => ports.inputs.push({ id: p.portId || p.id, label: p.label || p.portId || p.id }));
212
+ if (method.outputs) method.outputs.forEach(p => ports.outputs.push({ id: p.portId || p.id, label: p.label || p.portId || p.id }));
213
+ if (ports.inputs.length || ports.outputs.length) {
214
+ model.updateNode(nodeId, { ports });
215
+ termLog(` Updated ports from manifest`, "success");
216
+ }
217
+ }
218
+ }
219
+ found = true;
220
+ break;
221
+ }
222
+ } catch (e) { /* try next */ }
223
+ }
224
+ if (!found) {
225
+ // Show what we know in the terminal
226
+ termLog(` pluginId: ${node.pluginId}`, "info");
227
+ termLog(` methodId: ${node.methodId}`, "info");
228
+ termLog(` kind: ${node.kind}`, "info");
229
+ termLog(` drainPolicy: ${node.drainPolicy}`, "info");
230
+ termLog(` (manifest not found locally — set up artifact catalog for remote resolution)`, "warn");
231
+ }
232
+ renderProps(nodeId);
233
+ });
234
+
235
+ canvas.onEdgeSelect((edgeId) => {
236
+ renderProps(null);
237
+ if (edgeId) {
238
+ const edge = model.edges.get(edgeId);
239
+ if (edge) {
240
+ propsBody.innerHTML = `
241
+ <div class="prop-group"><label class="prop-label">Edge</label><span style="color:#ccc">${edge.edgeId}</span></div>
242
+ <div class="prop-group"><label class="prop-label">From</label><span style="color:#ccc">${edge.fromNodeId}:${edge.fromPortId}</span></div>
243
+ <div class="prop-group"><label class="prop-label">To</label><span style="color:#ccc">${edge.toNodeId}:${edge.toPortId}</span></div>
244
+ <div class="prop-group">
245
+ <label class="prop-label">Backpressure</label>
246
+ <select class="prop-select" id="edge-bp">
247
+ ${["drop","latest","queue","block-request","coalesce","drain-to-empty"]
248
+ .map(b => `<option value="${b}" ${b === edge.backpressurePolicy ? "selected" : ""}>${b}</option>`).join("")}
249
+ </select>
250
+ </div>
251
+ <div class="prop-group">
252
+ <label class="prop-label">Queue Depth</label>
253
+ <input class="prop-input" id="edge-qd" type="number" value="${edge.queueDepth}">
254
+ </div>
255
+ `;
256
+ document.getElementById("edge-bp")?.addEventListener("change", (e) => { edge.backpressurePolicy = e.target.value; });
257
+ document.getElementById("edge-qd")?.addEventListener("change", (e) => { edge.queueDepth = parseInt(e.target.value) || 32; });
258
+ }
259
+ }
260
+ });
261
+
262
+ model.addEventListener("change", () => {
263
+ updateCounts();
264
+ // Update CRC badge
265
+ const crc = model.computeCRC();
266
+ document.getElementById("crc-badge").textContent = crc;
267
+ updateGeneratedSource();
268
+ });
269
+
270
+ // ── Generated Source (read-only view of the compiled flow) ──
271
+
272
+ function updateGeneratedSource() {
273
+ const json = model.toJSON();
274
+ const nodes = json.nodes || [];
275
+ const edges = json.edges || [];
276
+ const triggers = json.triggers || [];
277
+
278
+ // Generate a C++ source preview of the flow runtime
279
+ const includes = [
280
+ '#include <cstdint>',
281
+ '#include <cstring>',
282
+ '#include "flatbuffers/flatbuffers.h"',
283
+ '#include "flow_manifest_generated.h"',
284
+ '',
285
+ '// ═══════════════════════════════════════════════════',
286
+ `// Generated flow: ${json.name || "Untitled"}`,
287
+ `// Program ID: ${json.programId || "(none)"}`,
288
+ `// Nodes: ${nodes.length} Edges: ${edges.length} Triggers: ${triggers.length}`,
289
+ `// CRC-32: ${model.computeCRC()}`,
290
+ '// ═══════════════════════════════════════════════════',
291
+ '',
292
+ ];
293
+
294
+ // Manifest embed
295
+ const manifest = [
296
+ '// ── Embedded FlatBuffer manifest ──',
297
+ 'static const uint8_t FLOW_MANIFEST[] = { /* built at compile time */ };',
298
+ 'static const uint32_t FLOW_MANIFEST_SIZE = sizeof(FLOW_MANIFEST);',
299
+ '',
300
+ 'extern "C" const uint8_t* flow_get_manifest_flatbuffer() { return FLOW_MANIFEST; }',
301
+ 'extern "C" uint32_t flow_get_manifest_flatbuffer_size() { return FLOW_MANIFEST_SIZE; }',
302
+ '',
303
+ ];
304
+
305
+ // Plugin artifact imports
306
+ const plugins = [...new Set(nodes.map(n => n.pluginId).filter(Boolean))];
307
+ const pluginDecls = plugins.length > 0 ? [
308
+ '// ── Plugin artifact imports ──',
309
+ ...plugins.map((p, i) => `static const uint8_t* PLUGIN_${i}_WASM = nullptr; // ${p}`),
310
+ ...plugins.map((p, i) => `static uint32_t PLUGIN_${i}_SIZE = 0;`),
311
+ '',
312
+ ] : [];
313
+
314
+ // Node declarations
315
+ const nodeDecls = [
316
+ '// ── Node declarations ──',
317
+ ...nodes.map(n => {
318
+ const kind = n.kind.toUpperCase();
319
+ return `// [${kind}] ${n.label} (${n.pluginId || "inline"} :: ${n.methodId || "process"})`;
320
+ }),
321
+ '',
322
+ ];
323
+
324
+ // Topology (edges)
325
+ const topo = edges.length > 0 ? [
326
+ '// ── Topology ──',
327
+ ...edges.map(e =>
328
+ `// ${e.fromNodeId}:${e.fromPortId} ──► ${e.toNodeId}:${e.toPortId} [${e.backpressurePolicy}, depth=${e.queueDepth}]`
329
+ ),
330
+ '',
331
+ ] : [];
332
+
333
+ // Trigger bindings
334
+ const trigBindings = (json.triggerBindings || []).length > 0 ? [
335
+ '// ── Trigger bindings ──',
336
+ ...(json.triggerBindings || []).map(b =>
337
+ `// ${b.triggerId} ──► ${b.targetNodeId}:${b.targetPortId} [${b.backpressurePolicy}]`
338
+ ),
339
+ '',
340
+ ] : [];
341
+
342
+ // Main entry
343
+ const main = [
344
+ '// ── Runtime entry ──',
345
+ 'extern "C" int flow_init() {',
346
+ ' // Initialize node state, wire topology, register triggers',
347
+ ...nodes.map(n => ` // init_node("${n.nodeId}", ${n.kind});`),
348
+ ...edges.map(e => ` // wire("${e.fromNodeId}:${e.fromPortId}", "${e.toNodeId}:${e.toPortId}");`),
349
+ ' return 0;',
350
+ '}',
351
+ '',
352
+ 'extern "C" int flow_step(const uint8_t* frame, uint32_t len) {',
353
+ ' // Dispatch frame through topology',
354
+ ' return 0;',
355
+ '}',
356
+ ];
357
+
358
+ const source = [...includes, ...manifest, ...pluginDecls, ...nodeDecls, ...topo, ...trigBindings, ...main].join('\n');
359
+
360
+ editorPanel.setGeneratedSource(source);
361
+ }
362
+
363
+ // ── Palette drag ──
364
+ document.querySelectorAll(".palette-item[draggable]").forEach(item => {
365
+ item.addEventListener("dragstart", (e) => {
366
+ e.dataTransfer.setData("text/x-sdn-kind", item.dataset.kind);
367
+ e.dataTransfer.setData("text/x-sdn-lang", item.dataset.lang || "");
368
+ e.dataTransfer.effectAllowed = "copy";
369
+ });
370
+ });
371
+
372
+ // ── Toolbar: Import ──
373
+ document.getElementById("btn-import").addEventListener("click", async () => {
374
+ if (vscode) {
375
+ // VS Code: request file from extension host
376
+ vscode.postMessage({ command: "importFlow" });
377
+ return;
378
+ }
379
+ const input = document.createElement("input");
380
+ input.type = "file";
381
+ input.accept = ".json";
382
+ input.onchange = async () => {
383
+ const file = input.files[0];
384
+ if (!file) return;
385
+ try {
386
+ const text = await file.text();
387
+ const json = JSON.parse(text);
388
+ model.fromJSON(json);
389
+ setStatus(`Loaded: ${json.name || file.name}`, "success");
390
+ termLog(`Imported flow: ${json.name || file.name}`, "success");
391
+ } catch (err) {
392
+ setStatus("Import failed", "error");
393
+ termLog(`Import error: ${err.message}`, "error");
394
+ }
395
+ };
396
+ input.click();
397
+ });
398
+
399
+ // ── Toolbar: Export ──
400
+ document.getElementById("btn-export").addEventListener("click", () => {
401
+ const json = model.toJSON();
402
+ const text = JSON.stringify(json, null, 2);
403
+
404
+ if (vscode) {
405
+ vscode.postMessage({ command: "exportFlow", data: text });
406
+ return;
407
+ }
408
+ const blob = new Blob([text], { type: "application/json" });
409
+ const url = URL.createObjectURL(blob);
410
+ const a = document.createElement("a");
411
+ a.href = url;
412
+ a.download = `${json.name || "flow"}.json`;
413
+ a.click();
414
+ URL.revokeObjectURL(url);
415
+ termLog(`Exported: ${a.download}`, "success");
416
+ });
417
+
418
+ // ── Toolbar: CRC ──
419
+ document.getElementById("btn-crc").addEventListener("click", async () => {
420
+ const crc = model.computeCRC();
421
+ const sha = await model.computeSHA256();
422
+ termLog(`CRC-32: ${crc}`, "info");
423
+ termLog(`SHA-256: ${sha}`, "info");
424
+ setStatus(`CRC: ${crc}`);
425
+ });
426
+
427
+ // ── Compilation Workers ──
428
+ let emceptionWorker = null;
429
+ let emceptionRPC = null;
430
+ let emceptionReady = false;
431
+
432
+ let pyodideWorker = null;
433
+ let pyodideRPC = null;
434
+ let pyodideReady = false;
435
+
436
+ // Resolve worker URLs relative to this module, not the page
437
+ const WORKER_BASE = new URL("./workers/", import.meta.url).href;
438
+
439
+ // Emception base URL — override via ?emception=<url> query param
440
+ const EMCEPTION_BASE = new URLSearchParams(location.search).get("emception") || "https://digitalarsenal.github.io/emception/";
441
+
442
+ async function ensureEmception() {
443
+ if (emceptionReady) return;
444
+ if (!emceptionWorker) {
445
+ emceptionWorker = new Worker(WORKER_BASE + "emception.worker.js");
446
+ emceptionRPC = workerRPC(emceptionWorker);
447
+ }
448
+ setStatus("Loading emception...");
449
+ await emceptionRPC("init", { baseUrl: EMCEPTION_BASE });
450
+ emceptionReady = true;
451
+ setStatus("Emception ready", "success");
452
+ }
453
+
454
+ async function ensurePyodide() {
455
+ if (pyodideReady) return;
456
+ if (!pyodideWorker) {
457
+ pyodideWorker = new Worker(WORKER_BASE + "pyodide.worker.js");
458
+ pyodideRPC = workerRPC(pyodideWorker);
459
+ }
460
+ setStatus("Loading Pyodide...");
461
+ await pyodideRPC("init");
462
+ pyodideReady = true;
463
+ setStatus("Pyodide ready", "success");
464
+ }
465
+
466
+ // ── Toolbar: Compile ──
467
+ document.getElementById("btn-compile").addEventListener("click", async () => {
468
+ const nodes = [...model.nodes.values()];
469
+ const cppNodes = nodes.filter(n => n.lang === "cpp" || n.lang === "c");
470
+ const pyNodes = nodes.filter(n => n.lang === "python");
471
+
472
+ if (cppNodes.length === 0 && pyNodes.length === 0) {
473
+ termLog("No compilable nodes (add C++ or Python modules).", "warn");
474
+ return;
475
+ }
476
+
477
+ termLog("=== Compile started ===", "info");
478
+ setStatus("Compiling...");
479
+
480
+ try {
481
+ // Compile C++ nodes via emception
482
+ if (cppNodes.length > 0) {
483
+ await ensureEmception();
484
+ for (const node of cppNodes) {
485
+ termLog(`Compiling ${node.label} (${node.lang})...`, "info");
486
+ const result = await emceptionRPC("compile", {
487
+ source: node.source,
488
+ lang: node.lang,
489
+ flags: ["-O2", "-std=c++20", "-sWASM=1"],
490
+ outputName: node.nodeId,
491
+ });
492
+ if (result.returncode === 0) {
493
+ termLog(` ${node.label}: OK`, "success");
494
+ // Set node status
495
+ const el = canvas.nodeElements.get(node.nodeId);
496
+ el?.querySelector(".node-status")?.classList.add("success");
497
+ } else {
498
+ termLog(` ${node.label}: FAILED (exit ${result.returncode})`, "error");
499
+ if (result.stderr) termLog(result.stderr, "error");
500
+ const el = canvas.nodeElements.get(node.nodeId);
501
+ el?.querySelector(".node-status")?.classList.add("error");
502
+ }
503
+ }
504
+ }
505
+
506
+ // Run Python nodes via Pyodide
507
+ if (pyNodes.length > 0) {
508
+ await ensurePyodide();
509
+ for (const node of pyNodes) {
510
+ termLog(`Running ${node.label} (Python)...`, "info");
511
+ const result = await pyodideRPC("run", { code: node.source });
512
+ termLog(` ${node.label}: OK`, "success");
513
+ if (result.stdout) termLog(result.stdout, "info");
514
+ const el = canvas.nodeElements.get(node.nodeId);
515
+ el?.querySelector(".node-status")?.classList.add("success");
516
+ }
517
+ }
518
+
519
+ // Compute final artifact hash
520
+ const sha = await model.computeSHA256();
521
+ termLog(`Graph SHA-256: ${sha}`, "info");
522
+ setStatus("Compile complete", "success");
523
+ termLog("=== Compile finished ===", "success");
524
+ } catch (err) {
525
+ termLog(`Compile error: ${err.message}`, "error");
526
+ setStatus("Compile failed", "error");
527
+ }
528
+ });
529
+
530
+ // ── Toolbar: Deploy ──
531
+ document.getElementById("btn-deploy").addEventListener("click", async () => {
532
+ termLog("=== Deploy pipeline started ===", "info");
533
+ setStatus("Deploying...");
534
+
535
+ try {
536
+ // Step 1: Compile (reuse compile logic)
537
+ document.getElementById("btn-compile").click();
538
+
539
+ // Step 2: Compute integrity
540
+ const crc = model.computeCRC();
541
+ const sha = await model.computeSHA256();
542
+ termLog(`Artifact CRC-32: ${crc}`, "info");
543
+ termLog(`Artifact SHA-256: ${sha}`, "info");
544
+
545
+ // Step 3: Sign with HD-wallet (if wallet is available)
546
+ const flowJson = model.toJSON();
547
+ const payload = canonicalBytes(flowJson);
548
+
549
+ termLog("Preparing deployment authorization...", "info");
550
+ termLog(` programId: ${flowJson.programId || "(unnamed)"}`, "info");
551
+ termLog(` graphHash: ${sha}`, "info");
552
+ termLog(` nodes: ${flowJson.nodes.length}, edges: ${flowJson.edges.length}`, "info");
553
+
554
+ // The actual signing would integrate with hd-wallet-wasm:
555
+ // const wallet = await initHDWallet();
556
+ // const master = wallet.hdkey.fromSeed(seed);
557
+ // const signingKey = getSigningKey(master, WellKnownCoinType.SDN);
558
+ // const digest = wallet.utils.sha256(payload);
559
+ // const signature = wallet.curves.secp256k1.sign(digest, signingKey.privateKey());
560
+ //
561
+ // For now, log the authorization shape:
562
+ termLog(" [wallet integration point: sign authorization envelope]", "warn");
563
+ termLog(" [wallet integration point: encrypt with recipient public key]", "warn");
564
+
565
+ // Step 4: Package
566
+ const deployment = {
567
+ version: 1,
568
+ encrypted: false,
569
+ payload: {
570
+ version: 1,
571
+ kind: "compiled-flow-wasm-deployment",
572
+ artifact: {
573
+ artifactId: `flow-${Date.now().toString(36)}`,
574
+ programId: flowJson.programId,
575
+ graphHash: sha,
576
+ manifestHash: null,
577
+ abiVersion: 1,
578
+ },
579
+ authorization: null, // would be signed envelope
580
+ target: null,
581
+ },
582
+ };
583
+
584
+ if (vscode) {
585
+ // In VS Code, send to extension host to save
586
+ vscode.postMessage({ command: "deploy", data: JSON.stringify(deployment, null, 2) });
587
+ termLog("Deployment package sent to VS Code host.", "success");
588
+ } else {
589
+ // In browser, offer download
590
+ const blob = new Blob([JSON.stringify(deployment, null, 2)], { type: "application/json" });
591
+ const url = URL.createObjectURL(blob);
592
+ const a = document.createElement("a");
593
+ a.href = url;
594
+ a.download = `deployment-${deployment.payload.artifact.artifactId}.json`;
595
+ a.click();
596
+ URL.revokeObjectURL(url);
597
+ termLog(`Deployment package downloaded: ${a.download}`, "success");
598
+ }
599
+
600
+ setStatus("Deploy complete", "success");
601
+ termLog("=== Deploy pipeline finished ===", "success");
602
+ } catch (err) {
603
+ termLog(`Deploy error: ${err.message}`, "error");
604
+ setStatus("Deploy failed", "error");
605
+ }
606
+ });
607
+
608
+ // ── Toolbar: Wallet ──
609
+ document.getElementById("btn-wallet").addEventListener("click", () => {
610
+ document.getElementById("wallet-dialog").showModal();
611
+ });
612
+ document.getElementById("btn-close-wallet").addEventListener("click", () => {
613
+ document.getElementById("wallet-dialog").close();
614
+ });
615
+
616
+ // ── Toolbar: Delete node ──
617
+ document.getElementById("btn-delete-node").addEventListener("click", () => {
618
+ for (const nodeId of [...canvas.selectedNodes]) {
619
+ model.removeNode(nodeId);
620
+ }
621
+ canvas.selectedNodes.clear();
622
+ renderProps(null);
623
+ editorPanel.setNode(null);
624
+ });
625
+
626
+ // ── Terminal: Clear ──
627
+ document.getElementById("btn-clear-term").addEventListener("click", termClear);
628
+
629
+ // ── Template: ISS Proximity OEM ──
630
+ document.getElementById("btn-load-iss").addEventListener("click", async () => {
631
+ try {
632
+ const resp = await fetch("../examples/flows/iss-proximity-oem/flow.json");
633
+ if (!resp.ok) {
634
+ // Try embedded example
635
+ loadISSExample();
636
+ return;
637
+ }
638
+ const json = await resp.json();
639
+ model.fromJSON(json);
640
+ setStatus(`Loaded: ${json.name}`, "success");
641
+ termLog(`Loaded template: ${json.name}`, "success");
642
+ } catch (e) {
643
+ loadISSExample();
644
+ }
645
+ });
646
+
647
+ function loadISSExample() {
648
+ // Embedded minimal version of ISS Proximity flow
649
+ model.fromJSON({
650
+ programId: "com.digitalarsenal.examples.iss-proximity-oem",
651
+ name: "ISS Proximity OEM Flow",
652
+ version: "0.1.0",
653
+ description: "Stream OMMs, query proximity, propagate, generate OEMs.",
654
+ nodes: [
655
+ { nodeId: "db-ingest", pluginId: "com.digitalarsenal.flatsql.store", methodId: "upsert_records", kind: "transform", drainPolicy: "drain-until-yield", label: "DB Ingest", ports: { inputs: [{ id: "records", label: "records" }], outputs: [{ id: "out", label: "out" }] } },
656
+ { nodeId: "build-query", pluginId: "com.digitalarsenal.flow.query-anchor", methodId: "build_radius_query", kind: "analyzer", drainPolicy: "drain-until-yield", label: "Build Query", ports: { inputs: [{ id: "tick", label: "tick" }], outputs: [{ id: "query", label: "query" }] } },
657
+ { nodeId: "db-query", pluginId: "com.digitalarsenal.flatsql.store", methodId: "query_objects_within_radius", kind: "analyzer", drainPolicy: "drain-until-yield", label: "DB Query", ports: { inputs: [{ id: "query", label: "query" }], outputs: [{ id: "matches", label: "matches" }] } },
658
+ { nodeId: "propagate", pluginId: "com.digitalarsenal.propagator.sgp4", methodId: "propagate_one_orbit_samples", kind: "transform", drainPolicy: "drain-until-yield", label: "Propagate", ports: { inputs: [{ id: "selection", label: "selection" }], outputs: [{ id: "samples", label: "samples" }] } },
659
+ { nodeId: "generate-oem", pluginId: "com.digitalarsenal.oem.generator", methodId: "generate_oem", kind: "transform", drainPolicy: "drain-until-yield", label: "Gen OEM", ports: { inputs: [{ id: "samples", label: "samples" }], outputs: [{ id: "oems", label: "oems" }] } },
660
+ { nodeId: "write-oem", pluginId: "com.digitalarsenal.oem.file-writer", methodId: "write_oem_files", kind: "sink", drainPolicy: "drain-until-yield", label: "Write OEM", ports: { inputs: [{ id: "oems", label: "oems" }], outputs: [] } },
661
+ { nodeId: "publish-oem", pluginId: "com.digitalarsenal.oem.publisher", methodId: "publish_oem", kind: "publisher", drainPolicy: "drain-until-yield", label: "Publish OEM", ports: { inputs: [{ id: "oems", label: "oems" }], outputs: [] } },
662
+ ],
663
+ edges: [
664
+ { edgeId: "e1", fromNodeId: "build-query", fromPortId: "query", toNodeId: "db-query", toPortId: "query", backpressurePolicy: "latest", queueDepth: 1 },
665
+ { edgeId: "e2", fromNodeId: "db-query", fromPortId: "matches", toNodeId: "propagate", toPortId: "selection", backpressurePolicy: "queue", queueDepth: 32 },
666
+ { edgeId: "e3", fromNodeId: "propagate", fromPortId: "samples", toNodeId: "generate-oem", toPortId: "samples", backpressurePolicy: "queue", queueDepth: 32 },
667
+ { edgeId: "e4", fromNodeId: "generate-oem", fromPortId: "oems", toNodeId: "write-oem", toPortId: "oems", backpressurePolicy: "queue", queueDepth: 32 },
668
+ { edgeId: "e5", fromNodeId: "generate-oem", fromPortId: "oems", toNodeId: "publish-oem", toPortId: "oems", backpressurePolicy: "queue", queueDepth: 32 },
669
+ ],
670
+ triggers: [
671
+ { triggerId: "omm-subscription", kind: "pubsub-subscription", source: "/sdn/catalog/omm" },
672
+ { triggerId: "refresh-query", kind: "timer", source: "refresh-query", defaultIntervalMs: 15000 },
673
+ ],
674
+ triggerBindings: [
675
+ { triggerId: "omm-subscription", targetNodeId: "db-ingest", targetPortId: "records", backpressurePolicy: "queue", queueDepth: 4096 },
676
+ { triggerId: "refresh-query", targetNodeId: "build-query", targetPortId: "tick", backpressurePolicy: "latest", queueDepth: 1 },
677
+ ],
678
+ editor: {
679
+ viewport: { x: 0, y: 0, zoom: 0.85 },
680
+ nodes: {
681
+ "db-ingest": { x: 80, y: 160 },
682
+ "build-query": { x: 80, y: 380 },
683
+ "db-query": { x: 360, y: 380 },
684
+ "propagate": { x: 640, y: 380 },
685
+ "generate-oem": { x: 920, y: 380 },
686
+ "write-oem": { x: 1200, y: 280 },
687
+ "publish-oem": { x: 1200, y: 480 },
688
+ },
689
+ },
690
+ });
691
+ setStatus("Loaded: ISS Proximity OEM Flow", "success");
692
+ termLog("Loaded template: ISS Proximity OEM Flow", "success");
693
+ }
694
+
695
+ // ── Resize handles ──
696
+ function setupResize(handleId, direction, getTarget) {
697
+ const handle = document.getElementById(handleId);
698
+ if (!handle) return;
699
+ handle.addEventListener("mousedown", (e) => {
700
+ e.preventDefault();
701
+ const target = getTarget();
702
+ const startX = e.clientX;
703
+ const startY = e.clientY;
704
+ const startW = target.offsetWidth;
705
+ const startH = target.offsetHeight;
706
+ const onMove = (e2) => {
707
+ if (direction === "horizontal") {
708
+ const newW = startW - (e2.clientX - startX);
709
+ target.style.width = `${Math.max(200, newW)}px`;
710
+ } else {
711
+ const newH = startH + (e2.clientY - startY);
712
+ target.style.height = `${Math.max(60, newH)}px`;
713
+ }
714
+ editorPanel.layout();
715
+ };
716
+ const onUp = () => {
717
+ window.removeEventListener("mousemove", onMove);
718
+ window.removeEventListener("mouseup", onUp);
719
+ };
720
+ window.addEventListener("mousemove", onMove);
721
+ window.addEventListener("mouseup", onUp);
722
+ });
723
+ }
724
+
725
+ setupResize("right-resize", "horizontal", () => document.getElementById("right-panel"));
726
+
727
+ // ── VS Code message handler ──
728
+ if (vscode) {
729
+ window.addEventListener("message", ({ data }) => {
730
+ if (data.command === "loadFlow") {
731
+ try {
732
+ model.fromJSON(JSON.parse(data.data));
733
+ setStatus("Flow loaded from VS Code", "success");
734
+ } catch (e) {
735
+ termLog(`VS Code load error: ${e.message}`, "error");
736
+ }
737
+ }
738
+ });
739
+ }
740
+
741
+ // ── Init ──
742
+ async function init() {
743
+ await editorPanel.ready();
744
+ editorPanel.bindModel(model);
745
+ updateCounts();
746
+ setStatus("Ready");
747
+ termLog("sdn-flow IDE ready.", "success");
748
+ termLog("Drag nodes from the palette, or load the ISS Proximity template.", "info");
749
+ }
750
+
751
+ init();