@hasna/todos 0.12.2 → 0.13.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/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.12.2",
44
+ version: "0.13.0",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
package/dist/registry.js CHANGED
@@ -11823,7 +11823,7 @@ var init_tasks = __esm(() => {
11823
11823
  // package.json
11824
11824
  var package_default = {
11825
11825
  name: "@hasna/todos",
11826
- version: "0.12.2",
11826
+ version: "0.13.0",
11827
11827
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
11828
11828
  type: "module",
11829
11829
  main: "dist/index.js",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.12.2",
3
+ "packageVersion": "0.13.0",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "6271f2144e08ad291b34c6d38d46a3cdadb14647",
6
- "gitTree": "9f02f36bf7e73db36e738655b28841cf16fa5f6e",
7
- "sourceTreeSha256": "520a72ea1d578387cf7403547f2deba823df0105214b6f404227e5f7c244f63f",
8
- "generatedAt": "2026-07-24T21:29:56.000Z"
5
+ "gitCommit": "f1e13ecb6f374dacff6905a78e7dbd60a92883bb",
6
+ "gitTree": "e6d133cf8c25a83c7085b7edf52cb8a7e7745fdb",
7
+ "sourceTreeSha256": "fa93e1e4b62bdd70bd753daf4e86b47ec2aa92a1e6a6f091a2924c30649391f4",
8
+ "generatedAt": "2026-07-25T17:29:55.000Z"
9
9
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Auth posture for the Todos HTTP server — resolved ONCE at startup.
3
+ *
4
+ * Historically `checkAuth` failed OPEN: when neither `TODOS_API_KEY` nor a
5
+ * generated key existed it returned "authorized" for every request, which made
6
+ * `/mcp` and all of `/api/*` an anonymous read/write plane on any deployment
7
+ * that bound a non-loopback host (e.g. `HOST=0.0.0.0` behind a public ALB).
8
+ *
9
+ * The unconfigured case now DENIES. `resolveAuthPosture` is the single decision
10
+ * point; it is pure so the matrix can be unit-tested without a live server.
11
+ *
12
+ * Postures:
13
+ * - `enforce` — a credential source exists; every data route requires it.
14
+ * - `local-plane-disabled`— hosted server (a cloud DSN is configured, so the
15
+ * self-authenticating `/v1` plane works) with NO local
16
+ * credential: the local-only planes (`/api/*`, `/mcp`)
17
+ * are not served at all. `/v1` + probes keep working,
18
+ * so this never takes a hosted deployment down.
19
+ * - `anonymous-loopback` — explicitly opted in AND bound to loopback. Anonymous
20
+ * requests are additionally required to come from a
21
+ * loopback peer. This is the documented local-dev /
22
+ * dashboard path, never reachable off-box.
23
+ *
24
+ * Anything else throws `AuthNotConfiguredError`: refusing to start beats
25
+ * starting wide open.
26
+ */
27
+ /** Env var that configures the static server credential for `/api/*` + `/mcp`. */
28
+ export declare const AUTH_ENV_VAR = "TODOS_API_KEY";
29
+ /** Env var that opts a loopback-bound server into the anonymous local plane. */
30
+ export declare const ALLOW_ANONYMOUS_ENV_VAR = "TODOS_ALLOW_ANONYMOUS";
31
+ export type AuthPostureMode = "enforce" | "local-plane-disabled" | "anonymous-loopback";
32
+ export interface AuthPosture {
33
+ mode: AuthPostureMode;
34
+ /** Human-readable reason, logged once at startup. */
35
+ reason: string;
36
+ }
37
+ export declare class AuthNotConfiguredError extends Error {
38
+ static readonly code = "AUTH_NOT_CONFIGURED";
39
+ readonly code = "AUTH_NOT_CONFIGURED";
40
+ constructor(message: string);
41
+ }
42
+ /** True when `host` is a loopback bind address (i.e. unreachable from off-box). */
43
+ export declare function isLoopbackHost(host: string | undefined | null): boolean;
44
+ /**
45
+ * True when a peer address is loopback. Covers IPv4 127/8, IPv6 ::1 and the
46
+ * IPv4-mapped form Bun reports on dual-stack sockets (`::ffff:127.0.0.1`).
47
+ */
48
+ export declare function isLoopbackAddress(address: string | undefined | null): boolean;
49
+ /** Truthy-flag parsing for the anonymous opt-in env var. */
50
+ export declare function isAnonymousOptInEnv(env?: NodeJS.ProcessEnv): boolean;
51
+ export interface AuthPostureInput {
52
+ /** Static credential from `--api-key` / `TODOS_API_KEY`. */
53
+ apiKey: string | null;
54
+ /** Whether the local `api_keys` table holds at least one active key. */
55
+ hasGeneratedKeys: boolean;
56
+ /** Bind host passed to `Bun.serve`. */
57
+ host: string | undefined;
58
+ /** Explicit opt-in to the anonymous loopback plane (flag or env). */
59
+ allowAnonymous: boolean;
60
+ /**
61
+ * Whether this process serves the hosted, self-authenticating `/v1` plane
62
+ * (a cloud DSN is configured). Defaults to the live env.
63
+ */
64
+ hosted?: boolean;
65
+ }
66
+ /** Actionable, credential-free startup error text. */
67
+ export declare function authNotConfiguredMessage(host: string | undefined): string;
68
+ /**
69
+ * Resolve the startup auth posture, or throw `AuthNotConfiguredError` when the
70
+ * only remaining option would be to serve data anonymously off-box.
71
+ */
72
+ export declare function resolveAuthPosture(input: AuthPostureInput): AuthPosture;
73
+ /** One-line startup log describing the resolved posture. */
74
+ export declare function describeAuthPosture(posture: AuthPosture): string;
75
+ //# sourceMappingURL=auth-posture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-posture.d.ts","sourceRoot":"","sources":["../../src/server/auth-posture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,kFAAkF;AAClF,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAC5C,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,sBAAsB,GAAG,oBAAoB,CAAC;AAExF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,MAAM,CAAC,QAAQ,CAAC,IAAI,yBAAyB;IAC7C,QAAQ,CAAC,IAAI,yBAA+B;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAID,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAMvE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAY7E;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAKjF;AAED,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uCAAuC;IACvC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,sDAAsD;AACtD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAezE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAmCvE;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAWhE"}
@@ -70,7 +70,7 @@ var package_default;
70
70
  var init_package = __esm(() => {
71
71
  package_default = {
72
72
  name: "@hasna/todos",
73
- version: "0.12.2",
73
+ version: "0.13.0",
74
74
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
75
75
  type: "module",
76
76
  main: "dist/index.js",
@@ -8827,6 +8827,123 @@ var init_api_keys = __esm(() => {
8827
8827
  init_database();
8828
8828
  });
8829
8829
 
8830
+ // src/server/auth-posture.ts
8831
+ var exports_auth_posture = {};
8832
+ __export(exports_auth_posture, {
8833
+ resolveAuthPosture: () => resolveAuthPosture,
8834
+ isLoopbackHost: () => isLoopbackHost,
8835
+ isLoopbackAddress: () => isLoopbackAddress,
8836
+ isAnonymousOptInEnv: () => isAnonymousOptInEnv,
8837
+ describeAuthPosture: () => describeAuthPosture,
8838
+ authNotConfiguredMessage: () => authNotConfiguredMessage,
8839
+ AuthNotConfiguredError: () => AuthNotConfiguredError,
8840
+ AUTH_ENV_VAR: () => AUTH_ENV_VAR,
8841
+ ALLOW_ANONYMOUS_ENV_VAR: () => ALLOW_ANONYMOUS_ENV_VAR
8842
+ });
8843
+ function isLoopbackHost(host) {
8844
+ if (!host)
8845
+ return true;
8846
+ const trimmed = host.trim().toLowerCase();
8847
+ if (trimmed === "")
8848
+ return true;
8849
+ if (LOOPBACK_HOSTNAMES.has(trimmed))
8850
+ return true;
8851
+ return isLoopbackAddress(trimmed);
8852
+ }
8853
+ function isLoopbackAddress(address) {
8854
+ if (!address)
8855
+ return false;
8856
+ let value = address.trim().toLowerCase();
8857
+ if (value.startsWith("[") && value.endsWith("]"))
8858
+ value = value.slice(1, -1);
8859
+ if (value === "::1")
8860
+ return true;
8861
+ if (value.startsWith("::ffff:"))
8862
+ value = value.slice("::ffff:".length);
8863
+ if (value === "localhost")
8864
+ return true;
8865
+ const octets = value.split(".");
8866
+ if (octets.length !== 4)
8867
+ return false;
8868
+ const parsed = octets.map((o) => /^\d{1,3}$/.test(o) ? Number.parseInt(o, 10) : Number.NaN);
8869
+ if (parsed.some((n) => Number.isNaN(n) || n > 255))
8870
+ return false;
8871
+ return parsed[0] === 127;
8872
+ }
8873
+ function isAnonymousOptInEnv(env = process.env) {
8874
+ const raw = env[ALLOW_ANONYMOUS_ENV_VAR];
8875
+ if (!raw)
8876
+ return false;
8877
+ const value = raw.trim().toLowerCase();
8878
+ return value === "1" || value === "true" || value === "yes";
8879
+ }
8880
+ function authNotConfiguredMessage(host) {
8881
+ const bind = host && host.trim() !== "" ? host : "127.0.0.1";
8882
+ return [
8883
+ `todos serve: refusing to start \u2014 no API credential is configured, and this server`,
8884
+ `would otherwise expose /api/* and /mcp (task read/write, agent registration, webhook`,
8885
+ `creation) to every caller that can reach ${bind}:<port>.`,
8886
+ ``,
8887
+ `Fix ONE of the following, then restart:`,
8888
+ ` 1. Set the server credential: export ${AUTH_ENV_VAR}=<key> (or pass --api-key <key>)`,
8889
+ ` 2. Mint a stored key: todos api-keys create "<caller name>"`,
8890
+ ` 3. Local dev only, loopback bind: todos serve --allow-anonymous`,
8891
+ ` (or ${ALLOW_ANONYMOUS_ENV_VAR}=1; refused unless the bind host is loopback)`,
8892
+ ``,
8893
+ `Never use option 3 with --host 0.0.0.0 or any other off-box bind.`
8894
+ ].join(`
8895
+ `);
8896
+ }
8897
+ function resolveAuthPosture(input) {
8898
+ const hosted = input.hosted ?? isCloudModeEnabled();
8899
+ const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
8900
+ if (hasCredentialSource) {
8901
+ return {
8902
+ mode: "enforce",
8903
+ reason: input.apiKey ? `credential from ${AUTH_ENV_VAR}/--api-key` : "at least one active generated API key"
8904
+ };
8905
+ }
8906
+ if (hosted) {
8907
+ return {
8908
+ mode: "local-plane-disabled",
8909
+ reason: `hosted deployment with no ${AUTH_ENV_VAR}: /api/* and /mcp are not served`
8910
+ };
8911
+ }
8912
+ if (input.allowAnonymous) {
8913
+ if (!isLoopbackHost(input.host)) {
8914
+ throw new AuthNotConfiguredError(`todos serve: --allow-anonymous is refused for the non-loopback bind host "${input.host}".
8915
+ ` + `An anonymous /api/* + /mcp plane must never be reachable off-box.
8916
+
8917
+ ` + authNotConfiguredMessage(input.host));
8918
+ }
8919
+ return { mode: "anonymous-loopback", reason: "explicit --allow-anonymous on a loopback bind" };
8920
+ }
8921
+ throw new AuthNotConfiguredError(authNotConfiguredMessage(input.host));
8922
+ }
8923
+ function describeAuthPosture(posture) {
8924
+ switch (posture.mode) {
8925
+ case "enforce":
8926
+ return `auth: ENFORCED on /api/* and /mcp (${posture.reason})`;
8927
+ case "local-plane-disabled":
8928
+ return `auth: /api/* and /mcp DISABLED (${posture.reason}); /v1 remains authenticated, ` + `/health /ready /version /openapi.json remain public. Set ${AUTH_ENV_VAR} to enable them.`;
8929
+ case "anonymous-loopback":
8930
+ return `auth: ANONYMOUS local plane on loopback only (${posture.reason}). ` + `Set ${AUTH_ENV_VAR} to require a credential.`;
8931
+ }
8932
+ }
8933
+ var AUTH_ENV_VAR = "TODOS_API_KEY", ALLOW_ANONYMOUS_ENV_VAR = "TODOS_ALLOW_ANONYMOUS", AuthNotConfiguredError, LOOPBACK_HOSTNAMES;
8934
+ var init_auth_posture = __esm(() => {
8935
+ init_cloud();
8936
+ AuthNotConfiguredError = class AuthNotConfiguredError extends Error {
8937
+ static code = "AUTH_NOT_CONFIGURED";
8938
+ code = AuthNotConfiguredError.code;
8939
+ constructor(message) {
8940
+ super(message);
8941
+ this.name = "AuthNotConfiguredError";
8942
+ }
8943
+ };
8944
+ LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "ip6-localhost"]);
8945
+ });
8946
+
8830
8947
  // src/db/storage-tombstones.ts
8831
8948
  function recordStorageTombstone(input, db) {
8832
8949
  const d = db ?? getDatabase();
@@ -91259,7 +91376,7 @@ async function main() {
91259
91376
  }
91260
91377
  const { startServer } = await Promise.resolve().then(() => (init_serve(), exports_serve));
91261
91378
  const port = resolveHttpPort();
91262
- await startServer(port, { open: false, host: "127.0.0.1" });
91379
+ await startServer(port, { open: false, host: "127.0.0.1", allowAnonymous: true });
91263
91380
  console.error(`todos MCP HTTP mounted at http://127.0.0.1:${port}/mcp`);
91264
91381
  }
91265
91382
  var agentFocusMap, isDirectRun;
@@ -91636,6 +91753,7 @@ __export(exports_serve, {
91636
91753
  startServer: () => startServer,
91637
91754
  serveStaticFile: () => serveStaticFile,
91638
91755
  json: () => json,
91756
+ checkAuth: () => checkAuth,
91639
91757
  SECURITY_HEADERS: () => SECURITY_HEADERS,
91640
91758
  MIME_TYPES: () => MIME_TYPES
91641
91759
  });
@@ -91670,19 +91788,32 @@ function getProvidedApiKey(req) {
91670
91788
  return null;
91671
91789
  return auth.replace(/^Bearer\s+/i, "").trim() || null;
91672
91790
  }
91673
- function checkAuth(req, apiKey) {
91674
- const generatedKeysEnabled = hasActiveApiKeys();
91675
- if (!apiKey && !generatedKeysEnabled)
91676
- return null;
91791
+ function unauthorized() {
91792
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
91793
+ status: 401,
91794
+ headers: { "Content-Type": "application/json", "WWW-Authenticate": "Bearer", ...SECURITY_HEADERS }
91795
+ });
91796
+ }
91797
+ function localPlaneDisabled() {
91798
+ return new Response(JSON.stringify({
91799
+ error: "Not found",
91800
+ code: "LOCAL_PLANE_DISABLED",
91801
+ hint: `/api/* and /mcp are local-only planes and are disabled on this deployment. Set ${AUTH_ENV_VAR} to serve them behind an API key, or use the authenticated /v1 API.`
91802
+ }), { status: 404, headers: { "Content-Type": "application/json", ...SECURITY_HEADERS } });
91803
+ }
91804
+ function checkAuth(req, apiKey, posture, clientIp) {
91805
+ if (posture.mode === "local-plane-disabled")
91806
+ return localPlaneDisabled();
91807
+ if (posture.mode === "anonymous-loopback") {
91808
+ if (!hasActiveApiKeys()) {
91809
+ return isLoopbackAddress(clientIp) ? null : unauthorized();
91810
+ }
91811
+ }
91677
91812
  const provided = getProvidedApiKey(req);
91678
91813
  const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
91679
91814
  const matchesGeneratedKey = Boolean(provided && verifyApiKey2(provided));
91680
- if (!matchesEnvKey && !matchesGeneratedKey) {
91681
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
91682
- status: 401,
91683
- headers: { "Content-Type": "application/json", "WWW-Authenticate": "Bearer", ...SECURITY_HEADERS }
91684
- });
91685
- }
91815
+ if (!matchesEnvKey && !matchesGeneratedKey)
91816
+ return unauthorized();
91686
91817
  return null;
91687
91818
  }
91688
91819
  function resolveClientIp(req, server) {
@@ -91761,6 +91892,22 @@ async function startServer(port, options) {
91761
91892
  const shouldOpen = options?.open ?? true;
91762
91893
  const apiKey = options?.apiKey || process.env.TODOS_API_KEY || null;
91763
91894
  const db = getDatabase();
91895
+ const authPosture = resolveAuthPosture({
91896
+ apiKey,
91897
+ hasGeneratedKeys: hasActiveApiKeys(),
91898
+ host: options?.host,
91899
+ allowAnonymous: options?.allowAnonymous === true || isAnonymousOptInEnv()
91900
+ });
91901
+ if (authPosture.mode === "anonymous-loopback") {
91902
+ console.error(`
91903
+ \u26A0 ${describeAuthPosture(authPosture)}
91904
+ ` + ` /api/* and /mcp accept UNAUTHENTICATED task read/write from any process on this
91905
+ ` + ` machine. Set ${AUTH_ENV_VAR}=<key> (or run \`todos api-keys create "<name>"\`) to require a
91906
+ ` + ` credential, and never combine ${ALLOW_ANONYMOUS_ENV_VAR} with an off-box bind.
91907
+ `);
91908
+ } else {
91909
+ console.log(describeAuthPosture(authPosture));
91910
+ }
91764
91911
  try {
91765
91912
  const { startRuntimeShadowDrain: startRuntimeShadowDrain2 } = await Promise.resolve().then(() => (init_shadow_runtime(), exports_shadow_runtime));
91766
91913
  startRuntimeShadowDrain2(db);
@@ -91845,6 +91992,7 @@ Dashboard not found at: ${dashboardDir}`);
91845
91992
  });
91846
91993
  }
91847
91994
  const ip = resolveClientIp(req, server2);
91995
+ const peerIp = server2.requestIP(req)?.address;
91848
91996
  const rl = checkRateLimit(ip);
91849
91997
  if (!rl.allowed) {
91850
91998
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
@@ -91883,15 +92031,15 @@ Dashboard not found at: ${dashboardDir}`);
91883
92031
  return res;
91884
92032
  }
91885
92033
  if (path === "/mcp") {
91886
- const authError = checkAuth(req, apiKey);
92034
+ const authError = checkAuth(req, apiKey, authPosture, peerIp);
91887
92035
  if (authError)
91888
92036
  return authError;
91889
92037
  const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
91890
92038
  const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
91891
92039
  return handleMcpHttpRequest2(req, buildServer2);
91892
92040
  }
91893
- if (path.startsWith("/api/")) {
91894
- const authError = checkAuth(req, apiKey);
92041
+ if (path === "/api" || path.startsWith("/api/")) {
92042
+ const authError = checkAuth(req, apiKey, authPosture, peerIp);
91895
92043
  if (authError)
91896
92044
  return authError;
91897
92045
  }
@@ -92131,6 +92279,7 @@ var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RA
92131
92279
  var init_serve = __esm(() => {
92132
92280
  init_database();
92133
92281
  init_api_keys();
92282
+ init_auth_posture();
92134
92283
  init_routes();
92135
92284
  MIME_TYPES = {
92136
92285
  ".html": "text/html; charset=utf-8",
@@ -92322,10 +92471,22 @@ async function main2() {
92322
92471
  }
92323
92472
  const noOpen = process.argv.includes("--no-open") || process.env["TODOS_NO_OPEN"] === "true" || Boolean(envPort);
92324
92473
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
92325
- startServer2(port, {
92326
- open: !noOpen,
92327
- host: parseStringArg("--host") || process.env.HOST,
92328
- apiKey: parseStringArg("--api-key")
92329
- });
92474
+ try {
92475
+ await startServer2(port, {
92476
+ open: !noOpen,
92477
+ host: parseStringArg("--host") || process.env.HOST,
92478
+ apiKey: parseStringArg("--api-key"),
92479
+ allowAnonymous: process.argv.includes("--allow-anonymous")
92480
+ });
92481
+ } catch (error3) {
92482
+ const { AuthNotConfiguredError: AuthNotConfiguredError2 } = await Promise.resolve().then(() => (init_auth_posture(), exports_auth_posture));
92483
+ if (error3 instanceof AuthNotConfiguredError2) {
92484
+ console.error(`
92485
+ ${error3.message}
92486
+ `);
92487
+ process.exit(1);
92488
+ }
92489
+ throw error3;
92490
+ }
92330
92491
  }
92331
92492
  main2();
@@ -3,9 +3,21 @@
3
3
  * Serves the Vite-built React/shadcn dashboard from dashboard/dist/.
4
4
  * Provides REST API endpoints for task management.
5
5
  */
6
+ import { type AuthPosture } from "./auth-posture.js";
6
7
  import type { Task } from "../types/index.js";
7
8
  export declare const MIME_TYPES: Record<string, string>;
8
9
  export declare const SECURITY_HEADERS: Record<string, string>;
10
+ /**
11
+ * Check API key auth for a data route — returns a Response if the request must
12
+ * be refused, null if it may proceed.
13
+ *
14
+ * FAILS CLOSED. There is deliberately no "no key configured, skip auth" branch:
15
+ * the unconfigured case is resolved at startup by `resolveAuthPosture`, which
16
+ * either refuses to start or disables these routes entirely. A request is only
17
+ * ever served anonymously under the explicit `anonymous-loopback` posture, and
18
+ * then only when the peer address is itself loopback.
19
+ */
20
+ export declare function checkAuth(req: Request, apiKey: string | null, posture: AuthPosture, clientIp?: string): Response | null;
9
21
  export declare function json(data: unknown, status?: number, headers?: HeadersInit): Response;
10
22
  export declare function serveStaticFile(filePath: string): Response | null;
11
23
  export declare function taskToSummary(task: Task, fields?: string[]): {
@@ -33,9 +45,17 @@ export declare function taskToSummary(task: Task, fields?: string[]): {
33
45
  } | {
34
46
  [k: string]: {} | null;
35
47
  };
36
- export declare function startServer(port: number, options?: {
48
+ export interface StartServerOptions {
37
49
  open?: boolean;
38
50
  host?: string;
39
51
  apiKey?: string;
40
- }): Promise<void>;
52
+ /**
53
+ * Explicitly opt into serving `/api/*` + `/mcp` anonymously. Honored ONLY when
54
+ * no credential is configured AND the bind host is loopback; otherwise the
55
+ * server refuses to start. `todos-mcp --http` sets this because that transport
56
+ * is loopback-pinned by contract.
57
+ */
58
+ allowAnonymous?: boolean;
59
+ }
60
+ export declare function startServer(port: number, options?: StartServerOptions): Promise<void>;
41
61
  //# sourceMappingURL=serve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AA+B9C,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAW7C,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMnD,CAAC;AAwEF,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,QAAQ,CASjF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAYjE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;EA0B1D;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsf3H"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAOL,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AA+B9C,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAW7C,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMnD,CAAC;AAiCF;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,OAAO,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,QAAQ,GAAG,IAAI,CAsBjB;AA6CD,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,QAAQ,CASjF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAYjE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;EA0B1D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+gB3F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.12.2",
3
+ "version": "0.13.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",