@ai-sdk/devtools 1.0.0-canary.29 → 1.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/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  // src/db.ts
2
2
  import path from "path";
3
3
  import fs from "fs";
4
- var DB_DIR = path.join(process.cwd(), ".devtools");
5
- var DB_PATH = path.join(DB_DIR, "generations.json");
4
+ var DEVTOOLS_DB_DIR = ".devtools";
5
+ var DEVTOOLS_DB_FILE = "generations.json";
6
+ var MAX_DB_BYTES = 100 * 1024 * 1024;
7
+ var DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
8
+ var DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
6
9
  var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
7
10
  var notifyServer = (event) => {
8
11
  notifyServerAsync(event);
@@ -2,7 +2,6 @@
2
2
  import { serve } from "@hono/node-server";
3
3
  import { serveStatic } from "@hono/node-server/serve-static";
4
4
  import { Hono } from "hono";
5
- import { cors } from "hono/cors";
6
5
  import { streamSSE } from "hono/streaming";
7
6
  import path2 from "path";
8
7
  import fs2 from "fs";
@@ -11,8 +10,11 @@ import { fileURLToPath } from "url";
11
10
  // src/db.ts
12
11
  import path from "path";
13
12
  import fs from "fs";
14
- var DB_DIR = path.join(process.cwd(), ".devtools");
15
- var DB_PATH = path.join(DB_DIR, "generations.json");
13
+ var DEVTOOLS_DB_DIR = ".devtools";
14
+ var DEVTOOLS_DB_FILE = "generations.json";
15
+ var MAX_DB_BYTES = 100 * 1024 * 1024;
16
+ var DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
17
+ var DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
16
18
  var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
17
19
  var notifyServer = (event) => {
18
20
  notifyServerAsync(event);
@@ -74,14 +76,40 @@ var saveDb = (db) => {
74
76
  dbCache = db;
75
77
  writeDb(db);
76
78
  };
79
+ var normalizeDevtoolsDbPath = (dbPath) => {
80
+ const resolvedPath = path.resolve(dbPath);
81
+ const dbDir = path.dirname(resolvedPath);
82
+ if (path.basename(resolvedPath) !== DEVTOOLS_DB_FILE || path.basename(dbDir) !== DEVTOOLS_DB_DIR) {
83
+ return void 0;
84
+ }
85
+ return resolvedPath;
86
+ };
87
+ var validateRemoteDbPath = (dbPath) => {
88
+ if (typeof dbPath !== "string") {
89
+ return void 0;
90
+ }
91
+ const normalizedPath = normalizeDevtoolsDbPath(dbPath);
92
+ if (!normalizedPath) {
93
+ return void 0;
94
+ }
95
+ try {
96
+ const stats = fs.statSync(normalizedPath);
97
+ if (!stats.isFile() || stats.size > MAX_DB_BYTES) {
98
+ return void 0;
99
+ }
100
+ const realPath = fs.realpathSync(normalizedPath);
101
+ return normalizeDevtoolsDbPath(realPath);
102
+ } catch {
103
+ return void 0;
104
+ }
105
+ };
77
106
  var reloadDb = async (remoteDbPath2) => {
78
- if (remoteDbPath2) {
107
+ const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath2);
108
+ if (validatedRemoteDbPath) {
79
109
  try {
80
- if (fs.existsSync(remoteDbPath2)) {
81
- const content = fs.readFileSync(remoteDbPath2, "utf-8");
82
- dbCache = JSON.parse(content);
83
- return;
84
- }
110
+ const content = fs.readFileSync(validatedRemoteDbPath, "utf-8");
111
+ dbCache = JSON.parse(content);
112
+ return;
85
113
  } catch {
86
114
  }
87
115
  }
@@ -131,8 +159,32 @@ var isDevMode = devEnv !== void 0 && devEnv !== "false" && devEnv !== "0";
131
159
  var projectRoot = path2.resolve(__dirname, "../..");
132
160
  var clientDir = path2.join(projectRoot, "dist/client");
133
161
  var remoteDbPath;
162
+ var viewerPort = 4983;
134
163
  var app = new Hono();
135
- app.use("/*", cors());
164
+ var getAllowedHosts = () => /* @__PURE__ */ new Set([
165
+ `localhost:${viewerPort}`,
166
+ `127.0.0.1:${viewerPort}`,
167
+ `[::1]:${viewerPort}`
168
+ ]);
169
+ var getAllowedOrigins = () => /* @__PURE__ */ new Set([
170
+ `http://localhost:${viewerPort}`,
171
+ `http://127.0.0.1:${viewerPort}`,
172
+ `http://[::1]:${viewerPort}`,
173
+ "http://localhost:5173",
174
+ "http://127.0.0.1:5173",
175
+ "http://[::1]:5173"
176
+ ]);
177
+ app.use("/api/*", async (c, next) => {
178
+ const host = c.req.header("host");
179
+ if (!host || !getAllowedHosts().has(host)) {
180
+ return c.text("Forbidden", 403);
181
+ }
182
+ const origin = c.req.header("origin");
183
+ if (origin && !getAllowedOrigins().has(origin)) {
184
+ return c.text("Forbidden", 403);
185
+ }
186
+ await next();
187
+ });
136
188
  app.get("/api/runs", async (c) => {
137
189
  await reloadDb(remoteDbPath);
138
190
  const runs = await getRuns();
@@ -258,8 +310,12 @@ app.get("/api/events", (c) => {
258
310
  });
259
311
  app.post("/api/notify", async (c) => {
260
312
  const body = await c.req.json();
261
- if (body.dbPath) {
262
- remoteDbPath = body.dbPath;
313
+ if (body.dbPath !== void 0) {
314
+ const validatedDbPath = validateRemoteDbPath(body.dbPath);
315
+ if (!validatedDbPath) {
316
+ return c.text("Invalid dbPath", 400);
317
+ }
318
+ remoteDbPath = validatedDbPath;
263
319
  }
264
320
  await reloadDb(remoteDbPath);
265
321
  broadcastToClients("update", body);
@@ -306,10 +362,12 @@ app.get("*", async (c) => {
306
362
  }
307
363
  });
308
364
  var startViewer = (port = 4983) => {
365
+ viewerPort = port;
309
366
  const server = serve(
310
367
  {
311
368
  fetch: app.fetch,
312
- port
369
+ port,
370
+ hostname: "localhost"
313
371
  },
314
372
  () => {
315
373
  if (isDevMode) {
@@ -346,5 +404,6 @@ if (isDirectRun) {
346
404
  startViewer(port);
347
405
  }
348
406
  export {
407
+ app,
349
408
  startViewer
350
409
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/devtools",
3
- "version": "1.0.0-canary.29",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -25,9 +25,9 @@
25
25
  "bin"
26
26
  ],
27
27
  "dependencies": {
28
- "@hono/node-server": "^1.19.14",
29
- "hono": "^4.12.18",
30
- "@ai-sdk/provider": "4.0.0-canary.18"
28
+ "@hono/node-server": "^1.19.13",
29
+ "hono": "^4.12.25",
30
+ "@ai-sdk/provider": "4.0.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@radix-ui/react-collapsible": "^1.1.12",
@@ -53,10 +53,10 @@
53
53
  "tw-animate-css": "^1.4.0",
54
54
  "typescript": "5.8.3",
55
55
  "vaul": "^1.1.2",
56
- "vite": "^6.4.2",
56
+ "vite": "^6.4.3",
57
57
  "vitest": "^4.1.6",
58
58
  "zod": "3.25.76",
59
- "ai": "7.0.0-canary.167"
59
+ "ai": "7.0.0"
60
60
  },
61
61
  "publishConfig": {
62
62
  "access": "public",
package/src/db.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
 
4
- const DB_DIR = path.join(process.cwd(), '.devtools');
5
- const DB_PATH = path.join(DB_DIR, 'generations.json');
4
+ const DEVTOOLS_DB_DIR = '.devtools';
5
+ const DEVTOOLS_DB_FILE = 'generations.json';
6
+ // Cap how many bytes we read from a remote database file. Guards against a
7
+ // synchronous hang / OOM if the file is enormous.
8
+ const MAX_DB_BYTES = 100 * 1024 * 1024; // 100 MB
9
+ const DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
10
+ const DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
6
11
  const DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT
7
12
  ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT)
8
13
  : 4983;
@@ -140,21 +145,57 @@ const saveDb = (db: Database): void => {
140
145
  writeDb(db);
141
146
  };
142
147
 
148
+ const normalizeDevtoolsDbPath = (dbPath: string): string | undefined => {
149
+ const resolvedPath = path.resolve(dbPath);
150
+ const dbDir = path.dirname(resolvedPath);
151
+
152
+ if (
153
+ path.basename(resolvedPath) !== DEVTOOLS_DB_FILE ||
154
+ path.basename(dbDir) !== DEVTOOLS_DB_DIR
155
+ ) {
156
+ return undefined;
157
+ }
158
+
159
+ return resolvedPath;
160
+ };
161
+
162
+ export const validateRemoteDbPath = (dbPath: unknown): string | undefined => {
163
+ if (typeof dbPath !== 'string') {
164
+ return undefined;
165
+ }
166
+
167
+ const normalizedPath = normalizeDevtoolsDbPath(dbPath);
168
+ if (!normalizedPath) {
169
+ return undefined;
170
+ }
171
+
172
+ try {
173
+ const stats = fs.statSync(normalizedPath);
174
+ if (!stats.isFile() || stats.size > MAX_DB_BYTES) {
175
+ return undefined;
176
+ }
177
+
178
+ const realPath = fs.realpathSync(normalizedPath);
179
+ return normalizeDevtoolsDbPath(realPath);
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ };
184
+
143
185
  /**
144
186
  * Reload the database from disk.
145
187
  * Used by the viewer server to pick up changes made by the middleware/integration.
146
- * When a remote dbPath is provided (from a notify POST), reads from that path
147
- * instead of the local CWD-based path, so the viewer works regardless of where
148
- * it was started.
188
+ * When a valid remote dbPath is provided (from a notify POST), reads from that
189
+ * path instead of the local CWD-based path, so the viewer works regardless of
190
+ * where it was started.
149
191
  */
150
192
  export const reloadDb = async (remoteDbPath?: string): Promise<void> => {
151
- if (remoteDbPath) {
193
+ const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath);
194
+ if (validatedRemoteDbPath) {
152
195
  try {
153
- if (fs.existsSync(remoteDbPath)) {
154
- const content = fs.readFileSync(remoteDbPath, 'utf-8');
155
- dbCache = JSON.parse(content);
156
- return;
157
- }
196
+ const content = fs.readFileSync(validatedRemoteDbPath, 'utf-8');
197
+ dbCache = JSON.parse(content);
198
+ return;
158
199
  } catch {
159
200
  // Fall through to default
160
201
  }
@@ -1,7 +1,6 @@
1
1
  import { serve } from '@hono/node-server';
2
2
  import { serveStatic } from '@hono/node-server/serve-static';
3
3
  import { Hono } from 'hono';
4
- import { cors } from 'hono/cors';
5
4
  import { streamSSE } from 'hono/streaming';
6
5
  import path from 'path';
7
6
  import fs from 'fs';
@@ -12,6 +11,7 @@ import {
12
11
  getStepsForRun,
13
12
  clearDatabase,
14
13
  reloadDb,
14
+ validateRemoteDbPath,
15
15
  } from '../db.js';
16
16
 
17
17
  // SSE client management
@@ -56,11 +56,40 @@ const clientDir = path.join(projectRoot, 'dist/client');
56
56
  // Track the DB path from the most recent notify call so all reads
57
57
  // use the correct file, even when the viewer runs in a different CWD.
58
58
  let remoteDbPath: string | undefined;
59
+ let viewerPort = 4983;
60
+
61
+ export const app = new Hono();
62
+
63
+ const getAllowedHosts = () =>
64
+ new Set([
65
+ `localhost:${viewerPort}`,
66
+ `127.0.0.1:${viewerPort}`,
67
+ `[::1]:${viewerPort}`,
68
+ ]);
69
+
70
+ const getAllowedOrigins = () =>
71
+ new Set([
72
+ `http://localhost:${viewerPort}`,
73
+ `http://127.0.0.1:${viewerPort}`,
74
+ `http://[::1]:${viewerPort}`,
75
+ 'http://localhost:5173',
76
+ 'http://127.0.0.1:5173',
77
+ 'http://[::1]:5173',
78
+ ]);
79
+
80
+ app.use('/api/*', async (c, next) => {
81
+ const host = c.req.header('host');
82
+ if (!host || !getAllowedHosts().has(host)) {
83
+ return c.text('Forbidden', 403);
84
+ }
59
85
 
60
- const app = new Hono();
86
+ const origin = c.req.header('origin');
87
+ if (origin && !getAllowedOrigins().has(origin)) {
88
+ return c.text('Forbidden', 403);
89
+ }
61
90
 
62
- // Enable CORS for development
63
- app.use('/*', cors());
91
+ await next();
92
+ });
64
93
 
65
94
  // API Routes
66
95
  app.get('/api/runs', async c => {
@@ -230,9 +259,13 @@ app.get('/api/events', c => {
230
259
 
231
260
  // Notification endpoint (called by middleware/integration)
232
261
  app.post('/api/notify', async c => {
233
- const body = await c.req.json();
234
- if (body.dbPath) {
235
- remoteDbPath = body.dbPath;
262
+ const body = (await c.req.json()) as Record<string, unknown>;
263
+ if (body.dbPath !== undefined) {
264
+ const validatedDbPath = validateRemoteDbPath(body.dbPath);
265
+ if (!validatedDbPath) {
266
+ return c.text('Invalid dbPath', 400);
267
+ }
268
+ remoteDbPath = validatedDbPath;
236
269
  }
237
270
  // Reload database from disk, using the remote dbPath if provided so the
238
271
  // viewer works even when started from a different directory than the app.
@@ -288,10 +321,13 @@ app.get('*', async c => {
288
321
  });
289
322
 
290
323
  export const startViewer = (port = 4983) => {
324
+ viewerPort = port;
325
+
291
326
  const server = serve(
292
327
  {
293
328
  fetch: app.fetch,
294
329
  port,
330
+ hostname: 'localhost',
295
331
  },
296
332
  () => {
297
333
  if (isDevMode) {