@hasna/todos 0.4.1 → 0.5.1

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/cli/index.js CHANGED
@@ -2319,6 +2319,19 @@ var init_database = __esm(() => {
2319
2319
  ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
2320
2320
  CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
2321
2321
  INSERT OR IGNORE INTO _migrations (id) VALUES (4);
2322
+ `,
2323
+ `
2324
+ CREATE TABLE IF NOT EXISTS api_keys (
2325
+ id TEXT PRIMARY KEY,
2326
+ name TEXT NOT NULL,
2327
+ key_hash TEXT NOT NULL UNIQUE,
2328
+ key_prefix TEXT NOT NULL,
2329
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2330
+ last_used_at TEXT,
2331
+ expires_at TEXT
2332
+ );
2333
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
2334
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
2322
2335
  `
2323
2336
  ];
2324
2337
  });
@@ -8015,6 +8028,60 @@ ${text}` }] };
8015
8028
  });
8016
8029
  });
8017
8030
 
8031
+ // src/db/api-keys.ts
8032
+ function generateApiKey() {
8033
+ const bytes = new Uint8Array(32);
8034
+ crypto.getRandomValues(bytes);
8035
+ return "td_" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
8036
+ }
8037
+ async function hashKey(key) {
8038
+ const encoder = new TextEncoder;
8039
+ const data = encoder.encode(key);
8040
+ const hash = await crypto.subtle.digest("SHA-256", data);
8041
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
8042
+ }
8043
+ async function createApiKey(input, db) {
8044
+ const d = db || getDatabase();
8045
+ const id = uuid();
8046
+ const timestamp = now();
8047
+ const key = generateApiKey();
8048
+ const keyHash = await hashKey(key);
8049
+ const keyPrefix = key.slice(0, 10) + "...";
8050
+ d.run(`INSERT INTO api_keys (id, name, key_hash, key_prefix, created_at, expires_at)
8051
+ VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, keyHash, keyPrefix, timestamp, input.expires_at || null]);
8052
+ const row = d.query("SELECT id, name, key_prefix, created_at, last_used_at, expires_at FROM api_keys WHERE id = ?").get(id);
8053
+ return { ...row, key };
8054
+ }
8055
+ function listApiKeys(db) {
8056
+ const d = db || getDatabase();
8057
+ return d.query("SELECT id, name, key_prefix, created_at, last_used_at, expires_at FROM api_keys ORDER BY created_at DESC").all();
8058
+ }
8059
+ function deleteApiKey(id, db) {
8060
+ const d = db || getDatabase();
8061
+ const result = d.run("DELETE FROM api_keys WHERE id = ?", [id]);
8062
+ return result.changes > 0;
8063
+ }
8064
+ async function validateApiKey(key, db) {
8065
+ const d = db || getDatabase();
8066
+ const keyHash = await hashKey(key);
8067
+ const row = d.query("SELECT id, name, key_prefix, created_at, last_used_at, expires_at FROM api_keys WHERE key_hash = ?").get(keyHash);
8068
+ if (!row)
8069
+ return null;
8070
+ if (row.expires_at && new Date(row.expires_at) < new Date) {
8071
+ return null;
8072
+ }
8073
+ d.run("UPDATE api_keys SET last_used_at = ? WHERE id = ?", [now(), row.id]);
8074
+ return row;
8075
+ }
8076
+ function hasAnyApiKeys(db) {
8077
+ const d = db || getDatabase();
8078
+ const row = d.query("SELECT COUNT(*) as count FROM api_keys").get();
8079
+ return (row?.count ?? 0) > 0;
8080
+ }
8081
+ var init_api_keys = __esm(() => {
8082
+ init_database();
8083
+ });
8084
+
8018
8085
  // src/server/serve.ts
8019
8086
  var exports_serve = {};
8020
8087
  __export(exports_serve, {
@@ -8092,18 +8159,35 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
8092
8159
  }
8093
8160
  const method = req.method;
8094
8161
  const port = getPort();
8162
+ if (path.startsWith("/api/") && !path.startsWith("/api/system/") && !path.startsWith("/api/keys")) {
8163
+ const hasKeys = hasAnyApiKeys();
8164
+ if (hasKeys) {
8165
+ const authHeader = req.headers.get("authorization");
8166
+ const apiKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
8167
+ if (!apiKey) {
8168
+ return json({ error: "API key required. Pass via Authorization: Bearer <key>" }, 401, port);
8169
+ }
8170
+ const valid = await validateApiKey(apiKey);
8171
+ if (!valid) {
8172
+ return json({ error: "Invalid or expired API key" }, 403, port);
8173
+ }
8174
+ }
8175
+ }
8095
8176
  if (path === "/api/tasks" && method === "GET") {
8096
8177
  try {
8097
8178
  const filter = {};
8098
8179
  const status = url.searchParams.get("status");
8099
8180
  const priority = url.searchParams.get("priority");
8100
8181
  const projectId = url.searchParams.get("project_id");
8182
+ const planId = url.searchParams.get("plan_id");
8101
8183
  if (status)
8102
8184
  filter.status = status;
8103
8185
  if (priority)
8104
8186
  filter.priority = priority;
8105
8187
  if (projectId)
8106
8188
  filter.project_id = projectId;
8189
+ if (planId)
8190
+ filter.plan_id = planId;
8107
8191
  const tasks = listTasks(filter);
8108
8192
  const projectCache = new Map;
8109
8193
  const planCache = new Map;
@@ -8466,6 +8550,39 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
8466
8550
  return json({ error: e instanceof Error ? e.message : "Failed to delete plan" }, 500, port);
8467
8551
  }
8468
8552
  }
8553
+ const projectGetMatch = path.match(/^\/api\/projects\/([^/]+)$/);
8554
+ if (projectGetMatch && method === "GET") {
8555
+ try {
8556
+ const id = projectGetMatch[1];
8557
+ const project = getProject(id);
8558
+ if (!project)
8559
+ return json({ error: "Project not found" }, 404, port);
8560
+ return json(project, 200, port);
8561
+ } catch (e) {
8562
+ return json({ error: e instanceof Error ? e.message : "Failed to get project" }, 500, port);
8563
+ }
8564
+ }
8565
+ const projectPatchMatch = path.match(/^\/api\/projects\/([^/]+)$/);
8566
+ if (projectPatchMatch && method === "PATCH") {
8567
+ try {
8568
+ const id = projectPatchMatch[1];
8569
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
8570
+ if (contentLength > MAX_BODY_SIZE)
8571
+ return json({ error: "Request body too large" }, 413, port);
8572
+ const body = await parseJsonBody(req);
8573
+ if (!body)
8574
+ return json({ error: "Invalid JSON" }, 400, port);
8575
+ const project = updateProject(id, {
8576
+ name: typeof body.name === "string" ? body.name : undefined,
8577
+ description: typeof body.description === "string" ? body.description : body.description === null ? "" : undefined,
8578
+ task_list_id: typeof body.task_list_id === "string" ? body.task_list_id : body.task_list_id === null ? "" : undefined
8579
+ });
8580
+ return json(project, 200, port);
8581
+ } catch (e) {
8582
+ const status = e instanceof Error && e.name === "ProjectNotFoundError" ? 404 : 500;
8583
+ return json({ error: e instanceof Error ? e.message : "Failed to update project" }, status, port);
8584
+ }
8585
+ }
8469
8586
  const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
8470
8587
  if (projectDeleteMatch && method === "DELETE") {
8471
8588
  try {
@@ -8478,12 +8595,61 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
8478
8595
  return json({ error: e instanceof Error ? e.message : "Failed to delete project" }, 500, port);
8479
8596
  }
8480
8597
  }
8598
+ if (path === "/api/keys" && method === "GET") {
8599
+ try {
8600
+ const keys = listApiKeys();
8601
+ return json(keys, 200, port);
8602
+ } catch (e) {
8603
+ return json({ error: e instanceof Error ? e.message : "Failed to list API keys" }, 500, port);
8604
+ }
8605
+ }
8606
+ if (path === "/api/keys" && method === "POST") {
8607
+ try {
8608
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
8609
+ if (contentLength > MAX_BODY_SIZE)
8610
+ return json({ error: "Request body too large" }, 413, port);
8611
+ const body = await parseJsonBody(req);
8612
+ if (!body)
8613
+ return json({ error: "Invalid JSON" }, 400, port);
8614
+ if (!body.name || typeof body.name !== "string") {
8615
+ return json({ error: "Missing required field: name" }, 400, port);
8616
+ }
8617
+ const parsed = createApiKeySchema.safeParse(body);
8618
+ if (!parsed.success) {
8619
+ return json({ error: "Invalid request body" }, 400, port);
8620
+ }
8621
+ const apiKey = await createApiKey(parsed.data);
8622
+ return json(apiKey, 201, port);
8623
+ } catch (e) {
8624
+ return json({ error: e instanceof Error ? e.message : "Failed to create API key" }, 500, port);
8625
+ }
8626
+ }
8627
+ const keyDeleteMatch = path.match(/^\/api\/keys\/([^/]+)$/);
8628
+ if (keyDeleteMatch && method === "DELETE") {
8629
+ try {
8630
+ const id = keyDeleteMatch[1];
8631
+ const deleted = deleteApiKey(id);
8632
+ if (!deleted)
8633
+ return json({ error: "API key not found" }, 404, port);
8634
+ return json({ deleted: true }, 200, port);
8635
+ } catch (e) {
8636
+ return json({ error: e instanceof Error ? e.message : "Failed to delete API key" }, 500, port);
8637
+ }
8638
+ }
8639
+ if (path === "/api/keys/status" && method === "GET") {
8640
+ try {
8641
+ const enabled = hasAnyApiKeys();
8642
+ return json({ auth_enabled: enabled }, 200, port);
8643
+ } catch (e) {
8644
+ return json({ error: e instanceof Error ? e.message : "Failed to check auth status" }, 500, port);
8645
+ }
8646
+ }
8481
8647
  if (method === "OPTIONS") {
8482
8648
  return new Response(null, {
8483
8649
  headers: {
8484
8650
  "Access-Control-Allow-Origin": `http://localhost:${port}`,
8485
8651
  "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
8486
- "Access-Control-Allow-Headers": "Content-Type"
8652
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
8487
8653
  }
8488
8654
  });
8489
8655
  }
@@ -8559,7 +8725,7 @@ Dashboard not found at: ${dashboardDir}`);
8559
8725
  }
8560
8726
  return server2;
8561
8727
  }
8562
- var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE, createTaskSchema, updateTaskSchema, createProjectSchema, createCommentSchema, createPlanSchema, updatePlanSchema, agentSchema;
8728
+ var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE, createTaskSchema, updateTaskSchema, createProjectSchema, createCommentSchema, createPlanSchema, updatePlanSchema, createApiKeySchema, agentSchema;
8563
8729
  var init_serve = __esm(() => {
8564
8730
  init_zod();
8565
8731
  init_tasks();
@@ -8567,6 +8733,7 @@ var init_serve = __esm(() => {
8567
8733
  init_plans();
8568
8734
  init_database();
8569
8735
  init_comments();
8736
+ init_api_keys();
8570
8737
  init_search();
8571
8738
  MIME_TYPES = {
8572
8739
  ".html": "text/html; charset=utf-8",
@@ -8630,6 +8797,10 @@ var init_serve = __esm(() => {
8630
8797
  description: exports_external.string().optional(),
8631
8798
  status: exports_external.enum(["active", "completed", "archived"]).optional()
8632
8799
  });
8800
+ createApiKeySchema = exports_external.object({
8801
+ name: exports_external.string(),
8802
+ expires_at: exports_external.string().optional()
8803
+ });
8633
8804
  agentSchema = exports_external.object({
8634
8805
  agent_id: exports_external.string().optional()
8635
8806
  });
package/dist/index.d.ts CHANGED
@@ -178,6 +178,25 @@ export interface CreateSessionInput {
178
178
  metadata?: Record<string, unknown>;
179
179
  }
180
180
 
181
+ // API Key
182
+ export interface ApiKey {
183
+ id: string;
184
+ name: string;
185
+ key_prefix: string;
186
+ created_at: string;
187
+ last_used_at: string | null;
188
+ expires_at: string | null;
189
+ }
190
+
191
+ export interface ApiKeyWithSecret extends ApiKey {
192
+ key: string; // full key, only returned on creation
193
+ }
194
+
195
+ export interface CreateApiKeyInput {
196
+ name: string;
197
+ expires_at?: string;
198
+ }
199
+
181
200
  // DB row types (raw from SQLite - JSON fields are strings)
182
201
  export interface TaskRow {
183
202
  id: string;
package/dist/index.js CHANGED
@@ -164,6 +164,19 @@ var MIGRATIONS = [
164
164
  ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
165
165
  CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
166
166
  INSERT OR IGNORE INTO _migrations (id) VALUES (4);
167
+ `,
168
+ `
169
+ CREATE TABLE IF NOT EXISTS api_keys (
170
+ id TEXT PRIMARY KEY,
171
+ name TEXT NOT NULL,
172
+ key_hash TEXT NOT NULL UNIQUE,
173
+ key_prefix TEXT NOT NULL,
174
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
175
+ last_used_at TEXT,
176
+ expires_at TEXT
177
+ );
178
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
179
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
167
180
  `
168
181
  ];
169
182
  var _db = null;
@@ -858,6 +871,56 @@ function deleteSession(id, db) {
858
871
  const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
859
872
  return result.changes > 0;
860
873
  }
874
+ // src/db/api-keys.ts
875
+ function generateApiKey() {
876
+ const bytes = new Uint8Array(32);
877
+ crypto.getRandomValues(bytes);
878
+ return "td_" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
879
+ }
880
+ async function hashKey(key) {
881
+ const encoder = new TextEncoder;
882
+ const data = encoder.encode(key);
883
+ const hash = await crypto.subtle.digest("SHA-256", data);
884
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
885
+ }
886
+ async function createApiKey(input, db) {
887
+ const d = db || getDatabase();
888
+ const id = uuid();
889
+ const timestamp = now();
890
+ const key = generateApiKey();
891
+ const keyHash = await hashKey(key);
892
+ const keyPrefix = key.slice(0, 10) + "...";
893
+ d.run(`INSERT INTO api_keys (id, name, key_hash, key_prefix, created_at, expires_at)
894
+ VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, keyHash, keyPrefix, timestamp, input.expires_at || null]);
895
+ const row = d.query("SELECT id, name, key_prefix, created_at, last_used_at, expires_at FROM api_keys WHERE id = ?").get(id);
896
+ return { ...row, key };
897
+ }
898
+ function listApiKeys(db) {
899
+ const d = db || getDatabase();
900
+ return d.query("SELECT id, name, key_prefix, created_at, last_used_at, expires_at FROM api_keys ORDER BY created_at DESC").all();
901
+ }
902
+ function deleteApiKey(id, db) {
903
+ const d = db || getDatabase();
904
+ const result = d.run("DELETE FROM api_keys WHERE id = ?", [id]);
905
+ return result.changes > 0;
906
+ }
907
+ async function validateApiKey(key, db) {
908
+ const d = db || getDatabase();
909
+ const keyHash = await hashKey(key);
910
+ const row = d.query("SELECT id, name, key_prefix, created_at, last_used_at, expires_at FROM api_keys WHERE key_hash = ?").get(keyHash);
911
+ if (!row)
912
+ return null;
913
+ if (row.expires_at && new Date(row.expires_at) < new Date) {
914
+ return null;
915
+ }
916
+ d.run("UPDATE api_keys SET last_used_at = ? WHERE id = ?", [now(), row.id]);
917
+ return row;
918
+ }
919
+ function hasAnyApiKeys(db) {
920
+ const d = db || getDatabase();
921
+ const row = d.query("SELECT COUNT(*) as count FROM api_keys").get();
922
+ return (row?.count ?? 0) > 0;
923
+ }
861
924
  // src/lib/search.ts
862
925
  function rowToTask2(row) {
863
926
  return {
@@ -1468,6 +1531,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
1468
1531
  return { pushed, pulled, errors };
1469
1532
  }
1470
1533
  export {
1534
+ validateApiKey,
1471
1535
  updateTask,
1472
1536
  updateSessionActivity,
1473
1537
  updateProject,
@@ -1488,6 +1552,8 @@ export {
1488
1552
  listProjects,
1489
1553
  listPlans,
1490
1554
  listComments,
1555
+ listApiKeys,
1556
+ hasAnyApiKeys,
1491
1557
  getTaskWithRelations,
1492
1558
  getTaskDependents,
1493
1559
  getTaskDependencies,
@@ -1504,11 +1570,13 @@ export {
1504
1570
  deleteProject,
1505
1571
  deletePlan,
1506
1572
  deleteComment,
1573
+ deleteApiKey,
1507
1574
  defaultSyncAgents,
1508
1575
  createTask,
1509
1576
  createSession,
1510
1577
  createProject,
1511
1578
  createPlan,
1579
+ createApiKey,
1512
1580
  completeTask,
1513
1581
  closeDatabase,
1514
1582
  addDependency,
package/dist/mcp/index.js CHANGED
@@ -4206,6 +4206,19 @@ var MIGRATIONS = [
4206
4206
  ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
4207
4207
  CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
4208
4208
  INSERT OR IGNORE INTO _migrations (id) VALUES (4);
4209
+ `,
4210
+ `
4211
+ CREATE TABLE IF NOT EXISTS api_keys (
4212
+ id TEXT PRIMARY KEY,
4213
+ name TEXT NOT NULL,
4214
+ key_hash TEXT NOT NULL UNIQUE,
4215
+ key_prefix TEXT NOT NULL,
4216
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4217
+ last_used_at TEXT,
4218
+ expires_at TEXT
4219
+ );
4220
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
4221
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
4209
4222
  `
4210
4223
  ];
4211
4224
  var _db = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",