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
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP -Engine: Application Server
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Express server that mounts custom endpoints, webhooks, and
|
|
5
|
+
// schedules from a .0n application bundle.
|
|
6
|
+
//
|
|
7
|
+
// Startup flow:
|
|
8
|
+
// 1. Decrypt connections + secrets
|
|
9
|
+
// 2. Create ConnectionManager + OperationRegistry + WorkflowRunner
|
|
10
|
+
// 3. Mount endpoints → route handler → workflow → response
|
|
11
|
+
// 4. Mount webhook automations with signature verification
|
|
12
|
+
// 5. Start schedule automations with CronScheduler
|
|
13
|
+
// 6. Print startup banner
|
|
14
|
+
//
|
|
15
|
+
// Patent Pending: US Provisional Patent Application #63/968,814
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
import { Application } from "./application.js";
|
|
19
|
+
import { OperationRegistry } from "./operations.js";
|
|
20
|
+
import { CronScheduler } from "./scheduler.js";
|
|
21
|
+
import { WorkflowRunner, INTERNAL_ACTIONS } from "../workflow.js";
|
|
22
|
+
import { ConnectionManager } from "../connections.js";
|
|
23
|
+
import { RateLimiter } from "../ratelimit.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve template expressions in endpoint mappings.
|
|
27
|
+
* Handles {{body.*}}, {{query.*}}, {{params.*}}, {{headers.*}}, {{env.*}},
|
|
28
|
+
* {{payload.*}}, {{workflow.outputs.*}}, {{workflow.executionId}}
|
|
29
|
+
*/
|
|
30
|
+
function resolveMapping(mapping, context) {
|
|
31
|
+
if (!mapping) return {};
|
|
32
|
+
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, template] of Object.entries(mapping)) {
|
|
35
|
+
if (typeof template !== "string") {
|
|
36
|
+
result[key] = template;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const singleMatch = template.match(/^\{\{(.+?)\}\}$/);
|
|
41
|
+
if (singleMatch) {
|
|
42
|
+
result[key] = deepGet(context, singleMatch[1].trim());
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
result[key] = template.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
47
|
+
const val = deepGet(context, expr.trim());
|
|
48
|
+
return val == null ? "" : String(val);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deepGet(obj, path) {
|
|
56
|
+
if (!obj || !path) return undefined;
|
|
57
|
+
const segs = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
58
|
+
let cur = obj;
|
|
59
|
+
for (const s of segs) {
|
|
60
|
+
if (cur == null) return undefined;
|
|
61
|
+
cur = cur[s];
|
|
62
|
+
}
|
|
63
|
+
return cur;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Application Server -runs a .0n application bundle as an HTTP server.
|
|
68
|
+
*/
|
|
69
|
+
export class ApplicationServer {
|
|
70
|
+
/**
|
|
71
|
+
* @param {Application} app -Loaded Application instance
|
|
72
|
+
*/
|
|
73
|
+
constructor(app) {
|
|
74
|
+
this._app = app;
|
|
75
|
+
this._express = null;
|
|
76
|
+
this._server = null;
|
|
77
|
+
this._scheduler = new CronScheduler();
|
|
78
|
+
this._rateLimiters = new Map();
|
|
79
|
+
this._startedAt = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start the application server.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} options
|
|
86
|
+
* @param {number} [options.port=3000]
|
|
87
|
+
* @param {string} [options.host="0.0.0.0"]
|
|
88
|
+
* @returns {Promise<{ server: object, port: number }>}
|
|
89
|
+
*/
|
|
90
|
+
async start({ port = 3000, host = "0.0.0.0" } = {}) {
|
|
91
|
+
let express;
|
|
92
|
+
try {
|
|
93
|
+
express = (await import("express")).default;
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error("express is required. Install with: npm install express");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 1. Decrypt connections + secrets
|
|
99
|
+
const connections = this._app.getConnections();
|
|
100
|
+
const env = this._app.getFlatEnv();
|
|
101
|
+
|
|
102
|
+
// 2. Create ConnectionManager + OperationRegistry + WorkflowRunner
|
|
103
|
+
const connManager = new ConnectionManager();
|
|
104
|
+
// Inject decrypted credentials into connection manager
|
|
105
|
+
for (const conn of connections) {
|
|
106
|
+
connManager.connections[conn.service] = {
|
|
107
|
+
serviceKey: conn.service,
|
|
108
|
+
name: conn.name,
|
|
109
|
+
type: "service",
|
|
110
|
+
credentials: conn.credentials,
|
|
111
|
+
connectedAt: new Date().toISOString(),
|
|
112
|
+
environment: conn.environment,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const registry = new OperationRegistry();
|
|
117
|
+
registry.setInternalActions(INTERNAL_ACTIONS);
|
|
118
|
+
registry.registerAll(this._app.getOperations());
|
|
119
|
+
|
|
120
|
+
const workflowRunner = new WorkflowRunner(connManager);
|
|
121
|
+
|
|
122
|
+
// 3. Build Express app
|
|
123
|
+
const app = express();
|
|
124
|
+
app.use(express.json());
|
|
125
|
+
|
|
126
|
+
// 4. Mount endpoints
|
|
127
|
+
const endpoints = this._app.getEndpoints();
|
|
128
|
+
for (const [route, epDef] of Object.entries(endpoints)) {
|
|
129
|
+
this._mountEndpoint(app, route, epDef, {
|
|
130
|
+
workflowRunner,
|
|
131
|
+
registry,
|
|
132
|
+
env,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 5. Mount automations
|
|
137
|
+
const automations = this._app.getAutomations();
|
|
138
|
+
for (const [id, auto] of Object.entries(automations)) {
|
|
139
|
+
if (auto.type === "webhook") {
|
|
140
|
+
this._mountWebhook(app, id, auto, {
|
|
141
|
+
workflowRunner,
|
|
142
|
+
registry,
|
|
143
|
+
env,
|
|
144
|
+
});
|
|
145
|
+
} else if (auto.type === "schedule") {
|
|
146
|
+
this._mountSchedule(id, auto, {
|
|
147
|
+
workflowRunner,
|
|
148
|
+
registry,
|
|
149
|
+
env,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Default health endpoint (always available)
|
|
155
|
+
app.get("/health", (req, res) => {
|
|
156
|
+
res.json({
|
|
157
|
+
status: "ok",
|
|
158
|
+
application: this._app.getSummary(),
|
|
159
|
+
uptime: Math.floor((Date.now() - this._startedAt) / 1000),
|
|
160
|
+
schedules: this._scheduler.list().length,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 6. Start server
|
|
165
|
+
this._express = app;
|
|
166
|
+
this._startedAt = Date.now();
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
this._server = app.listen(port, host, () => {
|
|
170
|
+
this._printBanner(port, host, endpoints, automations);
|
|
171
|
+
resolve({ server: this._server, port });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Stop the server and all scheduled jobs.
|
|
178
|
+
*/
|
|
179
|
+
async stop() {
|
|
180
|
+
this._scheduler.stopAll();
|
|
181
|
+
if (this._server) {
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
this._server.close(() => resolve());
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Mount an endpoint route.
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
_mountEndpoint(app, route, epDef, ctx) {
|
|
193
|
+
// Parse "METHOD /path" format
|
|
194
|
+
const parts = route.split(/\s+/);
|
|
195
|
+
let method, path;
|
|
196
|
+
if (parts.length === 2) {
|
|
197
|
+
method = parts[0].toLowerCase();
|
|
198
|
+
path = parts[1];
|
|
199
|
+
} else {
|
|
200
|
+
method = "get";
|
|
201
|
+
path = route;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Built-in handlers
|
|
205
|
+
if (epDef.handler === "health") {
|
|
206
|
+
app[method](path, (req, res) => {
|
|
207
|
+
res.json({
|
|
208
|
+
status: "ok",
|
|
209
|
+
application: this._app.getSummary(),
|
|
210
|
+
uptime: Math.floor((Date.now() - this._startedAt) / 1000),
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Workflow-based endpoint
|
|
217
|
+
const handler = async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
// Auth check
|
|
220
|
+
if (epDef.auth && epDef.auth.type !== "none") {
|
|
221
|
+
const authOk = this._checkAuth(req, epDef.auth, ctx.env);
|
|
222
|
+
if (!authOk) {
|
|
223
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Rate limiting
|
|
229
|
+
if (epDef.rate_limit) {
|
|
230
|
+
const limiterKey = `endpoint:${route}`;
|
|
231
|
+
if (!this._rateLimiters.has(limiterKey)) {
|
|
232
|
+
const rpm = epDef.rate_limit.requests_per_minute || 60;
|
|
233
|
+
this._rateLimiters.set(limiterKey, new RateLimiter(rpm, 60000));
|
|
234
|
+
}
|
|
235
|
+
const limiter = this._rateLimiters.get(limiterKey);
|
|
236
|
+
if (!limiter.tryAcquire()) {
|
|
237
|
+
res.status(429).json({ error: "Rate limit exceeded" });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Resolve input mapping
|
|
243
|
+
const mappingContext = {
|
|
244
|
+
body: req.body || {},
|
|
245
|
+
query: req.query || {},
|
|
246
|
+
params: req.params || {},
|
|
247
|
+
headers: req.headers || {},
|
|
248
|
+
env: ctx.env,
|
|
249
|
+
};
|
|
250
|
+
const inputs = epDef.input_mapping
|
|
251
|
+
? resolveMapping(epDef.input_mapping, mappingContext)
|
|
252
|
+
: req.body || {};
|
|
253
|
+
|
|
254
|
+
// Run workflow
|
|
255
|
+
const workflow = this._app.getWorkflow(epDef.workflow);
|
|
256
|
+
if (!workflow) {
|
|
257
|
+
res.status(500).json({ error: `Workflow "${epDef.workflow}" not found in application` });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = await ctx.workflowRunner.runWithOperations({
|
|
262
|
+
workflow,
|
|
263
|
+
inputs,
|
|
264
|
+
operations: ctx.registry,
|
|
265
|
+
env: ctx.env,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Resolve output mapping
|
|
269
|
+
const outputContext = {
|
|
270
|
+
workflow: {
|
|
271
|
+
outputs: result.outputs || {},
|
|
272
|
+
executionId: result.executionId,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const output = epDef.output_mapping
|
|
276
|
+
? resolveMapping(epDef.output_mapping, outputContext)
|
|
277
|
+
: result.outputs;
|
|
278
|
+
|
|
279
|
+
res.json({
|
|
280
|
+
success: result.success,
|
|
281
|
+
...output,
|
|
282
|
+
executionId: result.executionId,
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
res.status(500).json({ error: err.message });
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
app[method](path, handler);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Mount a webhook automation.
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_mountWebhook(app, id, auto, ctx) {
|
|
297
|
+
const webhookPath = auto.config?.path || `/hooks/${id}`;
|
|
298
|
+
|
|
299
|
+
app.post(webhookPath, async (req, res) => {
|
|
300
|
+
try {
|
|
301
|
+
// Verify signature if configured
|
|
302
|
+
if (auto.config?.verify && auto.config?.secret_env) {
|
|
303
|
+
const secret = ctx.env[auto.config.secret_env];
|
|
304
|
+
if (secret) {
|
|
305
|
+
const { verifyHmac } = await import("../webhooks.js");
|
|
306
|
+
const rawBody = JSON.stringify(req.body);
|
|
307
|
+
const sig = req.headers["x-webhook-signature"] || req.headers["x-signature"] || "";
|
|
308
|
+
const verification = verifyHmac(rawBody, sig, secret);
|
|
309
|
+
if (!verification.verified) {
|
|
310
|
+
res.status(401).json({ error: "Webhook signature verification failed" });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Resolve input mapping
|
|
317
|
+
const mappingContext = {
|
|
318
|
+
payload: req.body || {},
|
|
319
|
+
headers: req.headers || {},
|
|
320
|
+
env: ctx.env,
|
|
321
|
+
};
|
|
322
|
+
const inputs = auto.input_mapping
|
|
323
|
+
? resolveMapping(auto.input_mapping, mappingContext)
|
|
324
|
+
: req.body || {};
|
|
325
|
+
|
|
326
|
+
// Run workflow
|
|
327
|
+
const workflow = this._app.getWorkflow(auto.workflow);
|
|
328
|
+
if (!workflow) {
|
|
329
|
+
res.status(500).json({ error: `Workflow "${auto.workflow}" not found` });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const result = await ctx.workflowRunner.runWithOperations({
|
|
334
|
+
workflow,
|
|
335
|
+
inputs,
|
|
336
|
+
operations: ctx.registry,
|
|
337
|
+
env: ctx.env,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
res.json({
|
|
341
|
+
status: result.success ? "completed" : "failed",
|
|
342
|
+
executionId: result.executionId,
|
|
343
|
+
});
|
|
344
|
+
} catch (err) {
|
|
345
|
+
res.status(500).json({ error: err.message });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Mount a schedule automation.
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
_mountSchedule(id, auto, ctx) {
|
|
355
|
+
if (!auto.config?.cron) return;
|
|
356
|
+
|
|
357
|
+
this._scheduler.schedule(id, auto.config.cron, async ({ scheduledTime }) => {
|
|
358
|
+
try {
|
|
359
|
+
const workflow = this._app.getWorkflow(auto.workflow);
|
|
360
|
+
if (!workflow) {
|
|
361
|
+
console.error(`Schedule "${id}": workflow "${auto.workflow}" not found`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const inputs = auto.input_mapping
|
|
366
|
+
? resolveMapping(auto.input_mapping, {
|
|
367
|
+
env: ctx.env,
|
|
368
|
+
schedule: { time: scheduledTime.toISOString(), id },
|
|
369
|
+
})
|
|
370
|
+
: {};
|
|
371
|
+
|
|
372
|
+
await ctx.workflowRunner.runWithOperations({
|
|
373
|
+
workflow,
|
|
374
|
+
inputs,
|
|
375
|
+
operations: ctx.registry,
|
|
376
|
+
env: ctx.env,
|
|
377
|
+
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.error(`Schedule "${id}" execution error:`, err.message);
|
|
380
|
+
}
|
|
381
|
+
}, { timezone: auto.config.timezone });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check authentication for an endpoint request.
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
_checkAuth(req, authDef, env) {
|
|
389
|
+
if (!authDef || authDef.type === "none") return true;
|
|
390
|
+
|
|
391
|
+
if (authDef.type === "api_key") {
|
|
392
|
+
const headerName = authDef.header || "X-API-Key";
|
|
393
|
+
const expectedKey = env[authDef.key_env] || authDef.key;
|
|
394
|
+
if (!expectedKey) return true; // No key configured = skip auth
|
|
395
|
+
return req.headers[headerName.toLowerCase()] === expectedKey;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (authDef.type === "bearer") {
|
|
399
|
+
const expectedToken = env[authDef.token_env] || authDef.token;
|
|
400
|
+
if (!expectedToken) return true;
|
|
401
|
+
const authHeader = req.headers.authorization || "";
|
|
402
|
+
return authHeader === `Bearer ${expectedToken}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Print startup banner.
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
_printBanner(port, host, endpoints, automations) {
|
|
413
|
+
const c = {
|
|
414
|
+
reset: "\x1b[0m",
|
|
415
|
+
bright: "\x1b[1m",
|
|
416
|
+
green: "\x1b[32m",
|
|
417
|
+
yellow: "\x1b[33m",
|
|
418
|
+
cyan: "\x1b[36m",
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const summary = this._app.getSummary();
|
|
422
|
+
const schedules = this._scheduler.list();
|
|
423
|
+
|
|
424
|
+
console.log(`
|
|
425
|
+
${c.cyan}${c.bright}
|
|
426
|
+
┌─────────────────────────────────────────────────┐
|
|
427
|
+
│ 0nMCP Application Server │
|
|
428
|
+
├─────────────────────────────────────────────────┤
|
|
429
|
+
│ │
|
|
430
|
+
│ App: ${summary.name.padEnd(38)}│
|
|
431
|
+
│ Version: ${summary.version.padEnd(38)}│
|
|
432
|
+
│ │
|
|
433
|
+
│ URL: http://${host}:${port}${" ".repeat(Math.max(0, 28 - host.length - String(port).length))}│
|
|
434
|
+
│ Health: http://${host}:${port}/health${" ".repeat(Math.max(0, 21 - host.length - String(port).length))}│
|
|
435
|
+
│ │
|
|
436
|
+
│ Connections: ${String(summary.connections).padEnd(33)}│
|
|
437
|
+
│ Workflows: ${String(summary.workflows).padEnd(33)}│
|
|
438
|
+
│ Operations: ${String(summary.operations).padEnd(33)}│
|
|
439
|
+
│ Endpoints: ${String(summary.endpoints).padEnd(33)}│
|
|
440
|
+
│ Automations: ${String(summary.automations).padEnd(33)}│
|
|
441
|
+
└─────────────────────────────────────────────────┘
|
|
442
|
+
${c.reset}`);
|
|
443
|
+
|
|
444
|
+
// List endpoints
|
|
445
|
+
if (Object.keys(endpoints).length > 0) {
|
|
446
|
+
console.log(`${c.bright}Endpoints:${c.reset}`);
|
|
447
|
+
for (const route of Object.keys(endpoints)) {
|
|
448
|
+
console.log(` ${c.green}▸${c.reset} ${route}`);
|
|
449
|
+
}
|
|
450
|
+
console.log();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// List automations
|
|
454
|
+
const webhooks = Object.entries(automations).filter(([, a]) => a.type === "webhook");
|
|
455
|
+
if (webhooks.length > 0) {
|
|
456
|
+
console.log(`${c.bright}Webhooks:${c.reset}`);
|
|
457
|
+
for (const [id, auto] of webhooks) {
|
|
458
|
+
console.log(` ${c.yellow}▸${c.reset} ${auto.config?.path || `/hooks/${id}`} → ${auto.workflow}`);
|
|
459
|
+
}
|
|
460
|
+
console.log();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (schedules.length > 0) {
|
|
464
|
+
console.log(`${c.bright}Schedules:${c.reset}`);
|
|
465
|
+
for (const job of schedules) {
|
|
466
|
+
console.log(` ${c.cyan}▸${c.reset} ${job.id}: ${job.expr} (next: ${job.nextRun.toISOString()})`);
|
|
467
|
+
}
|
|
468
|
+
console.log();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP -Engine: Application Runtime
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Runtime representation of a loaded .0n application.
|
|
5
|
+
// Provides accessor methods for all application sections.
|
|
6
|
+
//
|
|
7
|
+
// Patent Pending: US Provisional Patent Application #63/968,814
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
import { unsealPortable } from "./cipher-portable.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runtime representation of a loaded .0n application bundle.
|
|
14
|
+
*/
|
|
15
|
+
export class Application {
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} bundle -Parsed application bundle JSON
|
|
18
|
+
* @param {object} [options]
|
|
19
|
+
* @param {string} [options.passphrase] -Passphrase for decryption
|
|
20
|
+
*/
|
|
21
|
+
constructor(bundle, options = {}) {
|
|
22
|
+
if (!bundle.$0n || bundle.$0n.type !== "application") {
|
|
23
|
+
throw new Error(`Invalid application bundle: $0n.type must be "application", got "${bundle.$0n?.type}"`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this._bundle = bundle;
|
|
27
|
+
this._passphrase = options.passphrase || null;
|
|
28
|
+
this._decryptedConnections = null;
|
|
29
|
+
this._decryptedSecrets = null;
|
|
30
|
+
this._startedAt = new Date();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Application metadata */
|
|
34
|
+
get name() { return this._bundle.$0n.name || "Unnamed Application"; }
|
|
35
|
+
get version() { return this._bundle.$0n.version || "1.0.0"; }
|
|
36
|
+
get description() { return this._bundle.$0n.description || ""; }
|
|
37
|
+
get author() { return this._bundle.$0n.author || ""; }
|
|
38
|
+
get created() { return this._bundle.$0n.created; }
|
|
39
|
+
get updated() { return this._bundle.$0n.updated; }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get decrypted connections.
|
|
43
|
+
* @returns {Array<{ service: string, credentials: object }>}
|
|
44
|
+
*/
|
|
45
|
+
getConnections() {
|
|
46
|
+
if (this._decryptedConnections) return this._decryptedConnections;
|
|
47
|
+
|
|
48
|
+
const connections = [];
|
|
49
|
+
for (const conn of this._bundle.connections || []) {
|
|
50
|
+
let credentials;
|
|
51
|
+
|
|
52
|
+
if (conn.sealed && conn.vault?.data) {
|
|
53
|
+
if (!this._passphrase) {
|
|
54
|
+
throw new Error(`Connection ${conn.service} is sealed -passphrase required.`);
|
|
55
|
+
}
|
|
56
|
+
const decrypted = unsealPortable(conn.vault.data, this._passphrase);
|
|
57
|
+
credentials = JSON.parse(decrypted);
|
|
58
|
+
} else if (conn.credentials) {
|
|
59
|
+
credentials = conn.credentials;
|
|
60
|
+
} else {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
connections.push({
|
|
65
|
+
service: conn.service,
|
|
66
|
+
name: conn.name || conn.service,
|
|
67
|
+
auth_type: conn.auth_type || "api_key",
|
|
68
|
+
environment: conn.environment || "production",
|
|
69
|
+
credentials,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._decryptedConnections = connections;
|
|
74
|
+
return connections;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get a workflow definition by ID.
|
|
79
|
+
* @param {string} id
|
|
80
|
+
* @returns {object|undefined}
|
|
81
|
+
*/
|
|
82
|
+
getWorkflow(id) {
|
|
83
|
+
const workflows = this._bundle.workflows || {};
|
|
84
|
+
return workflows[id];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get all workflow definitions.
|
|
89
|
+
* @returns {Record<string, object>}
|
|
90
|
+
*/
|
|
91
|
+
getWorkflows() {
|
|
92
|
+
return this._bundle.workflows || {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get all operation definitions.
|
|
97
|
+
* @returns {Record<string, object>}
|
|
98
|
+
*/
|
|
99
|
+
getOperations() {
|
|
100
|
+
return this._bundle.operations || {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get all endpoint definitions.
|
|
105
|
+
* @returns {Record<string, object>}
|
|
106
|
+
*/
|
|
107
|
+
getEndpoints() {
|
|
108
|
+
return this._bundle.endpoints || {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get all automation definitions.
|
|
113
|
+
* @returns {Record<string, object>}
|
|
114
|
+
*/
|
|
115
|
+
getAutomations() {
|
|
116
|
+
return this._bundle.automations || {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get environment configuration with secrets decrypted.
|
|
121
|
+
* @returns {{ variables: object, secrets: object, settings: object, feature_flags: object }}
|
|
122
|
+
*/
|
|
123
|
+
getEnv() {
|
|
124
|
+
const env = this._bundle.environment || {};
|
|
125
|
+
const result = {
|
|
126
|
+
variables: { ...env.variables },
|
|
127
|
+
secrets: {},
|
|
128
|
+
settings: { ...env.settings },
|
|
129
|
+
feature_flags: { ...env.feature_flags },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Decrypt secrets
|
|
133
|
+
if (env.secrets) {
|
|
134
|
+
for (const [key, val] of Object.entries(env.secrets)) {
|
|
135
|
+
if (typeof val === "string" && val.length > 50 && this._passphrase) {
|
|
136
|
+
// Looks like sealed data -try to decrypt
|
|
137
|
+
try {
|
|
138
|
+
result.secrets[key] = unsealPortable(val, this._passphrase);
|
|
139
|
+
} catch {
|
|
140
|
+
result.secrets[key] = val; // Keep as-is if decryption fails
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
result.secrets[key] = val;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get a flat environment object for template resolution.
|
|
153
|
+
* Combines variables + decrypted secrets.
|
|
154
|
+
* @returns {object}
|
|
155
|
+
*/
|
|
156
|
+
getFlatEnv() {
|
|
157
|
+
const env = this.getEnv();
|
|
158
|
+
return {
|
|
159
|
+
...env.variables,
|
|
160
|
+
...env.secrets,
|
|
161
|
+
...env.settings,
|
|
162
|
+
...env.feature_flags,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get platform configurations.
|
|
168
|
+
* @returns {Record<string, object>}
|
|
169
|
+
*/
|
|
170
|
+
getPlatforms() {
|
|
171
|
+
return this._bundle.platforms || {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the manifest.
|
|
176
|
+
* @returns {object}
|
|
177
|
+
*/
|
|
178
|
+
getManifest() {
|
|
179
|
+
return this._bundle.manifest || {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get application summary for display.
|
|
184
|
+
* @returns {object}
|
|
185
|
+
*/
|
|
186
|
+
getSummary() {
|
|
187
|
+
const endpoints = this.getEndpoints();
|
|
188
|
+
const automations = this.getAutomations();
|
|
189
|
+
const workflows = this.getWorkflows();
|
|
190
|
+
const operations = this.getOperations();
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
name: this.name,
|
|
194
|
+
version: this.version,
|
|
195
|
+
description: this.description,
|
|
196
|
+
author: this.author,
|
|
197
|
+
connections: (this._bundle.connections || []).length,
|
|
198
|
+
workflows: Object.keys(workflows).length,
|
|
199
|
+
operations: Object.keys(operations).length,
|
|
200
|
+
endpoints: Object.keys(endpoints).length,
|
|
201
|
+
automations: Object.keys(automations).length,
|
|
202
|
+
uptime: Math.floor((Date.now() - this._startedAt.getTime()) / 1000),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
package/engine/bundler.js
CHANGED
|
@@ -198,6 +198,10 @@ export function openBundle(bundlePath, passphrase, options = {}) {
|
|
|
198
198
|
const raw = readFileSync(bundlePath, "utf-8");
|
|
199
199
|
const bundle = JSON.parse(raw);
|
|
200
200
|
|
|
201
|
+
if (bundle.$0n?.type === "application") {
|
|
202
|
+
throw new Error("This is a .0n application file, not a bundle. Use app_open instead.");
|
|
203
|
+
}
|
|
204
|
+
|
|
201
205
|
if (!bundle.$0n || bundle.$0n.type !== "bundle") {
|
|
202
206
|
throw new Error("Not a valid .0n bundle file.");
|
|
203
207
|
}
|
|
@@ -302,6 +306,11 @@ export function inspectBundle(bundlePath) {
|
|
|
302
306
|
const raw = readFileSync(bundlePath, "utf-8");
|
|
303
307
|
const bundle = JSON.parse(raw);
|
|
304
308
|
|
|
309
|
+
// Delegate application bundles
|
|
310
|
+
if (bundle.$0n?.type === "application") {
|
|
311
|
+
throw new Error("This is a .0n application file, not a bundle. Use app_inspect/app_open instead.");
|
|
312
|
+
}
|
|
313
|
+
|
|
305
314
|
if (!bundle.$0n || bundle.$0n.type !== "bundle") {
|
|
306
315
|
throw new Error("Not a valid .0n bundle file.");
|
|
307
316
|
}
|
|
@@ -338,6 +347,10 @@ export function verifyBundle(bundlePath, passphrase) {
|
|
|
338
347
|
const bundle = JSON.parse(raw);
|
|
339
348
|
const errors = [];
|
|
340
349
|
|
|
350
|
+
if (bundle.$0n?.type === "application") {
|
|
351
|
+
return { valid: false, errors: ["This is a .0n application file. Use app_validate instead."] };
|
|
352
|
+
}
|
|
353
|
+
|
|
341
354
|
if (!bundle.$0n || bundle.$0n.type !== "bundle") {
|
|
342
355
|
return { valid: false, errors: ["Not a valid .0n bundle file."] };
|
|
343
356
|
}
|