@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/proxy.ts CHANGED
@@ -1,6 +1,17 @@
1
- import { NextResponse, type NextRequest } from 'next/server';
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
- export function proxy(req: NextRequest) {
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 token check.
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
- // Check for NextAuth session cookie (browser login)
39
- const hasSession =
40
- req.cookies.has('authjs.session-token') ||
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
- // Check for Forge API token (Help AI / CLI tools)
48
- // Token obtained via POST /api/auth/verify with admin password
49
- const forgeToken = req.headers.get('x-forge-token') || req.cookies.get('forge-api-token')?.value;
50
- if (forgeToken) {
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
  };
@@ -32,51 +32,14 @@ function initSchema(db: Database.Database) {
32
32
  console.error('[db] Migration failed:', sql, e.message);
33
33
  }
34
34
  };
35
- migrate('ALTER TABLE tasks ADD COLUMN scheduled_at TEXT');
36
- migrate("ALTER TABLE tasks ADD COLUMN mode TEXT NOT NULL DEFAULT 'prompt'");
37
- migrate('ALTER TABLE tasks ADD COLUMN watch_config TEXT');
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() {