@runtimescope/collector 0.9.3 → 0.10.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.
@@ -929,7 +929,7 @@ var CollectorServer = class {
929
929
  this.disconnectCallbacks.push(cb);
930
930
  }
931
931
  start(options = {}) {
932
- const port = options.port ?? 9090;
932
+ const port = options.port ?? 6767;
933
933
  const host = options.host ?? "127.0.0.1";
934
934
  const maxRetries = options.maxRetries ?? 5;
935
935
  const retryDelayMs = options.retryDelayMs ?? 1e3;
@@ -1090,7 +1090,15 @@ var CollectorServer = class {
1090
1090
  switch (msg.type) {
1091
1091
  case "handshake": {
1092
1092
  const payload = msg.payload;
1093
- if (this.authManager?.isEnabled()) {
1093
+ let workspaceFromKey = null;
1094
+ if (payload.authToken && this.pmStore?.getWorkspaceByApiKey) {
1095
+ try {
1096
+ const ws2 = this.pmStore.getWorkspaceByApiKey(payload.authToken);
1097
+ if (ws2) workspaceFromKey = { id: ws2.id, slug: ws2.slug };
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ if (this.authManager?.isEnabled() && !workspaceFromKey) {
1094
1102
  if (!this.authManager.isAuthorized(payload.authToken)) {
1095
1103
  try {
1096
1104
  ws.send(JSON.stringify({
@@ -1103,15 +1111,27 @@ var CollectorServer = class {
1103
1111
  ws.close(4001, "Authentication failed");
1104
1112
  return;
1105
1113
  }
1106
- this.pendingHandshakes.delete(ws);
1107
1114
  }
1115
+ this.pendingHandshakes.delete(ws);
1108
1116
  const projectName = payload.appName;
1109
1117
  const projectId = payload.projectId ?? (this.projectManager ? resolveProjectId(this.projectManager, projectName, this.pmStore) : void 0);
1110
1118
  this.clients.set(ws, {
1111
1119
  sessionId: payload.sessionId,
1112
1120
  projectName,
1113
- projectId
1121
+ projectId,
1122
+ workspaceId: workspaceFromKey?.id
1114
1123
  });
1124
+ if (workspaceFromKey && projectId && this.pmStore?.listProjects && this.pmStore.setProjectWorkspace) {
1125
+ try {
1126
+ const existing = this.pmStore.listProjects().find(
1127
+ (p) => p.runtimeProjectId === projectId
1128
+ );
1129
+ if (existing && !existing.workspaceId) {
1130
+ this.pmStore.setProjectWorkspace(existing.id, workspaceFromKey.id);
1131
+ }
1132
+ } catch {
1133
+ }
1134
+ }
1115
1135
  const sqliteStore = this.ensureSqliteStore(projectName);
1116
1136
  if (sqliteStore) {
1117
1137
  const sessionInfo = {
@@ -1273,9 +1293,9 @@ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as
1273
1293
  import { join } from "path";
1274
1294
  import { homedir } from "os";
1275
1295
  var DEFAULT_GLOBAL_CONFIG = {
1276
- defaultPort: 9090,
1296
+ defaultPort: 6767,
1277
1297
  bufferSize: 1e4,
1278
- httpPort: 9091
1298
+ httpPort: 6768
1279
1299
  };
1280
1300
  var ProjectManager = class {
1281
1301
  baseDir;
@@ -1547,7 +1567,7 @@ function scaffoldProjectConfig(projectDir, opts) {
1547
1567
  return existing;
1548
1568
  }
1549
1569
  const projectId = generateProjectId();
1550
- const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
1570
+ const httpPort = process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768";
1551
1571
  const dsn = `runtimescope://${projectId}@localhost:${httpPort}/${opts.appName}`;
1552
1572
  const config = {
1553
1573
  projectId,
@@ -2156,7 +2176,11 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2156
2176
  helpers.json(res, { ...project, stats });
2157
2177
  return;
2158
2178
  }
2159
- const projects = pmStore.listProjects();
2179
+ let projects = pmStore.listProjects();
2180
+ const workspaceId = params.get("workspace_id");
2181
+ if (workspaceId) {
2182
+ projects = projects.filter((p) => p.workspaceId === workspaceId);
2183
+ }
2160
2184
  helpers.json(res, { data: projects, count: projects.length });
2161
2185
  });
2162
2186
  route("GET", "/api/pm/projects/export-csv", (_req, res, params) => {
@@ -2264,6 +2288,142 @@ function createPmRouter(pmStore, discovery, helpers, broadcastDevServer) {
2264
2288
  pmStore.deleteProject(id);
2265
2289
  helpers.json(res, { ok: true, deleted: project.name });
2266
2290
  });
2291
+ route("PUT", "/api/pm/projects/:id/workspace", async (req, res, params) => {
2292
+ const id = params.get("id");
2293
+ const body = await helpers.readBody(req, 4096);
2294
+ if (!body) {
2295
+ helpers.json(res, { error: "Missing body" }, 400);
2296
+ return;
2297
+ }
2298
+ let parsed;
2299
+ try {
2300
+ parsed = JSON.parse(body);
2301
+ } catch {
2302
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2303
+ return;
2304
+ }
2305
+ if (!parsed.workspace_id) {
2306
+ helpers.json(res, { error: "Missing workspace_id" }, 400);
2307
+ return;
2308
+ }
2309
+ try {
2310
+ pmStore.setProjectWorkspace(id, parsed.workspace_id);
2311
+ helpers.json(res, { ok: true });
2312
+ } catch (err) {
2313
+ helpers.json(res, { error: err.message }, 400);
2314
+ }
2315
+ });
2316
+ route("GET", "/api/pm/workspaces", (_req, res) => {
2317
+ helpers.json(res, { data: pmStore.listWorkspaces() });
2318
+ });
2319
+ route("POST", "/api/pm/workspaces", async (req, res) => {
2320
+ const body = await helpers.readBody(req, 4096);
2321
+ if (!body) {
2322
+ helpers.json(res, { error: "Missing body" }, 400);
2323
+ return;
2324
+ }
2325
+ let parsed;
2326
+ try {
2327
+ parsed = JSON.parse(body);
2328
+ } catch {
2329
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2330
+ return;
2331
+ }
2332
+ if (!parsed.name || typeof parsed.name !== "string") {
2333
+ helpers.json(res, { error: "Missing name" }, 400);
2334
+ return;
2335
+ }
2336
+ try {
2337
+ const ws = pmStore.createWorkspace({ name: parsed.name, slug: parsed.slug, description: parsed.description });
2338
+ helpers.json(res, ws, 201);
2339
+ } catch (err) {
2340
+ helpers.json(res, { error: err.message }, 400);
2341
+ }
2342
+ });
2343
+ route("GET", "/api/pm/workspaces/:id", (_req, res, params) => {
2344
+ const id = params.get("id");
2345
+ const ws = pmStore.getWorkspace(id);
2346
+ if (!ws) {
2347
+ helpers.json(res, { error: "Workspace not found" }, 404);
2348
+ return;
2349
+ }
2350
+ helpers.json(res, ws);
2351
+ });
2352
+ route("PUT", "/api/pm/workspaces/:id", async (req, res, params) => {
2353
+ const id = params.get("id");
2354
+ if (!pmStore.getWorkspace(id)) {
2355
+ helpers.json(res, { error: "Workspace not found" }, 404);
2356
+ return;
2357
+ }
2358
+ const body = await helpers.readBody(req, 4096);
2359
+ if (!body) {
2360
+ helpers.json(res, { error: "Missing body" }, 400);
2361
+ return;
2362
+ }
2363
+ let parsed;
2364
+ try {
2365
+ parsed = JSON.parse(body);
2366
+ } catch {
2367
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2368
+ return;
2369
+ }
2370
+ try {
2371
+ pmStore.updateWorkspace(id, parsed);
2372
+ helpers.json(res, pmStore.getWorkspace(id));
2373
+ } catch (err) {
2374
+ helpers.json(res, { error: err.message }, 400);
2375
+ }
2376
+ });
2377
+ route("DELETE", "/api/pm/workspaces/:id", (_req, res, params) => {
2378
+ const id = params.get("id");
2379
+ try {
2380
+ pmStore.deleteWorkspace(id);
2381
+ helpers.json(res, { ok: true });
2382
+ } catch (err) {
2383
+ helpers.json(res, { error: err.message }, 400);
2384
+ }
2385
+ });
2386
+ route("GET", "/api/pm/workspaces/:id/api-keys", (_req, res, params) => {
2387
+ const id = params.get("id");
2388
+ if (!pmStore.getWorkspace(id)) {
2389
+ helpers.json(res, { error: "Workspace not found" }, 404);
2390
+ return;
2391
+ }
2392
+ helpers.json(res, { data: pmStore.listApiKeys(id) });
2393
+ });
2394
+ route("POST", "/api/pm/workspaces/:id/api-keys", async (req, res, params) => {
2395
+ const id = params.get("id");
2396
+ if (!pmStore.getWorkspace(id)) {
2397
+ helpers.json(res, { error: "Workspace not found" }, 404);
2398
+ return;
2399
+ }
2400
+ const body = await helpers.readBody(req, 4096);
2401
+ if (!body) {
2402
+ helpers.json(res, { error: "Missing body" }, 400);
2403
+ return;
2404
+ }
2405
+ let parsed;
2406
+ try {
2407
+ parsed = JSON.parse(body);
2408
+ } catch {
2409
+ helpers.json(res, { error: "Invalid JSON" }, 400);
2410
+ return;
2411
+ }
2412
+ if (!parsed.label) {
2413
+ helpers.json(res, { error: "Missing label" }, 400);
2414
+ return;
2415
+ }
2416
+ try {
2417
+ const key = pmStore.createApiKey(id, parsed.label, parsed.expires_at);
2418
+ helpers.json(res, key, 201);
2419
+ } catch (err) {
2420
+ helpers.json(res, { error: err.message }, 400);
2421
+ }
2422
+ });
2423
+ route("DELETE", "/api/pm/api-keys/:key", (_req, res, params) => {
2424
+ pmStore.revokeApiKey(params.get("key"));
2425
+ helpers.json(res, { ok: true });
2426
+ });
2267
2427
  route("GET", "/api/pm/tasks", (_req, res, params) => {
2268
2428
  const projectId = params.get("project_id") ?? void 0;
2269
2429
  const status = params.get("status") ?? void 0;
@@ -3164,6 +3324,16 @@ async function readRuleFile(filePath) {
3164
3324
  }
3165
3325
 
3166
3326
  // src/http-server.ts
3327
+ var COLLECTOR_VERSION = (() => {
3328
+ try {
3329
+ const here = dirname(fileURLToPath(import.meta.url));
3330
+ const pkgJson = readFileSync4(resolve(here, "..", "package.json"), "utf-8");
3331
+ const pkg = JSON.parse(pkgJson);
3332
+ return pkg.version ?? "unknown";
3333
+ } catch {
3334
+ return "unknown";
3335
+ }
3336
+ })();
3167
3337
  var HttpServer = class {
3168
3338
  server = null;
3169
3339
  wss = null;
@@ -3177,7 +3347,7 @@ var HttpServer = class {
3177
3347
  routes = /* @__PURE__ */ new Map();
3178
3348
  pmRouter = null;
3179
3349
  sdkBundlePath = null;
3180
- activePort = 9091;
3350
+ activePort = 6768;
3181
3351
  startedAt = Date.now();
3182
3352
  connectedSessionsGetter = null;
3183
3353
  pmStore = null;
@@ -3203,6 +3373,7 @@ var HttpServer = class {
3203
3373
  this.routes.set("GET /api/health", (_req, res) => {
3204
3374
  this.json(res, {
3205
3375
  status: "ok",
3376
+ version: COLLECTOR_VERSION,
3206
3377
  timestamp: Date.now(),
3207
3378
  uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
3208
3379
  sessions: this.store.getSessionInfo().filter((s) => s.isConnected).length,
@@ -3426,6 +3597,21 @@ var HttpServer = class {
3426
3597
  } catch {
3427
3598
  }
3428
3599
  }
3600
+ if (this.pmStore) {
3601
+ try {
3602
+ const token = AuthManager.extractBearer(req.headers.authorization);
3603
+ if (token) {
3604
+ const ws = this.pmStore.getWorkspaceByApiKey(token);
3605
+ if (ws && projectId) {
3606
+ const existing = this.pmStore.listProjects().find((p) => p.runtimeProjectId === projectId);
3607
+ if (existing && !existing.workspaceId) {
3608
+ this.pmStore.setProjectWorkspace(existing.id, ws.id);
3609
+ }
3610
+ }
3611
+ }
3612
+ } catch {
3613
+ }
3614
+ }
3429
3615
  }
3430
3616
  const VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
3431
3617
  "network",
@@ -3492,7 +3678,7 @@ var HttpServer = class {
3492
3678
  return null;
3493
3679
  }
3494
3680
  async start(options = {}) {
3495
- const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
3681
+ const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "6768", 10);
3496
3682
  const host = options.host ?? "127.0.0.1";
3497
3683
  const tls = options.tls;
3498
3684
  const maxRetries = 5;
@@ -3624,7 +3810,9 @@ var HttpServer = class {
3624
3810
  const isPublic = url.pathname === "/api/health" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet";
3625
3811
  if (!isPublic && this.authManager?.isEnabled()) {
3626
3812
  const token = AuthManager.extractBearer(req.headers.authorization);
3627
- if (!this.authManager.isAuthorized(token)) {
3813
+ const isGlobal = this.authManager.isAuthorized(token);
3814
+ const isWorkspaceToken = !!(token && this.pmStore?.getWorkspaceByApiKey(token));
3815
+ if (!isGlobal && !isWorkspaceToken) {
3628
3816
  this.json(res, { error: "Unauthorized", code: "AUTH_FAILED" }, 401);
3629
3817
  return;
3630
3818
  }
@@ -3756,6 +3944,7 @@ function numParam(params, key) {
3756
3944
 
3757
3945
  // src/pm/pm-store.ts
3758
3946
  import Database from "better-sqlite3";
3947
+ import { randomBytes as randomBytes3 } from "crypto";
3759
3948
  var PmStore = class {
3760
3949
  db;
3761
3950
  constructor(options) {
@@ -3888,6 +4077,30 @@ var PmStore = class {
3888
4077
  deleted_at INTEGER NOT NULL
3889
4078
  );
3890
4079
  CREATE INDEX IF NOT EXISTS idx_deleted_path ON pm_deleted_projects(path);
4080
+
4081
+ -- Multi-tenant workspaces (Phase 1) --
4082
+ CREATE TABLE IF NOT EXISTS pm_workspaces (
4083
+ id TEXT PRIMARY KEY,
4084
+ name TEXT NOT NULL,
4085
+ slug TEXT UNIQUE NOT NULL,
4086
+ description TEXT,
4087
+ is_default INTEGER NOT NULL DEFAULT 0,
4088
+ created_at INTEGER NOT NULL,
4089
+ updated_at INTEGER NOT NULL
4090
+ );
4091
+ CREATE INDEX IF NOT EXISTS idx_workspaces_slug ON pm_workspaces(slug);
4092
+
4093
+ CREATE TABLE IF NOT EXISTS pm_api_keys (
4094
+ key TEXT PRIMARY KEY,
4095
+ workspace_id TEXT NOT NULL,
4096
+ label TEXT NOT NULL,
4097
+ created_at INTEGER NOT NULL,
4098
+ last_used_at INTEGER,
4099
+ expires_at INTEGER,
4100
+ revoked_at INTEGER,
4101
+ FOREIGN KEY (workspace_id) REFERENCES pm_workspaces(id) ON DELETE CASCADE
4102
+ );
4103
+ CREATE INDEX IF NOT EXISTS idx_api_keys_workspace ON pm_api_keys(workspace_id);
3891
4104
  `);
3892
4105
  }
3893
4106
  runMigrations() {
@@ -3907,18 +4120,45 @@ var PmStore = class {
3907
4120
  this.db.exec("ALTER TABLE pm_projects ADD COLUMN runtime_project_id TEXT DEFAULT NULL");
3908
4121
  } catch {
3909
4122
  }
4123
+ try {
4124
+ this.db.exec("ALTER TABLE pm_projects ADD COLUMN workspace_id TEXT DEFAULT NULL");
4125
+ } catch {
4126
+ }
4127
+ this.ensureDefaultWorkspace();
4128
+ }
4129
+ /**
4130
+ * Ensure a default "personal" workspace exists and every project has a
4131
+ * workspace_id. Runs on every startup — idempotent.
4132
+ */
4133
+ ensureDefaultWorkspace() {
4134
+ const existing = this.db.prepare("SELECT id FROM pm_workspaces WHERE is_default = 1").get();
4135
+ let defaultId;
4136
+ if (existing) {
4137
+ defaultId = existing.id;
4138
+ } else {
4139
+ defaultId = generateWorkspaceId();
4140
+ const now = Date.now();
4141
+ this.db.prepare(
4142
+ `INSERT INTO pm_workspaces (id, name, slug, description, is_default, created_at, updated_at)
4143
+ VALUES (?, ?, ?, ?, 1, ?, ?)`
4144
+ ).run(defaultId, "Personal", "personal", "Your personal workspace", now, now);
4145
+ }
4146
+ this.db.prepare("UPDATE pm_projects SET workspace_id = ? WHERE workspace_id IS NULL").run(defaultId);
3910
4147
  }
3911
4148
  // ============================================================
3912
4149
  // Projects
3913
4150
  // ============================================================
3914
4151
  upsertProject(project) {
4152
+ const workspaceId = project.workspaceId ?? this.db.prepare("SELECT id FROM pm_workspaces WHERE is_default = 1 LIMIT 1").get();
4153
+ const resolvedWorkspaceId = typeof workspaceId === "string" ? workspaceId : workspaceId?.id ?? null;
3915
4154
  this.db.prepare(`
3916
- INSERT INTO pm_projects (id, name, path, claude_project_key, runtimescope_project,
4155
+ INSERT INTO pm_projects (id, workspace_id, name, path, claude_project_key, runtimescope_project,
3917
4156
  phase, management_authorized, probable_to_complete, project_status,
3918
4157
  category, sdk_installed, runtime_apps,
3919
4158
  created_at, updated_at, metadata)
3920
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4159
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3921
4160
  ON CONFLICT(id) DO UPDATE SET
4161
+ workspace_id = COALESCE(pm_projects.workspace_id, excluded.workspace_id),
3922
4162
  name = excluded.name,
3923
4163
  path = COALESCE(excluded.path, pm_projects.path),
3924
4164
  claude_project_key = COALESCE(excluded.claude_project_key, pm_projects.claude_project_key),
@@ -3929,6 +4169,7 @@ var PmStore = class {
3929
4169
  metadata = COALESCE(excluded.metadata, pm_projects.metadata)
3930
4170
  `).run(
3931
4171
  project.id,
4172
+ resolvedWorkspaceId,
3932
4173
  project.name,
3933
4174
  project.path ?? null,
3934
4175
  project.claudeProjectKey ?? null,
@@ -4064,9 +4305,123 @@ var PmStore = class {
4064
4305
  const rows = this.db.prepare("SELECT DISTINCT category FROM pm_projects WHERE category IS NOT NULL ORDER BY category ASC").all();
4065
4306
  return rows.map((r) => r.category);
4066
4307
  }
4308
+ // ============================================================
4309
+ // Workspaces (multi-tenant)
4310
+ // ============================================================
4311
+ listWorkspaces() {
4312
+ const rows = this.db.prepare("SELECT * FROM pm_workspaces ORDER BY is_default DESC, name ASC").all();
4313
+ return rows.map(mapWorkspaceRow);
4314
+ }
4315
+ getWorkspace(id) {
4316
+ const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE id = ?").get(id);
4317
+ return row ? mapWorkspaceRow(row) : null;
4318
+ }
4319
+ getWorkspaceBySlug(slug) {
4320
+ const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE slug = ?").get(slug);
4321
+ return row ? mapWorkspaceRow(row) : null;
4322
+ }
4323
+ getDefaultWorkspace() {
4324
+ const row = this.db.prepare("SELECT * FROM pm_workspaces WHERE is_default = 1 LIMIT 1").get();
4325
+ if (!row) {
4326
+ throw new Error("Default workspace missing \u2014 ensureDefaultWorkspace() must run first");
4327
+ }
4328
+ return mapWorkspaceRow(row);
4329
+ }
4330
+ createWorkspace(input) {
4331
+ const id = generateWorkspaceId();
4332
+ const slug = (input.slug ?? input.name).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4333
+ if (!slug) {
4334
+ throw new Error("Workspace slug cannot be empty");
4335
+ }
4336
+ if (this.getWorkspaceBySlug(slug)) {
4337
+ throw new Error(`Workspace with slug "${slug}" already exists`);
4338
+ }
4339
+ const now = Date.now();
4340
+ this.db.prepare(
4341
+ `INSERT INTO pm_workspaces (id, name, slug, description, is_default, created_at, updated_at)
4342
+ VALUES (?, ?, ?, ?, 0, ?, ?)`
4343
+ ).run(id, input.name, slug, input.description ?? null, now, now);
4344
+ return { id, name: input.name, slug, description: input.description, createdAt: now, updatedAt: now, isDefault: false };
4345
+ }
4346
+ updateWorkspace(id, updates) {
4347
+ const sets = [];
4348
+ const params = [];
4349
+ if (updates.name !== void 0) {
4350
+ sets.push("name = ?");
4351
+ params.push(updates.name);
4352
+ }
4353
+ if (updates.slug !== void 0) {
4354
+ sets.push("slug = ?");
4355
+ params.push(updates.slug);
4356
+ }
4357
+ if (updates.description !== void 0) {
4358
+ sets.push("description = ?");
4359
+ params.push(updates.description);
4360
+ }
4361
+ if (!sets.length) return;
4362
+ sets.push("updated_at = ?");
4363
+ params.push(Date.now());
4364
+ params.push(id);
4365
+ this.db.prepare(`UPDATE pm_workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
4366
+ }
4367
+ deleteWorkspace(id) {
4368
+ const ws = this.getWorkspace(id);
4369
+ if (!ws) return;
4370
+ if (ws.isDefault) {
4371
+ throw new Error("Cannot delete the default workspace");
4372
+ }
4373
+ const def = this.getDefaultWorkspace();
4374
+ this.db.prepare("UPDATE pm_projects SET workspace_id = ? WHERE workspace_id = ?").run(def.id, id);
4375
+ this.db.prepare("DELETE FROM pm_api_keys WHERE workspace_id = ?").run(id);
4376
+ this.db.prepare("DELETE FROM pm_workspaces WHERE id = ?").run(id);
4377
+ }
4378
+ /** Move a project between workspaces. */
4379
+ setProjectWorkspace(projectId, workspaceId) {
4380
+ const ws = this.getWorkspace(workspaceId);
4381
+ if (!ws) throw new Error(`Workspace ${workspaceId} does not exist`);
4382
+ this.db.prepare("UPDATE pm_projects SET workspace_id = ?, updated_at = ? WHERE id = ?").run(workspaceId, Date.now(), projectId);
4383
+ }
4384
+ // ============================================================
4385
+ // API Keys (workspace-scoped)
4386
+ // ============================================================
4387
+ createApiKey(workspaceId, label, expiresAt) {
4388
+ const ws = this.getWorkspace(workspaceId);
4389
+ if (!ws) throw new Error(`Workspace ${workspaceId} does not exist`);
4390
+ const key = `tk_${randomBytes3(24).toString("hex")}`;
4391
+ const now = Date.now();
4392
+ this.db.prepare(
4393
+ `INSERT INTO pm_api_keys (key, workspace_id, label, created_at, expires_at)
4394
+ VALUES (?, ?, ?, ?, ?)`
4395
+ ).run(key, workspaceId, label, now, expiresAt ?? null);
4396
+ return { key, workspaceId, label, createdAt: now, expiresAt };
4397
+ }
4398
+ listApiKeys(workspaceId) {
4399
+ const rows = this.db.prepare(
4400
+ `SELECT * FROM pm_api_keys WHERE workspace_id = ? AND revoked_at IS NULL ORDER BY created_at DESC`
4401
+ ).all(workspaceId);
4402
+ return rows.map(mapApiKeyRow);
4403
+ }
4404
+ revokeApiKey(key) {
4405
+ this.db.prepare("UPDATE pm_api_keys SET revoked_at = ? WHERE key = ?").run(Date.now(), key);
4406
+ }
4407
+ getWorkspaceByApiKey(key) {
4408
+ const row = this.db.prepare(
4409
+ `SELECT w.* FROM pm_api_keys k
4410
+ JOIN pm_workspaces w ON w.id = k.workspace_id
4411
+ WHERE k.key = ? AND k.revoked_at IS NULL
4412
+ AND (k.expires_at IS NULL OR k.expires_at > ?)`
4413
+ ).get(key, Date.now());
4414
+ if (!row) return null;
4415
+ try {
4416
+ this.db.prepare("UPDATE pm_api_keys SET last_used_at = ? WHERE key = ?").run(Date.now(), key);
4417
+ } catch {
4418
+ }
4419
+ return mapWorkspaceRow(row);
4420
+ }
4067
4421
  mapProjectRow(row) {
4068
4422
  return {
4069
4423
  id: row.id,
4424
+ workspaceId: row.workspace_id ?? void 0,
4070
4425
  name: row.name,
4071
4426
  path: row.path ?? void 0,
4072
4427
  claudeProjectKey: row.claude_project_key ?? void 0,
@@ -4857,6 +5212,31 @@ var PmStore = class {
4857
5212
  this.db.close();
4858
5213
  }
4859
5214
  };
5215
+ function generateWorkspaceId() {
5216
+ return `ws_${randomBytes3(8).toString("hex")}`;
5217
+ }
5218
+ function mapWorkspaceRow(row) {
5219
+ return {
5220
+ id: row.id,
5221
+ name: row.name,
5222
+ slug: row.slug,
5223
+ description: row.description ?? void 0,
5224
+ isDefault: row.is_default === 1,
5225
+ createdAt: row.created_at,
5226
+ updatedAt: row.updated_at
5227
+ };
5228
+ }
5229
+ function mapApiKeyRow(row) {
5230
+ return {
5231
+ key: row.key,
5232
+ workspaceId: row.workspace_id,
5233
+ label: row.label,
5234
+ createdAt: row.created_at,
5235
+ lastUsedAt: row.last_used_at ?? void 0,
5236
+ expiresAt: row.expires_at ?? void 0,
5237
+ revokedAt: row.revoked_at ?? void 0
5238
+ };
5239
+ }
4860
5240
 
4861
5241
  // src/pm/session-parser.ts
4862
5242
  import { createReadStream } from "fs";
@@ -5665,4 +6045,4 @@ export {
5665
6045
  parseSessionJsonl,
5666
6046
  ProjectDiscovery
5667
6047
  };
5668
- //# sourceMappingURL=chunk-MM44DN7Y.js.map
6048
+ //# sourceMappingURL=chunk-WWFIEANS.js.map