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.
@@ -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
  }