@aion0/forge 0.10.46 → 0.10.48
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/RELEASE_NOTES.md +5 -42
- package/app/api/auth/keys/[id]/route.ts +16 -0
- package/app/api/auth/keys/route.ts +36 -0
- package/app/api/mcp/route.ts +144 -0
- package/cli/key.ts +67 -0
- package/cli/mcp-install.ts +106 -0
- package/cli/mcp-proxy.ts +196 -0
- package/cli/mw.mjs +453 -35
- package/cli/mw.ts +26 -1
- package/components/Dashboard.tsx +25 -20
- package/components/SettingsModal.tsx +123 -0
- package/lib/api-auth.ts +50 -0
- package/lib/api-keys.ts +157 -0
- package/lib/jobs/store.ts +25 -0
- package/lib/projects.ts +79 -5
- package/lib/settings.ts +12 -2
- package/mcp/README.md +46 -0
- package/mcp/server.ts +30 -0
- package/mcp/tools/_shared.ts +103 -0
- package/mcp/tools/automation.ts +244 -0
- package/mcp/tools/connectors.ts +83 -0
- package/mcp/tools/help.ts +50 -0
- package/mcp/tools/index.ts +39 -0
- package/mcp/tools/integrations.ts +97 -0
- package/mcp/tools/logs.ts +57 -0
- package/mcp/tools/marketplace.ts +75 -0
- package/mcp/tools/observability.ts +96 -0
- package/mcp/tools/pipelines.ts +150 -0
- package/mcp/tools/projects.ts +54 -0
- package/mcp/tools/tasks.ts +93 -0
- package/mcp/tools/workspace.ts +94 -0
- package/package.json +1 -1
- package/proxy.ts +27 -16
- package/src/core/db/database.ts +50 -43
package/proxy.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { NextResponse
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from './lib/auth';
|
|
3
|
+
import { hasValidApiKey } from './lib/api-auth';
|
|
4
|
+
import { isValidToken } from './app/api/auth/verify/route';
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
// Node runtime: the auth() wrapper decodes the NextAuth session JWT with the
|
|
7
|
+
// AUTH_SECRET that the route layer also uses (auto-generated in-process when
|
|
8
|
+
// unset). That secret is NOT visible to the Edge runtime, and lib/auth pulls in
|
|
9
|
+
// node:crypto — so this proxy must run in Node. The wrapper populates
|
|
10
|
+
// req.auth with the VALIDATED session (null for a forged/unsigned cookie),
|
|
11
|
+
// replacing the old presence-only cookie check that let any cookie through.
|
|
12
|
+
// (Next 16 renamed middleware.ts → proxy.ts; the auth() handler is exported
|
|
13
|
+
// under the `proxy` name the new convention expects.)
|
|
14
|
+
export const proxy = auth((req) => {
|
|
4
15
|
// Skip auth entirely in dev mode
|
|
5
16
|
const isDev = process.env.NODE_ENV !== 'production' || process.env.FORGE_DEV === '1';
|
|
6
17
|
if (isDev) {
|
|
@@ -11,6 +22,8 @@ export function proxy(req: NextRequest) {
|
|
|
11
22
|
|
|
12
23
|
// Allow auth endpoints, version probe (used by IDE plugins to detect a live
|
|
13
24
|
// server before prompting for password), and static assets without login.
|
|
25
|
+
// These /api/auth/* routes self-validate any credential at the handler, so they
|
|
26
|
+
// need no edge check.
|
|
14
27
|
if (
|
|
15
28
|
pathname.startsWith('/login') ||
|
|
16
29
|
pathname.startsWith('/api/auth') ||
|
|
@@ -27,7 +40,7 @@ export function proxy(req: NextRequest) {
|
|
|
27
40
|
|
|
28
41
|
// /api/connector-tool — loopback-only, no auth (used by Forge-internal
|
|
29
42
|
// callers: pipelines via curl, jobs scheduler, CLI). Non-loopback hosts
|
|
30
|
-
// fall through to the normal
|
|
43
|
+
// fall through to the normal check.
|
|
31
44
|
if (pathname === '/api/connector-tool') {
|
|
32
45
|
const host = req.headers.get('host') || '';
|
|
33
46
|
if (host.startsWith('127.0.0.1:') || host.startsWith('localhost:')) {
|
|
@@ -35,21 +48,16 @@ export function proxy(req: NextRequest) {
|
|
|
35
48
|
}
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
req.cookies.has('__Secure-authjs.session-token');
|
|
42
|
-
|
|
43
|
-
if (hasSession) {
|
|
51
|
+
// Validated NextAuth session (browser login). req.auth is the decoded session
|
|
52
|
+
// or null — a forged/unsigned session-token cookie does not pass.
|
|
53
|
+
if (req.auth) {
|
|
44
54
|
return NextResponse.next();
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
// Token validation happens in API route layer via isValidToken()
|
|
52
|
-
// Middleware passes it through — only localhost can obtain tokens
|
|
57
|
+
// A valid API key or admin-minted token authorizes any route; a bare or forged
|
|
58
|
+
// credential does not. Both checks run because x-forge-token/forge-api-token can
|
|
59
|
+
// carry either kind. /api/mcp still re-validates in its own handler.
|
|
60
|
+
if (hasValidApiKey(req) || isValidToken(req)) {
|
|
53
61
|
return NextResponse.next();
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -57,8 +65,11 @@ export function proxy(req: NextRequest) {
|
|
|
57
65
|
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
|
58
66
|
}
|
|
59
67
|
return NextResponse.redirect(new URL('/login', req.url));
|
|
60
|
-
}
|
|
68
|
+
});
|
|
61
69
|
|
|
70
|
+
// Next.js 16: Proxy is always Node runtime — declaring `runtime: 'nodejs'`
|
|
71
|
+
// here errors out at build time (`Route segment config is not allowed in
|
|
72
|
+
// Proxy file`). Keep only the matcher.
|
|
62
73
|
export const config = {
|
|
63
74
|
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
64
75
|
};
|
package/src/core/db/database.ts
CHANGED
|
@@ -32,51 +32,14 @@ function initSchema(db: Database.Database) {
|
|
|
32
32
|
console.error('[db] Migration failed:', sql, e.message);
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
migrate("ALTER TABLE skills ADD COLUMN type TEXT NOT NULL DEFAULT 'skill'");
|
|
39
|
-
migrate('ALTER TABLE skills ADD COLUMN archive TEXT');
|
|
40
|
-
migrate("ALTER TABLE skills ADD COLUMN installed_version TEXT NOT NULL DEFAULT ''");
|
|
41
|
-
migrate('ALTER TABLE skills ADD COLUMN rating REAL DEFAULT 0');
|
|
42
|
-
migrate('ALTER TABLE skills ADD COLUMN deleted_remotely INTEGER NOT NULL DEFAULT 0');
|
|
43
|
-
// 'registry' (synced from forge-skills) vs 'local' (uploaded by user).
|
|
44
|
-
// Local skills are kept across syncs (they're not in the remote registry,
|
|
45
|
-
// so the deleted_remotely housekeeping would otherwise wipe them).
|
|
46
|
-
migrate("ALTER TABLE skills ADD COLUMN source TEXT NOT NULL DEFAULT 'registry'");
|
|
47
|
-
migrate('ALTER TABLE project_pipelines ADD COLUMN last_run_at TEXT');
|
|
48
|
-
migrate('ALTER TABLE pipeline_runs ADD COLUMN dedup_key TEXT');
|
|
49
|
-
migrate("ALTER TABLE tasks ADD COLUMN agent TEXT DEFAULT 'claude'");
|
|
50
|
-
// Recreate token_usage with day column (drop old version if schema changed)
|
|
35
|
+
|
|
36
|
+
// token_usage is RECREATED when its schema is stale (missing the `day` column).
|
|
37
|
+
// This must run BEFORE the CREATE block below so the CREATE rebuilds it.
|
|
51
38
|
try { db.exec("SELECT day FROM token_usage LIMIT 1"); } catch { try { db.exec("DROP TABLE IF EXISTS token_usage"); db.exec("DROP TABLE IF EXISTS usage_scan_state"); } catch {} }
|
|
52
|
-
// Unique index for dedup (only applies when dedup_key is NOT NULL)
|
|
53
|
-
try { db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_pipeline_runs_dedup ON pipeline_runs(project_path, workflow_name, dedup_key)'); } catch {}
|
|
54
|
-
// One-shot migration of old issue_autofix_processed → pipeline_runs.
|
|
55
|
-
// Previously this ran every startup (reading + re-inserting all rows on
|
|
56
|
-
// every cold worker boot — visible as `[db] Migrated N records …`).
|
|
57
|
-
// Now: skip entirely if the source table has zero rows OR if any rows
|
|
58
|
-
// have already been migrated (presence of issue: dedup keys is the
|
|
59
|
-
// tell — INSERT OR IGNORE already protects against dups, the loop
|
|
60
|
-
// itself was just wasted work).
|
|
61
|
-
try {
|
|
62
|
-
const alreadyMigrated = db.prepare("SELECT 1 FROM pipeline_runs WHERE dedup_key LIKE 'issue:%' LIMIT 1").get();
|
|
63
|
-
if (!alreadyMigrated) {
|
|
64
|
-
const old = db.prepare('SELECT * FROM issue_autofix_processed').all() as any[];
|
|
65
|
-
if (old.length > 0) {
|
|
66
|
-
const ins = db.prepare('INSERT OR IGNORE INTO pipeline_runs (id, project_path, workflow_name, pipeline_id, status, dedup_key, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
67
|
-
for (const r of old) {
|
|
68
|
-
ins.run(
|
|
69
|
-
r.pipeline_id?.slice(0, 8) || ('mig-' + r.issue_number),
|
|
70
|
-
r.project_path, 'issue-fix-and-review', r.pipeline_id || '',
|
|
71
|
-
r.status === 'processing' ? 'running' : (r.status || 'done'),
|
|
72
|
-
`issue:${r.issue_number}`, r.created_at || new Date().toISOString()
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
console.log(`[db] Migrated ${old.length} issue_autofix_processed records to pipeline_runs (one-shot)`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} catch {}
|
|
79
39
|
|
|
40
|
+
// Create all base tables FIRST so the ALTER / INDEX / one-shot steps below have
|
|
41
|
+
// tables to operate on. Previously the ALTERs ran before CREATE and failed
|
|
42
|
+
// ("no such table") on a FRESH db — leaving e.g. tasks.agent missing on first boot.
|
|
80
43
|
db.exec(`
|
|
81
44
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
82
45
|
id TEXT PRIMARY KEY,
|
|
@@ -268,6 +231,50 @@ function initSchema(db: Database.Database) {
|
|
|
268
231
|
last_scan TEXT NOT NULL DEFAULT (datetime('now'))
|
|
269
232
|
);
|
|
270
233
|
`);
|
|
234
|
+
|
|
235
|
+
// Column migrations — now that the tables above exist, add columns that
|
|
236
|
+
// postdate their original CREATE (duplicate-column errors are ignored).
|
|
237
|
+
migrate('ALTER TABLE tasks ADD COLUMN scheduled_at TEXT');
|
|
238
|
+
migrate("ALTER TABLE tasks ADD COLUMN mode TEXT NOT NULL DEFAULT 'prompt'");
|
|
239
|
+
migrate('ALTER TABLE tasks ADD COLUMN watch_config TEXT');
|
|
240
|
+
migrate("ALTER TABLE skills ADD COLUMN type TEXT NOT NULL DEFAULT 'skill'");
|
|
241
|
+
migrate('ALTER TABLE skills ADD COLUMN archive TEXT');
|
|
242
|
+
migrate("ALTER TABLE skills ADD COLUMN installed_version TEXT NOT NULL DEFAULT ''");
|
|
243
|
+
migrate('ALTER TABLE skills ADD COLUMN rating REAL DEFAULT 0');
|
|
244
|
+
migrate('ALTER TABLE skills ADD COLUMN deleted_remotely INTEGER NOT NULL DEFAULT 0');
|
|
245
|
+
// 'registry' (synced from forge-skills) vs 'local' (uploaded by user).
|
|
246
|
+
// Local skills are kept across syncs (they're not in the remote registry,
|
|
247
|
+
// so the deleted_remotely housekeeping would otherwise wipe them).
|
|
248
|
+
migrate("ALTER TABLE skills ADD COLUMN source TEXT NOT NULL DEFAULT 'registry'");
|
|
249
|
+
migrate('ALTER TABLE project_pipelines ADD COLUMN last_run_at TEXT');
|
|
250
|
+
migrate('ALTER TABLE pipeline_runs ADD COLUMN dedup_key TEXT');
|
|
251
|
+
migrate("ALTER TABLE tasks ADD COLUMN agent TEXT DEFAULT 'claude'");
|
|
252
|
+
|
|
253
|
+
// Unique index for dedup (needs pipeline_runs to exist; only applies when dedup_key is NOT NULL).
|
|
254
|
+
try { db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_pipeline_runs_dedup ON pipeline_runs(project_path, workflow_name, dedup_key)'); } catch {}
|
|
255
|
+
|
|
256
|
+
// One-shot migration of old issue_autofix_processed → pipeline_runs.
|
|
257
|
+
// Skip entirely if the source table has zero rows OR if any rows have already
|
|
258
|
+
// been migrated (presence of issue: dedup keys is the tell — INSERT OR IGNORE
|
|
259
|
+
// already protects against dups, the loop itself was just wasted work).
|
|
260
|
+
try {
|
|
261
|
+
const alreadyMigrated = db.prepare("SELECT 1 FROM pipeline_runs WHERE dedup_key LIKE 'issue:%' LIMIT 1").get();
|
|
262
|
+
if (!alreadyMigrated) {
|
|
263
|
+
const old = db.prepare('SELECT * FROM issue_autofix_processed').all() as any[];
|
|
264
|
+
if (old.length > 0) {
|
|
265
|
+
const ins = db.prepare('INSERT OR IGNORE INTO pipeline_runs (id, project_path, workflow_name, pipeline_id, status, dedup_key, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
266
|
+
for (const r of old) {
|
|
267
|
+
ins.run(
|
|
268
|
+
r.pipeline_id?.slice(0, 8) || ('mig-' + r.issue_number),
|
|
269
|
+
r.project_path, 'issue-fix-and-review', r.pipeline_id || '',
|
|
270
|
+
r.status === 'processing' ? 'running' : (r.status || 'done'),
|
|
271
|
+
`issue:${r.issue_number}`, r.created_at || new Date().toISOString()
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
console.log(`[db] Migrated ${old.length} issue_autofix_processed records to pipeline_runs (one-shot)`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
export function closeDb() {
|