commandmate 0.2.13 → 0.3.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.
Files changed (154) hide show
  1. package/.env.example +21 -2
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +32 -24
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +7 -7
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/2.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack +0 -0
  12. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  13. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  14. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  15. package/.next/cache/webpack/server-production/0.pack +0 -0
  16. package/.next/cache/webpack/server-production/index.pack +0 -0
  17. package/.next/next-server.js.nft.json +1 -1
  18. package/.next/prerender-manifest.json +1 -1
  19. package/.next/react-loadable-manifest.json +2 -2
  20. package/.next/required-server-files.json +1 -1
  21. package/.next/routes-manifest.json +1 -1
  22. package/.next/server/app/_not-found/page.js +1 -1
  23. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  24. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/app/update-check/route.js +1 -1
  26. package/.next/server/app/api/auth/login/route.js +1 -0
  27. package/.next/server/app/api/auth/login/route.js.nft.json +1 -0
  28. package/.next/server/app/api/auth/logout/route.js +1 -0
  29. package/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
  30. package/.next/server/app/api/auth/status/route.js +1 -0
  31. package/.next/server/app/api/auth/status/route.js.nft.json +1 -0
  32. package/.next/server/app/api/hooks/claude-done/route.js +1 -1
  33. package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
  34. package/.next/server/app/api/repositories/route.js +2 -2
  35. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  36. package/.next/server/app/api/slash-commands/route.js +1 -1
  37. package/.next/server/app/api/slash-commands/route.js.nft.json +1 -1
  38. package/.next/server/app/api/slash-commands.body +1 -1
  39. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  40. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  41. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  42. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  43. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  44. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  45. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  46. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
  48. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  50. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  51. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  52. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  55. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  56. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  57. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js.nft.json +1 -1
  58. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  59. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  60. package/.next/server/app/api/worktrees/route.js +1 -1
  61. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  62. package/.next/server/app/login/page.js +1 -0
  63. package/.next/server/app/login/page.js.nft.json +1 -0
  64. package/.next/server/app/login/page_client-reference-manifest.js +1 -0
  65. package/.next/server/app/page.js +2 -2
  66. package/.next/server/app/page.js.nft.json +1 -1
  67. package/.next/server/app/page_client-reference-manifest.js +1 -1
  68. package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
  69. package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +1 -1
  70. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app/worktrees/[id]/page.js +2 -2
  72. package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
  73. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/worktrees/[id]/terminal/page.js +1 -1
  75. package/.next/server/app/worktrees/[id]/terminal/page.js.nft.json +1 -1
  76. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app-paths-manifest.json +12 -8
  78. package/.next/server/chunks/3013.js +1 -0
  79. package/.next/server/chunks/3074.js +1 -0
  80. package/.next/server/chunks/{1287.js → 3294.js} +2 -2
  81. package/.next/server/chunks/3860.js +1 -1
  82. package/.next/server/chunks/4893.js +2 -2
  83. package/.next/server/chunks/539.js +35 -0
  84. package/.next/server/chunks/5795.js +1 -0
  85. package/.next/server/chunks/7536.js +1 -1
  86. package/.next/server/chunks/7566.js +19 -0
  87. package/.next/server/chunks/8693.js +1 -1
  88. package/.next/server/edge-runtime-webpack.js +2 -0
  89. package/.next/server/edge-runtime-webpack.js.map +1 -0
  90. package/.next/server/functions-config-manifest.json +1 -1
  91. package/.next/server/middleware-build-manifest.js +1 -1
  92. package/.next/server/middleware-manifest.json +28 -2
  93. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  94. package/.next/server/pages/500.html +1 -1
  95. package/.next/server/server-reference-manifest.json +1 -1
  96. package/.next/server/src/middleware.js +14 -0
  97. package/.next/server/src/middleware.js.map +1 -0
  98. package/.next/static/chunks/{2626.2125083a1ff3b80a.js → 6163.f672451d4575decf.js} +1 -1
  99. package/.next/static/chunks/{656.d72f25ce819bd77e.js → 656.5e2de0173f5a06bd.js} +1 -1
  100. package/.next/static/chunks/8091-925542bdfc843dce.js +1 -0
  101. package/.next/static/chunks/8528-4d554d3b94d4cf9b.js +1 -0
  102. package/.next/static/chunks/app/{layout-07755491d5d57242.js → layout-9110f9a5e41c6bf4.js} +1 -1
  103. package/.next/static/chunks/app/login/page-2d42204ba87cd136.js +1 -0
  104. package/.next/static/chunks/app/page-238b5a70d8c101e9.js +1 -0
  105. package/.next/static/chunks/app/worktrees/[id]/page-9418e49bdc1de02c.js +1 -0
  106. package/.next/static/chunks/main-db79434ee4a6c931.js +1 -0
  107. package/.next/static/chunks/webpack-3c0ee3ce5b546818.js +1 -0
  108. package/.next/static/css/b9ea6a4fad17dc32.css +3 -0
  109. package/.next/trace +5 -5
  110. package/.next/types/app/api/auth/login/route.ts +343 -0
  111. package/.next/types/app/api/auth/logout/route.ts +343 -0
  112. package/.next/types/app/api/auth/status/route.ts +343 -0
  113. package/.next/types/app/login/page.ts +79 -0
  114. package/README.md +6 -1
  115. package/dist/cli/commands/init.d.ts.map +1 -1
  116. package/dist/cli/commands/init.js +2 -0
  117. package/dist/cli/commands/start.d.ts +2 -0
  118. package/dist/cli/commands/start.d.ts.map +1 -1
  119. package/dist/cli/commands/start.js +159 -14
  120. package/dist/cli/commands/status.d.ts.map +1 -1
  121. package/dist/cli/commands/status.js +4 -0
  122. package/dist/cli/config/security-messages.d.ts +3 -1
  123. package/dist/cli/config/security-messages.d.ts.map +1 -1
  124. package/dist/cli/config/security-messages.js +6 -2
  125. package/dist/cli/index.js +17 -0
  126. package/dist/cli/types/index.d.ts +17 -0
  127. package/dist/cli/types/index.d.ts.map +1 -1
  128. package/dist/cli/utils/daemon.d.ts.map +1 -1
  129. package/dist/cli/utils/daemon.js +16 -3
  130. package/dist/config/auth-config.d.ts +43 -0
  131. package/dist/config/auth-config.d.ts.map +1 -0
  132. package/dist/config/auth-config.js +112 -0
  133. package/dist/lib/auth.d.ts +104 -0
  134. package/dist/lib/auth.d.ts.map +1 -0
  135. package/dist/lib/auth.js +250 -0
  136. package/dist/server/server.js +123 -12
  137. package/dist/server/src/config/auth-config.js +112 -0
  138. package/dist/server/src/lib/auth.js +250 -0
  139. package/dist/server/src/lib/auto-yes-manager.js +180 -96
  140. package/dist/server/src/lib/ip-restriction.js +241 -0
  141. package/dist/server/src/lib/ws-server.js +63 -33
  142. package/dist/server/src/types/slash-commands.js +1 -0
  143. package/package.json +2 -2
  144. package/.next/server/chunks/9238.js +0 -35
  145. package/.next/server/chunks/9367.js +0 -19
  146. package/.next/static/chunks/5970-2e18108d0cabd8af.js +0 -1
  147. package/.next/static/chunks/816-af44cb865b0c980e.js +0 -1
  148. package/.next/static/chunks/app/page-a6593b9640df66a6.js +0 -1
  149. package/.next/static/chunks/app/worktrees/[id]/page-d9a7913679eccfd9.js +0 -1
  150. package/.next/static/chunks/main-f00f82f1cf18dd99.js +0 -1
  151. package/.next/static/chunks/webpack-e6531fcf859d9451.js +0 -1
  152. package/.next/static/css/897ffb669f47c97b.css +0 -3
  153. /package/.next/static/{oUEq-Bd47xtkJcFDOI6rr → clTo9tuAoPMLcGRuVENfO}/_buildManifest.js +0 -0
  154. /package/.next/static/{oUEq-Bd47xtkJcFDOI6rr → clTo9tuAoPMLcGRuVENfO}/_ssgManifest.js +0 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Token Authentication Core Module
3
+ * Issue #331: Token authentication and HTTPS support
4
+ *
5
+ * CONSTRAINTS:
6
+ * - C001: No Next.js module dependencies (next/headers, next/server, etc.)
7
+ * This module must be compatible with CLI build (tsconfig.cli.json)
8
+ * - S001: Token verification uses crypto.timingSafeEqual() (timing-safe comparison)
9
+ * - S002: AUTH_EXCLUDED_PATHS matching uses === (exact match, no startsWith)
10
+ */
11
+ import { AUTH_COOKIE_NAME, AUTH_EXCLUDED_PATHS, parseDuration, computeExpireAt, DEFAULT_EXPIRE_DURATION_MS, isValidTokenHash } from '../config/auth-config';
12
+ export { AUTH_COOKIE_NAME, AUTH_EXCLUDED_PATHS, parseDuration, computeExpireAt, DEFAULT_EXPIRE_DURATION_MS, isValidTokenHash };
13
+ /** Rate limiting configuration for brute-force protection */
14
+ export declare const RATE_LIMIT_CONFIG: {
15
+ /** Maximum failed attempts before lockout */
16
+ readonly maxAttempts: 5;
17
+ /** Lockout duration in ms (15 minutes) */
18
+ readonly lockoutDuration: number;
19
+ /** Cleanup interval in ms (1 hour) */
20
+ readonly cleanupInterval: number;
21
+ };
22
+ /** Fallback cookie maxAge in seconds when no explicit expiry is set (24 hours) */
23
+ export declare const DEFAULT_COOKIE_MAX_AGE_SECONDS: number;
24
+ /**
25
+ * Generate a cryptographically secure random token
26
+ * @returns 64-character hex string (32 bytes of entropy)
27
+ */
28
+ export declare function generateToken(): string;
29
+ /**
30
+ * Hash a token using SHA-256
31
+ * @param token - The plain text token to hash
32
+ * @returns 64-character hex string (SHA-256 hash)
33
+ */
34
+ export declare function hashToken(token: string): string;
35
+ /**
36
+ * Verify a token against the stored hash
37
+ * S001: Uses crypto.timingSafeEqual() for timing-safe comparison
38
+ *
39
+ * @param token - The plain text token to verify
40
+ * @returns true if the token is valid and not expired
41
+ */
42
+ export declare function verifyToken(token: string): boolean;
43
+ /**
44
+ * Parse a Cookie header string into key-value pairs
45
+ * Used by WebSocket upgrade handler where next/headers is not available
46
+ *
47
+ * @param cookieHeader - Raw Cookie header string
48
+ * @returns Parsed cookies as Record<string, string>
49
+ */
50
+ export declare function parseCookies(cookieHeader: string): Record<string, string>;
51
+ /**
52
+ * Check if authentication is enabled.
53
+ * Returns true only when CM_AUTH_TOKEN_HASH is set AND passes format validation.
54
+ * This prevents the state where auth appears enabled but login is impossible
55
+ * (e.g., when the hash value is malformed).
56
+ */
57
+ export declare function isAuthEnabled(): boolean;
58
+ /**
59
+ * Calculate the Cookie maxAge in seconds (remaining token lifetime)
60
+ * @returns maxAge in seconds, or 0 if expired/no expiry
61
+ */
62
+ export declare function getTokenMaxAge(): number;
63
+ /**
64
+ * Check if HTTPS is enabled based on certificate environment variable
65
+ * @returns true if CM_HTTPS_CERT is set (indicating TLS certificates are configured)
66
+ */
67
+ export declare function isHttpsEnabled(): boolean;
68
+ /**
69
+ * Cookie options for authentication cookies.
70
+ * C001: Uses only standard types (no Next.js CookieOptions dependency).
71
+ */
72
+ export interface AuthCookieOptions {
73
+ httpOnly: boolean;
74
+ sameSite: 'strict';
75
+ secure: boolean;
76
+ maxAge: number;
77
+ path: string;
78
+ }
79
+ /**
80
+ * Build authentication cookie options with consistent security settings.
81
+ * Centralizes cookie configuration to enforce HttpOnly, SameSite, and Secure flags.
82
+ *
83
+ * @param maxAge - Cookie max age in seconds. Use 0 to clear the cookie.
84
+ * @returns Cookie options object compatible with Next.js response.cookies.set()
85
+ */
86
+ export declare function buildAuthCookieOptions(maxAge: number): AuthCookieOptions;
87
+ export interface RateLimitResult {
88
+ allowed: boolean;
89
+ retryAfter?: number;
90
+ }
91
+ export interface RateLimiter {
92
+ checkLimit(ip: string): RateLimitResult;
93
+ recordFailure(ip: string): void;
94
+ recordSuccess(ip: string): void;
95
+ destroy(): void;
96
+ }
97
+ /**
98
+ * Create a rate limiter for brute-force protection
99
+ * Uses in-memory Map with periodic cleanup
100
+ *
101
+ * @returns RateLimiter instance with checkLimit, recordFailure, recordSuccess, destroy
102
+ */
103
+ export declare function createRateLimiter(): RateLimiter;
104
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,aAAa,EAAE,eAAe,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC5J,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,aAAa,EAAE,eAAe,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,CAAC;AAE/H,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB;IAC5B,6CAA6C;;IAE7C,0CAA0C;;IAE1C,sCAAsC;;CAE9B,CAAC;AAEX,kFAAkF;AAClF,eAAO,MAAM,8BAA8B,QAAe,CAAC;AA0B3D;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CA0BlD;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBzE;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAWvC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAQxE;AAYD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,CAAC;IACxC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAmE/C"}
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ /**
3
+ * Token Authentication Core Module
4
+ * Issue #331: Token authentication and HTTPS support
5
+ *
6
+ * CONSTRAINTS:
7
+ * - C001: No Next.js module dependencies (next/headers, next/server, etc.)
8
+ * This module must be compatible with CLI build (tsconfig.cli.json)
9
+ * - S001: Token verification uses crypto.timingSafeEqual() (timing-safe comparison)
10
+ * - S002: AUTH_EXCLUDED_PATHS matching uses === (exact match, no startsWith)
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.DEFAULT_COOKIE_MAX_AGE_SECONDS = exports.RATE_LIMIT_CONFIG = exports.isValidTokenHash = exports.DEFAULT_EXPIRE_DURATION_MS = exports.computeExpireAt = exports.parseDuration = exports.AUTH_EXCLUDED_PATHS = exports.AUTH_COOKIE_NAME = void 0;
17
+ exports.generateToken = generateToken;
18
+ exports.hashToken = hashToken;
19
+ exports.verifyToken = verifyToken;
20
+ exports.parseCookies = parseCookies;
21
+ exports.isAuthEnabled = isAuthEnabled;
22
+ exports.getTokenMaxAge = getTokenMaxAge;
23
+ exports.isHttpsEnabled = isHttpsEnabled;
24
+ exports.buildAuthCookieOptions = buildAuthCookieOptions;
25
+ exports.createRateLimiter = createRateLimiter;
26
+ const crypto_1 = __importDefault(require("crypto"));
27
+ // Import and re-export shared constants and functions from Edge Runtime-compatible config
28
+ const auth_config_1 = require("../config/auth-config");
29
+ Object.defineProperty(exports, "AUTH_COOKIE_NAME", { enumerable: true, get: function () { return auth_config_1.AUTH_COOKIE_NAME; } });
30
+ Object.defineProperty(exports, "AUTH_EXCLUDED_PATHS", { enumerable: true, get: function () { return auth_config_1.AUTH_EXCLUDED_PATHS; } });
31
+ Object.defineProperty(exports, "parseDuration", { enumerable: true, get: function () { return auth_config_1.parseDuration; } });
32
+ Object.defineProperty(exports, "computeExpireAt", { enumerable: true, get: function () { return auth_config_1.computeExpireAt; } });
33
+ Object.defineProperty(exports, "DEFAULT_EXPIRE_DURATION_MS", { enumerable: true, get: function () { return auth_config_1.DEFAULT_EXPIRE_DURATION_MS; } });
34
+ Object.defineProperty(exports, "isValidTokenHash", { enumerable: true, get: function () { return auth_config_1.isValidTokenHash; } });
35
+ /** Rate limiting configuration for brute-force protection */
36
+ exports.RATE_LIMIT_CONFIG = {
37
+ /** Maximum failed attempts before lockout */
38
+ maxAttempts: 5,
39
+ /** Lockout duration in ms (15 minutes) */
40
+ lockoutDuration: 15 * 60 * 1000,
41
+ /** Cleanup interval in ms (1 hour) */
42
+ cleanupInterval: 60 * 60 * 1000,
43
+ };
44
+ /** Fallback cookie maxAge in seconds when no explicit expiry is set (24 hours) */
45
+ exports.DEFAULT_COOKIE_MAX_AGE_SECONDS = 24 * 60 * 60;
46
+ // ============================================================
47
+ // Module-level state (initialized from env at import time)
48
+ // ============================================================
49
+ /** The stored hash of the authentication token */
50
+ const storedTokenHash = (() => {
51
+ const hash = process.env.CM_AUTH_TOKEN_HASH || undefined;
52
+ if (!hash)
53
+ return undefined;
54
+ // Store validation result to avoid type predicate narrowing to 'never' in else branch
55
+ const valid = (0, auth_config_1.isValidTokenHash)(hash);
56
+ if (!valid) {
57
+ console.error(`[Security] CM_AUTH_TOKEN_HASH is not a valid 64-character hex string (got ${hash.length} chars). Authentication will be disabled.`);
58
+ return undefined;
59
+ }
60
+ return hash;
61
+ })();
62
+ /** Token expiration timestamp (ms since epoch) */
63
+ const expireAt = (0, auth_config_1.computeExpireAt)();
64
+ // ============================================================
65
+ // Token Functions
66
+ // ============================================================
67
+ /**
68
+ * Generate a cryptographically secure random token
69
+ * @returns 64-character hex string (32 bytes of entropy)
70
+ */
71
+ function generateToken() {
72
+ return crypto_1.default.randomBytes(32).toString('hex');
73
+ }
74
+ /**
75
+ * Hash a token using SHA-256
76
+ * @param token - The plain text token to hash
77
+ * @returns 64-character hex string (SHA-256 hash)
78
+ */
79
+ function hashToken(token) {
80
+ return crypto_1.default.createHash('sha256').update(token).digest('hex');
81
+ }
82
+ /**
83
+ * Verify a token against the stored hash
84
+ * S001: Uses crypto.timingSafeEqual() for timing-safe comparison
85
+ *
86
+ * @param token - The plain text token to verify
87
+ * @returns true if the token is valid and not expired
88
+ */
89
+ function verifyToken(token) {
90
+ if (!storedTokenHash) {
91
+ return false;
92
+ }
93
+ // Guard against undefined/null/empty token
94
+ if (!token || typeof token !== 'string') {
95
+ return false;
96
+ }
97
+ // Check expiration
98
+ if (expireAt !== null && Date.now() > expireAt) {
99
+ return false;
100
+ }
101
+ const tokenHash = hashToken(token);
102
+ // S001: timing-safe comparison to prevent timing attacks
103
+ const hashBuffer = Buffer.from(tokenHash, 'hex');
104
+ const storedBuffer = Buffer.from(storedTokenHash, 'hex');
105
+ if (hashBuffer.length !== storedBuffer.length) {
106
+ return false;
107
+ }
108
+ return crypto_1.default.timingSafeEqual(hashBuffer, storedBuffer);
109
+ }
110
+ // ============================================================
111
+ // Cookie Parsing (for WebSocket authentication)
112
+ // ============================================================
113
+ /**
114
+ * Parse a Cookie header string into key-value pairs
115
+ * Used by WebSocket upgrade handler where next/headers is not available
116
+ *
117
+ * @param cookieHeader - Raw Cookie header string
118
+ * @returns Parsed cookies as Record<string, string>
119
+ */
120
+ function parseCookies(cookieHeader) {
121
+ const cookies = {};
122
+ if (!cookieHeader) {
123
+ return cookies;
124
+ }
125
+ const pairs = cookieHeader.split(';');
126
+ for (const pair of pairs) {
127
+ const eqIndex = pair.indexOf('=');
128
+ if (eqIndex === -1) {
129
+ continue;
130
+ }
131
+ const name = pair.substring(0, eqIndex).trim();
132
+ const value = pair.substring(eqIndex + 1).trim();
133
+ if (name) {
134
+ cookies[name] = value;
135
+ }
136
+ }
137
+ return cookies;
138
+ }
139
+ // ============================================================
140
+ // Auth State Functions
141
+ // ============================================================
142
+ /**
143
+ * Check if authentication is enabled.
144
+ * Returns true only when CM_AUTH_TOKEN_HASH is set AND passes format validation.
145
+ * This prevents the state where auth appears enabled but login is impossible
146
+ * (e.g., when the hash value is malformed).
147
+ */
148
+ function isAuthEnabled() {
149
+ return !!storedTokenHash;
150
+ }
151
+ /**
152
+ * Calculate the Cookie maxAge in seconds (remaining token lifetime)
153
+ * @returns maxAge in seconds, or 0 if expired/no expiry
154
+ */
155
+ function getTokenMaxAge() {
156
+ if (expireAt === null) {
157
+ return 0;
158
+ }
159
+ const remaining = expireAt - Date.now();
160
+ if (remaining <= 0) {
161
+ return 0;
162
+ }
163
+ return Math.floor(remaining / 1000);
164
+ }
165
+ /**
166
+ * Check if HTTPS is enabled based on certificate environment variable
167
+ * @returns true if CM_HTTPS_CERT is set (indicating TLS certificates are configured)
168
+ */
169
+ function isHttpsEnabled() {
170
+ return !!process.env.CM_HTTPS_CERT;
171
+ }
172
+ /**
173
+ * Build authentication cookie options with consistent security settings.
174
+ * Centralizes cookie configuration to enforce HttpOnly, SameSite, and Secure flags.
175
+ *
176
+ * @param maxAge - Cookie max age in seconds. Use 0 to clear the cookie.
177
+ * @returns Cookie options object compatible with Next.js response.cookies.set()
178
+ */
179
+ function buildAuthCookieOptions(maxAge) {
180
+ return {
181
+ httpOnly: true,
182
+ sameSite: 'strict',
183
+ secure: isHttpsEnabled(),
184
+ maxAge,
185
+ path: '/',
186
+ };
187
+ }
188
+ /**
189
+ * Create a rate limiter for brute-force protection
190
+ * Uses in-memory Map with periodic cleanup
191
+ *
192
+ * @returns RateLimiter instance with checkLimit, recordFailure, recordSuccess, destroy
193
+ */
194
+ function createRateLimiter() {
195
+ const entries = new Map();
196
+ // Periodic cleanup of expired entries
197
+ const cleanupTimer = setInterval(() => {
198
+ const now = Date.now();
199
+ for (const [ip, entry] of entries) {
200
+ // Remove entries whose lockout has expired (or was never set)
201
+ // and whose last attempt is older than the lockout duration
202
+ const isLockoutExpired = entry.lockedUntil === null || now > entry.lockedUntil;
203
+ const isStale = now - entry.lastAttempt > exports.RATE_LIMIT_CONFIG.lockoutDuration;
204
+ if (isLockoutExpired && isStale) {
205
+ entries.delete(ip);
206
+ }
207
+ }
208
+ }, exports.RATE_LIMIT_CONFIG.cleanupInterval);
209
+ // Ensure timer doesn't prevent process exit
210
+ if (cleanupTimer.unref) {
211
+ cleanupTimer.unref();
212
+ }
213
+ return {
214
+ checkLimit(ip) {
215
+ const entry = entries.get(ip);
216
+ if (!entry) {
217
+ return { allowed: true };
218
+ }
219
+ // Check if lockout has expired
220
+ if (entry.lockedUntil !== null) {
221
+ const now = Date.now();
222
+ if (now < entry.lockedUntil) {
223
+ const retryAfter = Math.ceil((entry.lockedUntil - now) / 1000);
224
+ return { allowed: false, retryAfter };
225
+ }
226
+ // Lockout expired - reset entry
227
+ entry.attempts = 0;
228
+ entry.lockedUntil = null;
229
+ }
230
+ return { allowed: true };
231
+ },
232
+ recordFailure(ip) {
233
+ const now = Date.now();
234
+ const entry = entries.get(ip) || { attempts: 0, lockedUntil: null, lastAttempt: now };
235
+ entry.attempts++;
236
+ entry.lastAttempt = now;
237
+ if (entry.attempts >= exports.RATE_LIMIT_CONFIG.maxAttempts) {
238
+ entry.lockedUntil = now + exports.RATE_LIMIT_CONFIG.lockoutDuration;
239
+ }
240
+ entries.set(ip, entry);
241
+ },
242
+ recordSuccess(ip) {
243
+ entries.delete(ip);
244
+ },
245
+ destroy() {
246
+ clearInterval(cleanupTimer);
247
+ entries.clear();
248
+ },
249
+ };
250
+ }
@@ -2,6 +2,7 @@
2
2
  /**
3
3
  * Custom Next.js Server with WebSocket Support
4
4
  * Integrates WebSocket server for real-time communication
5
+ * Issue #331: HTTPS support and auth cleanup on shutdown
5
6
  */
6
7
  var __importDefault = (this && this.__importDefault) || function (mod) {
7
8
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -28,6 +29,9 @@ process.on('uncaughtException', (error) => {
28
29
  process.exit(1);
29
30
  });
30
31
  const http_1 = require("http");
32
+ const https_1 = require("https");
33
+ const fs_1 = require("fs");
34
+ const fs_2 = require("fs");
31
35
  const url_1 = require("url");
32
36
  const next_1 = __importDefault(require("next"));
33
37
  const ws_server_1 = require("./src/lib/ws-server");
@@ -42,22 +46,116 @@ const db_1 = require("./src/lib/db");
42
46
  const dev = process.env.NODE_ENV !== 'production';
43
47
  const hostname = (0, env_1.getEnvByKey)('CM_BIND') || '127.0.0.1';
44
48
  const port = parseInt((0, env_1.getEnvByKey)('CM_PORT') || '3000', 10);
49
+ // Issue #331: HTTPS configuration
50
+ const certPath = process.env.CM_HTTPS_CERT;
51
+ const keyPath = process.env.CM_HTTPS_KEY;
52
+ /** Maximum certificate file size: 1MB */
53
+ const MAX_CERT_FILE_SIZE = 1024 * 1024;
54
+ /**
55
+ * Validate certificate file path for security
56
+ * Issue #331: Certificate path validation
57
+ */
58
+ function validateCertPath(filePath, label) {
59
+ if (!(0, fs_1.existsSync)(filePath)) {
60
+ console.error(`[Security] ${label} file not found: ${filePath}`);
61
+ // ExitCode.CONFIG_ERROR = 2 (direct number, not imported from CLI types)
62
+ process.exit(2);
63
+ }
64
+ try {
65
+ (0, fs_1.accessSync)(filePath, fs_2.constants.R_OK);
66
+ }
67
+ catch {
68
+ console.error(`[Security] ${label} file not readable: ${filePath}`);
69
+ process.exit(2);
70
+ }
71
+ const realPath = (0, fs_1.realpathSync)(filePath);
72
+ const stats = (0, fs_1.statSync)(realPath);
73
+ if (stats.size > MAX_CERT_FILE_SIZE) {
74
+ console.error(`[Security] ${label} file too large (${stats.size} bytes, max ${MAX_CERT_FILE_SIZE}): ${filePath}`);
75
+ process.exit(2);
76
+ }
77
+ return realPath;
78
+ }
45
79
  // Create Next.js app
46
80
  const app = (0, next_1.default)({ dev, hostname, port });
47
81
  const handle = app.getRequestHandler();
82
+ // Issue #331: Prevent Next.js (NextCustomServer) from registering its own upgrade
83
+ // event listener on the HTTP server.
84
+ //
85
+ // When next() is called without customServer:false, it creates a NextCustomServer
86
+ // instance. NextCustomServer.getRequestHandler() lazily calls setupWebSocketHandler()
87
+ // on the first HTTP request, which registers an upgrade listener that calls
88
+ // router-server's upgradeHandler → resolveRoutes({ res: socket }) → middleware match
89
+ // → serverResult.requestHandler(req, socket, parsedUrl). This passes the raw TCP
90
+ // socket as the HTTP response object, causing:
91
+ // TypeError: Cannot read properties of undefined (reading 'bind')
92
+ // in handleRequestImpl when it tries to call _res.setHeader.bind(_res).
93
+ //
94
+ // Making setupWebSocketHandler a no-op prevents this listener from being added.
95
+ // All WebSocket upgrades are handled by ws-server.ts (our own upgrade listener).
96
+ app.setupWebSocketHandler = () => { };
48
97
  app.prepare().then(() => {
49
- // Create HTTP server
50
- const server = (0, http_1.createServer)(async (req, res) => {
98
+ // Request handler for both HTTP and HTTPS
99
+ const requestHandler = async (req, res) => {
100
+ // Guard: res must be a proper HTTP ServerResponse (not a raw net.Socket).
101
+ // Defense-in-depth: normally not needed after the setupWebSocketHandler fix above,
102
+ // but kept for safety in case any other path passes a non-ServerResponse object.
103
+ if (typeof res?.setHeader !== 'function') {
104
+ return;
105
+ }
106
+ // Issue #332: Inject X-Real-IP header for IP restriction
107
+ // [S3-005] This applies to HTTP requests only. WebSocket upgrade requests
108
+ // are skipped below and handled by ws-server.ts (which uses socket.remoteAddress directly).
109
+ const clientIp = req.socket.remoteAddress || '';
110
+ if (process.env.CM_TRUST_PROXY !== 'true') {
111
+ // CM_TRUST_PROXY=false: always overwrite to prevent forged headers
112
+ req.headers['x-real-ip'] = clientIp;
113
+ }
114
+ else {
115
+ // CM_TRUST_PROXY=true: only set if X-Forwarded-For is absent
116
+ if (!req.headers['x-forwarded-for']) {
117
+ req.headers['x-real-ip'] = clientIp;
118
+ }
119
+ }
120
+ // Skip WebSocket upgrade requests - they are handled by the server 'upgrade' event.
121
+ if (req.headers['upgrade']) {
122
+ return;
123
+ }
124
+ const method = req.method ?? 'UNKNOWN';
125
+ const url = req.url ?? '/';
51
126
  try {
52
- const parsedUrl = (0, url_1.parse)(req.url, true);
127
+ const parsedUrl = (0, url_1.parse)(url, true);
53
128
  await handle(req, res, parsedUrl);
54
129
  }
55
130
  catch (err) {
56
- console.error('Error handling request', err);
57
- res.statusCode = 500;
58
- res.end('Internal Server Error');
131
+ console.error(`handleRequestImpl failed: ${method} ${url}`, err);
132
+ if (!res.headersSent) {
133
+ res.statusCode = 500;
134
+ res.end('Internal Server Error');
135
+ }
59
136
  }
60
- });
137
+ };
138
+ // Issue #331: Create HTTP or HTTPS server
139
+ let server;
140
+ let protocol = 'http';
141
+ if (certPath && keyPath) {
142
+ const validatedCertPath = validateCertPath(certPath, 'Certificate');
143
+ const validatedKeyPath = validateCertPath(keyPath, 'Key');
144
+ try {
145
+ const cert = (0, fs_1.readFileSync)(validatedCertPath);
146
+ const key = (0, fs_1.readFileSync)(validatedKeyPath);
147
+ server = (0, https_1.createServer)({ cert, key }, requestHandler);
148
+ protocol = 'https';
149
+ console.log('HTTPS server created with TLS certificates');
150
+ }
151
+ catch (error) {
152
+ console.error('Failed to read TLS certificates:', error);
153
+ process.exit(2);
154
+ }
155
+ }
156
+ else {
157
+ server = (0, http_1.createServer)(requestHandler);
158
+ }
61
159
  // Setup WebSocket server
62
160
  (0, ws_server_1.setupWebSocket)(server);
63
161
  // Scan and sync worktrees on startup
@@ -103,16 +201,29 @@ app.prepare().then(() => {
103
201
  const worktrees = await (0, worktrees_1.scanMultipleRepositories)(filteredPaths);
104
202
  // Sync to database
105
203
  (0, worktrees_1.syncWorktreesToDB)(db, worktrees);
106
- console.log(`✓ Total: ${worktrees.length} worktree(s) synced to database`);
204
+ console.log(`Total: ${worktrees.length} worktree(s) synced to database`);
107
205
  }
108
206
  catch (error) {
109
207
  console.error('Error initializing worktrees:', error);
110
208
  }
111
209
  }
112
- server.listen(port, async (err) => {
113
- if (err)
114
- throw err;
115
- console.log(`> Ready on http://${hostname}:${port}`);
210
+ // H3 fix: Pass hostname to listen() so CM_BIND is respected.
211
+ // Note: http.Server.listen(port, hostname, callback) does not pass err to callback;
212
+ // listen errors emit an 'error' event instead.
213
+ server.on('error', (err) => {
214
+ if (err.code === 'EADDRINUSE') {
215
+ console.error(`Port ${port} is already in use`);
216
+ }
217
+ else if (err.code === 'EADDRNOTAVAIL') {
218
+ console.error(`Address ${hostname}:${port} is not available`);
219
+ }
220
+ else {
221
+ console.error('Server error:', err);
222
+ }
223
+ process.exit(1);
224
+ });
225
+ server.listen(port, hostname, async () => {
226
+ console.log(`> Ready on ${protocol}://${hostname}:${port}`);
116
227
  console.log(`> WebSocket server ready`);
117
228
  // Initialize worktrees after server starts
118
229
  await initializeWorktrees();
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Authentication Configuration Constants
4
+ * Issue #331: Shared constants for auth.ts and middleware.ts
5
+ *
6
+ * CONSTRAINT: This module must be Edge Runtime compatible.
7
+ * No Node.js-specific imports (crypto, fs, etc.) are allowed.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DEFAULT_EXPIRE_DURATION_MS = exports.AUTH_EXCLUDED_PATHS = exports.AUTH_COOKIE_NAME = void 0;
11
+ exports.isValidTokenHash = isValidTokenHash;
12
+ exports.parseDuration = parseDuration;
13
+ exports.computeExpireAt = computeExpireAt;
14
+ /** Cookie name for authentication token */
15
+ exports.AUTH_COOKIE_NAME = 'cm_auth_token';
16
+ /** Valid SHA-256 hex string pattern: exactly 64 hex characters */
17
+ const VALID_TOKEN_HASH_PATTERN = /^[0-9a-f]{64}$/;
18
+ /**
19
+ * Validate that a CM_AUTH_TOKEN_HASH value is a well-formed SHA-256 hex string.
20
+ * Used by both auth.ts and middleware.ts to ensure consistent auth-enabled detection.
21
+ * Returns a type predicate so callers can narrow the type after checking.
22
+ *
23
+ * @param hash - The hash string to validate
24
+ * @returns true if the hash is a valid 64-character hex string
25
+ */
26
+ function isValidTokenHash(hash) {
27
+ return !!hash && VALID_TOKEN_HASH_PATTERN.test(hash);
28
+ }
29
+ /**
30
+ * Paths excluded from authentication check.
31
+ * S002: Must use === for matching (no startsWith - bypass attack prevention)
32
+ */
33
+ exports.AUTH_EXCLUDED_PATHS = [
34
+ '/login',
35
+ '/api/auth/login',
36
+ '/api/auth/logout',
37
+ '/api/auth/status',
38
+ ];
39
+ // ============================================================
40
+ // Duration Parsing (Edge Runtime compatible)
41
+ // ============================================================
42
+ /** Milliseconds in one minute */
43
+ const MS_PER_MINUTE = 60 * 1000;
44
+ /** Milliseconds in one hour */
45
+ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
46
+ /** Milliseconds in one day */
47
+ const MS_PER_DAY = 24 * MS_PER_HOUR;
48
+ /** Default token expiration duration (24 hours) */
49
+ exports.DEFAULT_EXPIRE_DURATION_MS = 24 * MS_PER_HOUR;
50
+ /** Minimum duration: 1 hour */
51
+ const MIN_DURATION_MS = MS_PER_HOUR;
52
+ /** Maximum duration: 30 days */
53
+ const MAX_DURATION_MS = 30 * MS_PER_DAY;
54
+ /**
55
+ * Parse a duration string into milliseconds.
56
+ * Supported formats: Nh (hours), Nd (days), Nm (minutes)
57
+ * Minimum: 1h, Maximum: 30d
58
+ *
59
+ * @param s - Duration string (e.g., "24h", "7d", "90m")
60
+ * @returns Duration in milliseconds
61
+ * @throws Error if format is invalid or out of range
62
+ */
63
+ function parseDuration(s) {
64
+ const match = s.match(/^(\d+)([hdm])$/);
65
+ if (!match) {
66
+ throw new Error(`Invalid duration format: "${s}". Use Nh, Nd, or Nm (e.g., "24h", "7d", "90m")`);
67
+ }
68
+ const value = parseInt(match[1], 10);
69
+ const unit = match[2];
70
+ /** Map of duration unit characters to their millisecond multipliers */
71
+ const unitMultipliers = {
72
+ h: MS_PER_HOUR,
73
+ d: MS_PER_DAY,
74
+ m: MS_PER_MINUTE,
75
+ };
76
+ const multiplier = unitMultipliers[unit];
77
+ if (multiplier === undefined) {
78
+ throw new Error(`Invalid duration unit: "${unit}"`);
79
+ }
80
+ const ms = value * multiplier;
81
+ if (ms < MIN_DURATION_MS) {
82
+ throw new Error(`Duration too short: minimum is 1h (60m). Got: "${s}"`);
83
+ }
84
+ if (ms > MAX_DURATION_MS) {
85
+ throw new Error(`Duration too long: maximum is 30d (720h). Got: "${s}"`);
86
+ }
87
+ return ms;
88
+ }
89
+ /**
90
+ * Compute token expiration timestamp from environment variables.
91
+ * Used by both auth.ts (Node.js) and middleware.ts (Edge Runtime).
92
+ *
93
+ * @returns Expiration timestamp (ms since epoch), or null if auth is not enabled
94
+ */
95
+ function computeExpireAt() {
96
+ const expireStr = process.env.CM_AUTH_EXPIRE;
97
+ const now = Date.now();
98
+ if (expireStr) {
99
+ try {
100
+ return now + parseDuration(expireStr);
101
+ }
102
+ catch {
103
+ // Invalid duration format - use default
104
+ return now + exports.DEFAULT_EXPIRE_DURATION_MS;
105
+ }
106
+ }
107
+ // Default 24h if auth is enabled
108
+ if (process.env.CM_AUTH_TOKEN_HASH) {
109
+ return now + exports.DEFAULT_EXPIRE_DURATION_MS;
110
+ }
111
+ return null;
112
+ }