@hasna/todos 0.4.0 → 0.5.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/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,6 +8159,20 @@ 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 = {};
@@ -8478,12 +8559,61 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
8478
8559
  return json({ error: e instanceof Error ? e.message : "Failed to delete project" }, 500, port);
8479
8560
  }
8480
8561
  }
8562
+ if (path === "/api/keys" && method === "GET") {
8563
+ try {
8564
+ const keys = listApiKeys();
8565
+ return json(keys, 200, port);
8566
+ } catch (e) {
8567
+ return json({ error: e instanceof Error ? e.message : "Failed to list API keys" }, 500, port);
8568
+ }
8569
+ }
8570
+ if (path === "/api/keys" && method === "POST") {
8571
+ try {
8572
+ const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
8573
+ if (contentLength > MAX_BODY_SIZE)
8574
+ return json({ error: "Request body too large" }, 413, port);
8575
+ const body = await parseJsonBody(req);
8576
+ if (!body)
8577
+ return json({ error: "Invalid JSON" }, 400, port);
8578
+ if (!body.name || typeof body.name !== "string") {
8579
+ return json({ error: "Missing required field: name" }, 400, port);
8580
+ }
8581
+ const parsed = createApiKeySchema.safeParse(body);
8582
+ if (!parsed.success) {
8583
+ return json({ error: "Invalid request body" }, 400, port);
8584
+ }
8585
+ const apiKey = await createApiKey(parsed.data);
8586
+ return json(apiKey, 201, port);
8587
+ } catch (e) {
8588
+ return json({ error: e instanceof Error ? e.message : "Failed to create API key" }, 500, port);
8589
+ }
8590
+ }
8591
+ const keyDeleteMatch = path.match(/^\/api\/keys\/([^/]+)$/);
8592
+ if (keyDeleteMatch && method === "DELETE") {
8593
+ try {
8594
+ const id = keyDeleteMatch[1];
8595
+ const deleted = deleteApiKey(id);
8596
+ if (!deleted)
8597
+ return json({ error: "API key not found" }, 404, port);
8598
+ return json({ deleted: true }, 200, port);
8599
+ } catch (e) {
8600
+ return json({ error: e instanceof Error ? e.message : "Failed to delete API key" }, 500, port);
8601
+ }
8602
+ }
8603
+ if (path === "/api/keys/status" && method === "GET") {
8604
+ try {
8605
+ const enabled = hasAnyApiKeys();
8606
+ return json({ auth_enabled: enabled }, 200, port);
8607
+ } catch (e) {
8608
+ return json({ error: e instanceof Error ? e.message : "Failed to check auth status" }, 500, port);
8609
+ }
8610
+ }
8481
8611
  if (method === "OPTIONS") {
8482
8612
  return new Response(null, {
8483
8613
  headers: {
8484
8614
  "Access-Control-Allow-Origin": `http://localhost:${port}`,
8485
8615
  "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
8486
- "Access-Control-Allow-Headers": "Content-Type"
8616
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
8487
8617
  }
8488
8618
  });
8489
8619
  }
@@ -8559,7 +8689,7 @@ Dashboard not found at: ${dashboardDir}`);
8559
8689
  }
8560
8690
  return server2;
8561
8691
  }
8562
- var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE, createTaskSchema, updateTaskSchema, createProjectSchema, createCommentSchema, createPlanSchema, updatePlanSchema, agentSchema;
8692
+ var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE, createTaskSchema, updateTaskSchema, createProjectSchema, createCommentSchema, createPlanSchema, updatePlanSchema, createApiKeySchema, agentSchema;
8563
8693
  var init_serve = __esm(() => {
8564
8694
  init_zod();
8565
8695
  init_tasks();
@@ -8567,6 +8697,7 @@ var init_serve = __esm(() => {
8567
8697
  init_plans();
8568
8698
  init_database();
8569
8699
  init_comments();
8700
+ init_api_keys();
8570
8701
  init_search();
8571
8702
  MIME_TYPES = {
8572
8703
  ".html": "text/html; charset=utf-8",
@@ -8630,6 +8761,10 @@ var init_serve = __esm(() => {
8630
8761
  description: exports_external.string().optional(),
8631
8762
  status: exports_external.enum(["active", "completed", "archived"]).optional()
8632
8763
  });
8764
+ createApiKeySchema = exports_external.object({
8765
+ name: exports_external.string(),
8766
+ expires_at: exports_external.string().optional()
8767
+ });
8633
8768
  agentSchema = exports_external.object({
8634
8769
  agent_id: exports_external.string().optional()
8635
8770
  });
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.0",
3
+ "version": "0.5.0",
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",