0nmcp 1.6.0 → 2.0.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/README.md +24 -23
- package/cli.js +667 -1
- package/command-runner.js +224 -0
- package/commands.js +115 -0
- package/connections.js +3 -1
- package/engine/app-builder.js +318 -0
- package/engine/app-server.js +471 -0
- package/engine/application.js +205 -0
- package/engine/bundler.js +13 -0
- package/engine/index.js +281 -3
- package/engine/operations.js +227 -0
- package/engine/scheduler.js +270 -0
- package/index.js +8 -1
- package/lib/badges.json +1 -1
- package/lib/stats.json +4 -3
- package/package.json +45 -6
- package/server.js +2 -2
- package/vault/container.js +479 -0
- package/vault/crypto-container.js +278 -0
- package/vault/escrow.js +227 -0
- package/vault/layers.js +254 -0
- package/vault/registry.js +159 -0
- package/vault/seal.js +74 -0
- package/vault/tools-container.js +356 -0
- package/workflow.js +36 -4
package/engine/index.js
CHANGED
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
// 0nMCP — Engine Module
|
|
3
3
|
// ============================================================
|
|
4
4
|
// The .0n Conversion Engine — import credentials, verify keys,
|
|
5
|
-
// generate platform configs,
|
|
6
|
-
//
|
|
5
|
+
// generate platform configs, create portable AI Brain bundles,
|
|
6
|
+
// and build/run application bundles.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// 11 MCP Tools:
|
|
9
9
|
// engine_import — Import credentials from .env/CSV/JSON
|
|
10
10
|
// engine_verify — Verify API keys with test calls
|
|
11
11
|
// engine_platforms — Generate platform configs
|
|
12
12
|
// engine_export — Export .0n bundle from connections
|
|
13
13
|
// engine_bundle — Full pipeline: import → map → bundle
|
|
14
14
|
// engine_open — Open a .0n bundle file
|
|
15
|
+
// app_build — Build a .0n application bundle
|
|
16
|
+
// app_open — Open/extract a .0n application
|
|
17
|
+
// app_inspect — Show application metadata (no passphrase)
|
|
18
|
+
// app_validate — Validate application cross-references
|
|
19
|
+
// app_list — List installed applications
|
|
15
20
|
//
|
|
16
21
|
// Patent Pending: US Provisional Patent Application #63/968,814
|
|
17
22
|
// ============================================================
|
|
@@ -23,6 +28,11 @@ export { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
|
|
|
23
28
|
export { verifyCredentials, verifyAll } from "./validator.js";
|
|
24
29
|
export { generatePlatformConfig, generateAllPlatformConfigs, installPlatformConfig, getPlatformInfo, listPlatforms } from "./platforms.js";
|
|
25
30
|
export { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
|
|
31
|
+
export { OperationRegistry, validateOperations } from "./operations.js";
|
|
32
|
+
export { parseCron, CronScheduler } from "./scheduler.js";
|
|
33
|
+
export { Application } from "./application.js";
|
|
34
|
+
export { createApplication, openApplication, inspectApplication, validateApplication } from "./app-builder.js";
|
|
35
|
+
export { ApplicationServer } from "./app-server.js";
|
|
26
36
|
|
|
27
37
|
// ── Imports for tool handlers ──────────────────────────────
|
|
28
38
|
import { parseFile } from "./parser.js";
|
|
@@ -30,11 +40,14 @@ import { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
|
|
|
30
40
|
import { verifyCredentials, verifyAll } from "./validator.js";
|
|
31
41
|
import { generatePlatformConfig, generateAllPlatformConfigs, getPlatformInfo, listPlatforms } from "./platforms.js";
|
|
32
42
|
import { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
|
|
43
|
+
import { createApplication, openApplication, inspectApplication, validateApplication } from "./app-builder.js";
|
|
33
44
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
34
45
|
import { join } from "path";
|
|
35
46
|
import { homedir } from "os";
|
|
36
47
|
|
|
37
48
|
const CONNECTIONS_DIR = join(homedir(), ".0n", "connections");
|
|
49
|
+
const APPS_DIR = join(homedir(), ".0n", "apps");
|
|
50
|
+
const WORKFLOWS_DIR = join(homedir(), ".0n", "workflows");
|
|
38
51
|
|
|
39
52
|
/**
|
|
40
53
|
* Load all connections from ~/.0n/connections/ for bundling.
|
|
@@ -387,4 +400,269 @@ Example: engine_open({ bundle: "/path/to/bundle.0n", passphrase: "my-passphrase"
|
|
|
387
400
|
}
|
|
388
401
|
}
|
|
389
402
|
);
|
|
403
|
+
|
|
404
|
+
// ═══════════════════════════════════════════════════════════
|
|
405
|
+
// Application Engine Tools (v1.7.0)
|
|
406
|
+
// ═══════════════════════════════════════════════════════════
|
|
407
|
+
|
|
408
|
+
// ─── app_build ────────────────────────────────────────────
|
|
409
|
+
server.tool(
|
|
410
|
+
"app_build",
|
|
411
|
+
`Build a .0n application bundle — a portable encrypted file containing
|
|
412
|
+
endpoints, workflows, operations, automations, and connections.
|
|
413
|
+
Deploy anywhere with: 0nmcp app run <file>
|
|
414
|
+
|
|
415
|
+
Example: app_build({ name: "Lead Scorer", passphrase: "secret", workflows: {...}, endpoints: {...} })`,
|
|
416
|
+
{
|
|
417
|
+
name: z.string().describe("Application name"),
|
|
418
|
+
passphrase: z.string().describe("Passphrase to encrypt the bundle"),
|
|
419
|
+
workflows: z.record(z.any()).optional().describe("Workflow definitions { id: workflowDef }"),
|
|
420
|
+
connections: z.record(z.any()).optional().describe("Connections to include { service: { credentials } }"),
|
|
421
|
+
endpoints: z.record(z.any()).optional().describe("Endpoint definitions { 'METHOD /path': def }"),
|
|
422
|
+
operations: z.record(z.any()).optional().describe("Reusable operation definitions { id: def }"),
|
|
423
|
+
automations: z.record(z.any()).optional().describe("Automation definitions { id: def }"),
|
|
424
|
+
environment: z.object({
|
|
425
|
+
variables: z.record(z.string()).optional(),
|
|
426
|
+
secrets: z.record(z.string()).optional(),
|
|
427
|
+
settings: z.record(z.any()).optional(),
|
|
428
|
+
feature_flags: z.record(z.boolean()).optional(),
|
|
429
|
+
}).optional().describe("Environment configuration"),
|
|
430
|
+
output: z.string().optional().describe("Output file path"),
|
|
431
|
+
},
|
|
432
|
+
async ({ name, passphrase, workflows, connections, endpoints, operations, automations, environment, output }) => {
|
|
433
|
+
try {
|
|
434
|
+
// If no connections provided, load local connections
|
|
435
|
+
const conns = connections || loadLocalConnections();
|
|
436
|
+
|
|
437
|
+
const result = createApplication({
|
|
438
|
+
name,
|
|
439
|
+
passphrase,
|
|
440
|
+
connections: conns,
|
|
441
|
+
workflows: workflows || {},
|
|
442
|
+
endpoints: endpoints || {},
|
|
443
|
+
operations: operations || {},
|
|
444
|
+
automations: automations || {},
|
|
445
|
+
environment: environment || {},
|
|
446
|
+
output,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
content: [{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: JSON.stringify({
|
|
453
|
+
status: "built",
|
|
454
|
+
path: result.path,
|
|
455
|
+
manifest: result.manifest,
|
|
456
|
+
message: `Application "${name}" built at ${result.path}. Run with: 0nmcp app run ${result.path}`,
|
|
457
|
+
}, null, 2),
|
|
458
|
+
}],
|
|
459
|
+
};
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// ─── app_open ─────────────────────────────────────────────
|
|
467
|
+
server.tool(
|
|
468
|
+
"app_open",
|
|
469
|
+
`Open a .0n application bundle file.
|
|
470
|
+
Decrypts and extracts the application for local use.
|
|
471
|
+
|
|
472
|
+
Example: app_open({ bundle: "/path/to/app.0n", passphrase: "secret" })`,
|
|
473
|
+
{
|
|
474
|
+
bundle: z.string().describe("Path to .0n application file"),
|
|
475
|
+
passphrase: z.string().optional().describe("Passphrase to decrypt — omit to inspect only"),
|
|
476
|
+
},
|
|
477
|
+
async ({ bundle, passphrase }) => {
|
|
478
|
+
try {
|
|
479
|
+
if (!passphrase) {
|
|
480
|
+
const info = inspectApplication(bundle);
|
|
481
|
+
return {
|
|
482
|
+
content: [{
|
|
483
|
+
type: "text",
|
|
484
|
+
text: JSON.stringify({
|
|
485
|
+
status: "inspected",
|
|
486
|
+
...info,
|
|
487
|
+
message: `Application "${info.name}" has ${info.workflows.length} workflows, ${info.endpoints.length} endpoints. Provide passphrase to extract.`,
|
|
488
|
+
}, null, 2),
|
|
489
|
+
}],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const bundleData = openApplication(bundle, passphrase);
|
|
494
|
+
return {
|
|
495
|
+
content: [{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: JSON.stringify({
|
|
498
|
+
status: "opened",
|
|
499
|
+
name: bundleData.$0n.name,
|
|
500
|
+
workflows: Object.keys(bundleData.workflows || {}),
|
|
501
|
+
endpoints: Object.keys(bundleData.endpoints || {}),
|
|
502
|
+
operations: Object.keys(bundleData.operations || {}),
|
|
503
|
+
automations: Object.keys(bundleData.automations || {}),
|
|
504
|
+
connections: (bundleData.connections || []).map(c => c.service),
|
|
505
|
+
message: `Application "${bundleData.$0n.name}" opened. Run with: 0nmcp app run ${bundle}`,
|
|
506
|
+
}, null, 2),
|
|
507
|
+
}],
|
|
508
|
+
};
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// ─── app_inspect ──────────────────────────────────────────
|
|
516
|
+
server.tool(
|
|
517
|
+
"app_inspect",
|
|
518
|
+
`Inspect a .0n application bundle without passphrase.
|
|
519
|
+
Shows metadata, endpoints, workflows, and automations.
|
|
520
|
+
|
|
521
|
+
Example: app_inspect({ bundle: "/path/to/app.0n" })`,
|
|
522
|
+
{
|
|
523
|
+
bundle: z.string().describe("Path to .0n application file"),
|
|
524
|
+
},
|
|
525
|
+
async ({ bundle }) => {
|
|
526
|
+
try {
|
|
527
|
+
const info = inspectApplication(bundle);
|
|
528
|
+
return {
|
|
529
|
+
content: [{
|
|
530
|
+
type: "text",
|
|
531
|
+
text: JSON.stringify({
|
|
532
|
+
status: "inspected",
|
|
533
|
+
...info,
|
|
534
|
+
}, null, 2),
|
|
535
|
+
}],
|
|
536
|
+
};
|
|
537
|
+
} catch (err) {
|
|
538
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// ─── app_validate ─────────────────────────────────────────
|
|
544
|
+
server.tool(
|
|
545
|
+
"app_validate",
|
|
546
|
+
`Validate a .0n application bundle's structure and cross-references.
|
|
547
|
+
Checks that endpoints reference valid workflows, workflows reference valid operations, etc.
|
|
548
|
+
|
|
549
|
+
Example: app_validate({ bundle: "/path/to/app.0n", passphrase: "secret" })`,
|
|
550
|
+
{
|
|
551
|
+
bundle: z.string().describe("Path to .0n application file"),
|
|
552
|
+
passphrase: z.string().optional().describe("Passphrase to decrypt for full validation"),
|
|
553
|
+
},
|
|
554
|
+
async ({ bundle, passphrase }) => {
|
|
555
|
+
try {
|
|
556
|
+
const raw = readFileSync(bundle, "utf-8");
|
|
557
|
+
const bundleData = JSON.parse(raw);
|
|
558
|
+
|
|
559
|
+
const result = validateApplication(bundleData);
|
|
560
|
+
return {
|
|
561
|
+
content: [{
|
|
562
|
+
type: "text",
|
|
563
|
+
text: JSON.stringify({
|
|
564
|
+
status: result.valid ? "valid" : "invalid",
|
|
565
|
+
...result,
|
|
566
|
+
message: result.valid
|
|
567
|
+
? "Application bundle is valid."
|
|
568
|
+
: `Found ${result.errors.length} error(s).`,
|
|
569
|
+
}, null, 2),
|
|
570
|
+
}],
|
|
571
|
+
};
|
|
572
|
+
} catch (err) {
|
|
573
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// ─── app_list ─────────────────────────────────────────────
|
|
579
|
+
server.tool(
|
|
580
|
+
"app_list",
|
|
581
|
+
`List installed .0n applications from ~/.0n/apps/.
|
|
582
|
+
|
|
583
|
+
Example: app_list({})`,
|
|
584
|
+
{},
|
|
585
|
+
async () => {
|
|
586
|
+
try {
|
|
587
|
+
if (!existsSync(APPS_DIR)) {
|
|
588
|
+
return {
|
|
589
|
+
content: [{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: JSON.stringify({
|
|
592
|
+
status: "ok",
|
|
593
|
+
count: 0,
|
|
594
|
+
apps: [],
|
|
595
|
+
message: "No applications installed. Build one with app_build.",
|
|
596
|
+
}, null, 2),
|
|
597
|
+
}],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const files = readdirSync(APPS_DIR).filter(f => f.endsWith(".0n") || f.endsWith(".0n.json"));
|
|
602
|
+
const apps = [];
|
|
603
|
+
|
|
604
|
+
for (const file of files) {
|
|
605
|
+
try {
|
|
606
|
+
const filePath = join(APPS_DIR, file);
|
|
607
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
608
|
+
if (data.$0n?.type !== "application") continue;
|
|
609
|
+
|
|
610
|
+
apps.push({
|
|
611
|
+
name: data.$0n.name || file,
|
|
612
|
+
version: data.$0n.version || "1.0.0",
|
|
613
|
+
description: data.$0n.description || "",
|
|
614
|
+
author: data.$0n.author || "",
|
|
615
|
+
created: data.$0n.created,
|
|
616
|
+
file,
|
|
617
|
+
path: filePath,
|
|
618
|
+
workflows: Object.keys(data.workflows || {}).length,
|
|
619
|
+
endpoints: Object.keys(data.endpoints || {}).length,
|
|
620
|
+
operations: Object.keys(data.operations || {}).length,
|
|
621
|
+
automations: Object.keys(data.automations || {}).length,
|
|
622
|
+
connections: (data.connections || []).length,
|
|
623
|
+
});
|
|
624
|
+
} catch { /* skip invalid */ }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
content: [{
|
|
629
|
+
type: "text",
|
|
630
|
+
text: JSON.stringify({
|
|
631
|
+
status: "ok",
|
|
632
|
+
count: apps.length,
|
|
633
|
+
apps,
|
|
634
|
+
message: apps.length > 0
|
|
635
|
+
? `Found ${apps.length} application(s). Run with: 0nmcp app run <file>`
|
|
636
|
+
: "No applications installed.",
|
|
637
|
+
}, null, 2),
|
|
638
|
+
}],
|
|
639
|
+
};
|
|
640
|
+
} catch (err) {
|
|
641
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Load local workflows from ~/.0n/workflows/ for bundling into applications.
|
|
649
|
+
* @param {string[]} [ids] — Specific workflow IDs to load (default: all)
|
|
650
|
+
* @returns {Record<string, object>}
|
|
651
|
+
*/
|
|
652
|
+
export function loadLocalWorkflows(ids) {
|
|
653
|
+
const workflows = {};
|
|
654
|
+
if (!existsSync(WORKFLOWS_DIR)) return workflows;
|
|
655
|
+
|
|
656
|
+
const files = readdirSync(WORKFLOWS_DIR);
|
|
657
|
+
for (const file of files) {
|
|
658
|
+
if (!file.endsWith(".0n") && !file.endsWith(".0n.json")) continue;
|
|
659
|
+
try {
|
|
660
|
+
const data = JSON.parse(readFileSync(join(WORKFLOWS_DIR, file), "utf-8"));
|
|
661
|
+
if (!data.$0n || data.$0n.type !== "workflow") continue;
|
|
662
|
+
const id = file.replace(/\.0n(\.json)?$/, "");
|
|
663
|
+
if (ids && !ids.includes(id)) continue;
|
|
664
|
+
workflows[id] = data;
|
|
665
|
+
} catch { /* skip invalid */ }
|
|
666
|
+
}
|
|
667
|
+
return workflows;
|
|
390
668
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP -Engine: Operation Registry
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Reusable action definitions referenced by workflow steps
|
|
5
|
+
// via "operation": "id". Wraps INTERNAL_ACTIONS from workflow.js
|
|
6
|
+
// for type: "internal". Service operations delegate to
|
|
7
|
+
// catalog-based execution.
|
|
8
|
+
//
|
|
9
|
+
// Patent Pending: US Provisional Patent Application #63/968,814
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Registry of reusable operations for application bundles.
|
|
14
|
+
* Operations are defined once and referenced by ID in workflow steps.
|
|
15
|
+
*/
|
|
16
|
+
export class OperationRegistry {
|
|
17
|
+
constructor() {
|
|
18
|
+
/** @type {Map<string, object>} */
|
|
19
|
+
this._ops = new Map();
|
|
20
|
+
/** @type {object|null} */
|
|
21
|
+
this._internalActions = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load internal actions for use by "internal" type operations.
|
|
26
|
+
* @param {object} actions -INTERNAL_ACTIONS from workflow.js
|
|
27
|
+
*/
|
|
28
|
+
setInternalActions(actions) {
|
|
29
|
+
this._internalActions = actions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a single operation definition.
|
|
34
|
+
* @param {string} id
|
|
35
|
+
* @param {object} def -{ name, type, action, params, inputs, outputs }
|
|
36
|
+
*/
|
|
37
|
+
register(id, def) {
|
|
38
|
+
this._ops.set(id, { id, ...def });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register all operations from a definitions object.
|
|
43
|
+
* @param {Record<string, object>} defs -{ opId: { name, type, ... }, ... }
|
|
44
|
+
*/
|
|
45
|
+
registerAll(defs) {
|
|
46
|
+
for (const [id, def] of Object.entries(defs)) {
|
|
47
|
+
this.register(id, def);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if an operation exists.
|
|
53
|
+
* @param {string} id
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
has(id) {
|
|
57
|
+
return this._ops.has(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get an operation definition.
|
|
62
|
+
* @param {string} id
|
|
63
|
+
* @returns {object|undefined}
|
|
64
|
+
*/
|
|
65
|
+
get(id) {
|
|
66
|
+
return this._ops.get(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* List all registered operations.
|
|
71
|
+
* @returns {Array<{ id: string, name: string, type: string }>}
|
|
72
|
+
*/
|
|
73
|
+
list() {
|
|
74
|
+
return [...this._ops.values()].map(op => ({
|
|
75
|
+
id: op.id,
|
|
76
|
+
name: op.name || op.id,
|
|
77
|
+
type: op.type || "internal",
|
|
78
|
+
inputs: op.inputs ? Object.keys(op.inputs) : [],
|
|
79
|
+
outputs: op.outputs ? Object.keys(op.outputs) : [],
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute an operation by ID.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} id -Operation ID
|
|
87
|
+
* @param {object} params -Resolved parameters (after template resolution)
|
|
88
|
+
* @param {object} context -{ connections?, env? } for service calls
|
|
89
|
+
* @returns {Promise<object>} Operation result
|
|
90
|
+
*/
|
|
91
|
+
async execute(id, params, context = {}) {
|
|
92
|
+
const op = this._ops.get(id);
|
|
93
|
+
if (!op) {
|
|
94
|
+
throw new Error(`Unknown operation: ${id}. Available: ${[...this._ops.keys()].join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Merge operation's default params with step-level params
|
|
98
|
+
const mergedParams = { ...(op.params || {}), ...params };
|
|
99
|
+
|
|
100
|
+
if (op.type === "internal" || !op.type) {
|
|
101
|
+
return this._executeInternal(op.action, mergedParams);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (op.type === "service") {
|
|
105
|
+
return this._executeService(op.service, op.action, mergedParams, context);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`Unknown operation type: ${op.type} for operation ${id}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Execute an internal action.
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
_executeInternal(action, params) {
|
|
116
|
+
if (!this._internalActions) {
|
|
117
|
+
throw new Error("Internal actions not loaded. Call setInternalActions() first.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const handler = this._internalActions[action];
|
|
121
|
+
if (!handler) {
|
|
122
|
+
throw new Error(`Unknown internal action: ${action}. Available: ${Object.keys(this._internalActions).join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return handler(params);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Execute a service call via ConnectionManager.
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
async _executeService(service, action, params, context) {
|
|
133
|
+
if (!context.connections) {
|
|
134
|
+
throw new Error(`Service operation ${service}.${action} requires connections in context.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const creds = context.connections.getCredentials(service);
|
|
138
|
+
if (!creds) {
|
|
139
|
+
throw new Error(`Service ${service} not connected.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Delegate to catalog-based execution via the connection manager
|
|
143
|
+
// This follows the same pattern as WorkflowRunner._executeService
|
|
144
|
+
const { SERVICE_CATALOG } = await import("../catalog.js");
|
|
145
|
+
const catalog = SERVICE_CATALOG[service];
|
|
146
|
+
if (!catalog) {
|
|
147
|
+
throw new Error(`Unknown service: ${service}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const endpointKeys = Object.keys(catalog.endpoints);
|
|
151
|
+
const ep = catalog.endpoints[action] || catalog.endpoints[endpointKeys.find(k => k.includes(action))];
|
|
152
|
+
if (!ep) {
|
|
153
|
+
throw new Error(`No endpoint found for ${service}.${action}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let url = catalog.baseUrl + ep.path;
|
|
157
|
+
const allParams = { ...creds, ...params };
|
|
158
|
+
url = url.replace(/\{(\w+)\}/g, (_, key) => allParams[key] || `{${key}}`);
|
|
159
|
+
|
|
160
|
+
const headers = catalog.authHeader(creds);
|
|
161
|
+
const options = { method: ep.method, headers };
|
|
162
|
+
|
|
163
|
+
if (ep.method !== "GET" && params) {
|
|
164
|
+
headers["Content-Type"] = "application/json";
|
|
165
|
+
options.body = JSON.stringify(params);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (ep.method === "GET" && params) {
|
|
169
|
+
const flat = {};
|
|
170
|
+
for (const [k, v] of Object.entries(params)) {
|
|
171
|
+
if (typeof v !== "object") flat[k] = String(v);
|
|
172
|
+
}
|
|
173
|
+
const qs = new URLSearchParams(flat).toString();
|
|
174
|
+
if (qs) url += (url.includes("?") ? "&" : "?") + qs;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const response = await fetch(url, options);
|
|
178
|
+
const data = await response.json().catch(() => ({ status: response.status }));
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(`${service}.${action} failed (${response.status}): ${JSON.stringify(data)}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validate operation definitions from an application bundle.
|
|
190
|
+
*
|
|
191
|
+
* @param {Record<string, object>} defs -operations section of bundle
|
|
192
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
193
|
+
*/
|
|
194
|
+
export function validateOperations(defs) {
|
|
195
|
+
const errors = [];
|
|
196
|
+
|
|
197
|
+
if (!defs || typeof defs !== "object") {
|
|
198
|
+
return { valid: false, errors: ["Operations must be an object"] };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const [id, def] of Object.entries(defs)) {
|
|
202
|
+
if (!def.type && !def.action) {
|
|
203
|
+
errors.push(`Operation "${id}": must have "type" or "action"`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (def.type === "internal" || !def.type) {
|
|
207
|
+
if (!def.action) {
|
|
208
|
+
errors.push(`Operation "${id}": internal operations require "action"`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (def.type === "service") {
|
|
213
|
+
if (!def.service) errors.push(`Operation "${id}": service operations require "service"`);
|
|
214
|
+
if (!def.action) errors.push(`Operation "${id}": service operations require "action"`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (def.inputs && typeof def.inputs !== "object") {
|
|
218
|
+
errors.push(`Operation "${id}": "inputs" must be an object`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (def.outputs && typeof def.outputs !== "object") {
|
|
222
|
+
errors.push(`Operation "${id}": "outputs" must be an object`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { valid: errors.length === 0, errors };
|
|
227
|
+
}
|