@cuongtran001/kanna 1.71.2 → 1.72.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 (69) hide show
  1. package/dist/client/assets/{BoardsPage-DudYbDyu.js → BoardsPage-Cv8FIzb-.js} +1 -1
  2. package/dist/client/assets/{BoardsRoutePage-DUvJAyzh.js → BoardsRoutePage-D2B4CKzS.js} +1 -1
  3. package/dist/client/assets/{ChatPage-ChFKw7nK.js → ChatPage-DyVlp7tq.js} +1 -1
  4. package/dist/client/assets/{CronJobRow-DU1RtrCs.js → CronJobRow-DtlnNrFa.js} +1 -1
  5. package/dist/client/assets/{CronJobsPage-cjH7p3lh.js → CronJobsPage-BqBSaj2f.js} +1 -1
  6. package/dist/client/assets/{FilePreviewSheet-wcpWIb90.js → FilePreviewSheet-cAupHL1-.js} +1 -1
  7. package/dist/client/assets/{FlowSurface-Doyyct0h.js → FlowSurface-Cy6Ypvp4.js} +1 -1
  8. package/dist/client/assets/{GenUIBlock-D-TBN4Um.js → GenUIBlock-DS3aMR8o.js} +2 -2
  9. package/dist/client/assets/{KannaSocketProvider-uWU6WkNo.js → KannaSocketProvider-0GNmyg3O.js} +1 -1
  10. package/dist/client/assets/{LocalProjectsPage-D8V8a3WM.js → LocalProjectsPage-DQQtJlM1.js} +1 -1
  11. package/dist/client/assets/{SettingsPage-CiCU3nka.js → SettingsPage-X5P3EyCv.js} +1 -1
  12. package/dist/client/assets/{StackBoardsRoutePage-DDvdmA3Y.js → StackBoardsRoutePage-Dq3kQSm2.js} +1 -1
  13. package/dist/client/assets/{ToolCallMessage-CHJSiauG.js → ToolCallMessage-Dhc2sf0Y.js} +1 -1
  14. package/dist/client/assets/{VChartSurface-DVRoAi5e.js → VChartSurface-CWzLshgk.js} +1 -1
  15. package/dist/client/assets/{WorkflowsPage-LwL034ZP.js → WorkflowsPage-BW0zI7T4.js} +1 -1
  16. package/dist/client/assets/{WorkflowsSection-HlGaR0pJ.js → WorkflowsSection-yOlBEJiD.js} +1 -1
  17. package/dist/client/assets/{chart-renderer-BZXJQWtw.js → chart-renderer-CARfWmNI.js} +1 -1
  18. package/dist/client/assets/{chatSounds-BNvUSPAi.js → chatSounds-BgTY66uu.js} +1 -1
  19. package/dist/client/assets/chevron-left-CFGYD17H.js +1 -0
  20. package/dist/client/assets/{clipboard.adapter-CIRv1YNb.js → clipboard.adapter-DpHeqwMw.js} +1 -1
  21. package/dist/client/assets/{columnStyle-mS8DKLCV.js → columnStyle-DYXYcPnt.js} +1 -1
  22. package/dist/client/assets/{copyFeedbackStore-DHAfKV4i.js → copyFeedbackStore-DZw_cFtR.js} +1 -1
  23. package/dist/client/assets/{createScopedStore-or82_bxr.js → createScopedStore-BVEubecu.js} +1 -1
  24. package/dist/client/assets/{dist-Cvh6FEf5.js → dist-B_c3yYUS.js} +1 -1
  25. package/dist/client/assets/{ellipsis-CWiRFysC.js → ellipsis-63_6PNeI.js} +1 -1
  26. package/dist/client/assets/{flow-kinds-Digyo6AB.js → flow-kinds-CG9-P5sO.js} +1 -1
  27. package/dist/client/assets/{folder-CnSb5m9v.js → folder-uf9sKjmp.js} +1 -1
  28. package/dist/client/assets/{formatDuration-DsrNkArK.js → formatDuration-DXQnrqg6.js} +1 -1
  29. package/dist/client/assets/{genui-BagNQVNI.js → genui-D649Dv92.js} +1 -1
  30. package/dist/client/assets/{globe-tc_9UiU3.js → globe-DRxtkxRw.js} +1 -1
  31. package/dist/client/assets/{host-C7I7nFuH.js → host-BpGUm8Ks.js} +1 -1
  32. package/dist/client/assets/{index-D9OYZjb7.js → index-BIXIaFOx.js} +3 -3
  33. package/dist/client/assets/{info-CgLDBnLR.js → info-CzLBGkjs.js} +1 -1
  34. package/dist/client/assets/{input-BaAZ4OHy.js → input-Dj2AfDyu.js} +1 -1
  35. package/dist/client/assets/{lexicalToReact-ClgTGbzM.js → lexicalToReact-DTdGRuI2.js} +3 -3
  36. package/dist/client/assets/{log-Ch_SNBXH.js → log-C-CyaVcQ.js} +1 -1
  37. package/dist/client/assets/{monitor-fFRMWd1S.js → monitor-Cl6GABTi.js} +1 -1
  38. package/dist/client/assets/{pendingActionsStore-CWNaCyjS.js → pendingActionsStore-CpbHd9ei.js} +1 -1
  39. package/dist/client/assets/{play-DD0bVaYg.js → play-cuJ4aGHN.js} +1 -1
  40. package/dist/client/assets/{plus-By3sppx5.js → plus-DpSRUvXD.js} +1 -1
  41. package/dist/client/assets/{popover-CJNN95M0.js → popover-C_I8IOWx.js} +1 -1
  42. package/dist/client/assets/{pushClient-DbwI0iz1.js → pushClient-CtuTxUvx.js} +1 -1
  43. package/dist/client/assets/runDetached-QSRrHU3F.js +1 -0
  44. package/dist/client/assets/{segmented-control-DYmNRyQy.js → segmented-control-QukGPBuR.js} +1 -1
  45. package/dist/client/assets/{select-Dam9e0mi.js → select-BgMpt5jO.js} +1 -1
  46. package/dist/client/assets/{settings-header-button-DaCbsV31.js → settings-header-button-BLAONCyF.js} +1 -1
  47. package/dist/client/assets/{sidebarPendingActions-DxI6ubAL.js → sidebarPendingActions-Bde5y5HH.js} +1 -1
  48. package/dist/client/assets/{square-pen-DKZvXh3R.js → square-pen-CIU74x0s.js} +1 -1
  49. package/dist/client/assets/{state-mark-DxzVQsVa.js → state-mark-DS7GjjiN.js} +1 -1
  50. package/dist/client/assets/{status-pill-BFLy7eN9.js → status-pill-Bm1d4Yt2.js} +1 -1
  51. package/dist/client/assets/{terminal-DRYQ5avJ.js → terminal-D4dPFOU3.js} +1 -1
  52. package/dist/client/assets/{timer.adapter-BDh0KX9G.js → timer.adapter-5aM9hrUB.js} +1 -1
  53. package/dist/client/assets/{tools-toomKvSw.js → tools-5kr-voMM.js} +1 -1
  54. package/dist/client/assets/{triangle-alert-CM5uh-kq.js → triangle-alert-BAPOUptD.js} +1 -1
  55. package/dist/client/assets/{useAppGlobalState-BCUZEZEi.js → useAppGlobalState-BNrYZSMc.js} +1 -1
  56. package/dist/client/assets/{useKannaState-BvJAZTpL.js → useKannaState-DiqCTk6A.js} +1 -1
  57. package/dist/client/assets/{useTheme-B6cYOPpe.js → useTheme-DOxNN3XT.js} +1 -1
  58. package/dist/client/index.html +33 -33
  59. package/package.json +2 -1
  60. package/src/server/auth-backend.ts +16 -0
  61. package/src/server/auth.test.ts +62 -1
  62. package/src/server/auth.ts +90 -87
  63. package/src/server/better-auth.adapter.ts +197 -0
  64. package/src/server/http-dispatcher.ts +5 -5
  65. package/src/server/server.ts +7 -8
  66. package/dist/client/assets/chevron-left-FnMbL2tA.js +0 -1
  67. package/dist/client/assets/runDetached-CQOu05LK.js +0 -1
  68. package/src/server/auth-session-store.adapter.ts +0 -224
  69. package/src/server/auth-session-store.test.ts +0 -94
@@ -1,9 +1,11 @@
1
- import { randomBytes, timingSafeEqual } from "node:crypto"
2
- import type { AuthSessionStore } from "./auth-session-store.adapter"
3
1
  import { isJsonObject, type JsonValue } from "../shared/json"
4
-
5
- const SESSION_COOKIE_NAME = "kanna_session"
6
- const TOUCH_THROTTLE_MS = 60 * 1000
2
+ import {
3
+ CLIENT_IP_HEADER,
4
+ OWNER_EMAIL,
5
+ SESSION_COOKIE_NAME,
6
+ SIGN_IN_PATH,
7
+ type AuthBackend,
8
+ } from "./auth-backend"
7
9
 
8
10
  export interface AuthStatusPayload {
9
11
  enabled: boolean
@@ -11,12 +13,12 @@ export interface AuthStatusPayload {
11
13
  }
12
14
 
13
15
  export interface AuthManager {
14
- isAuthenticated(req: Request): boolean
16
+ isAuthenticated(req: Request): Promise<boolean>
15
17
  validateOrigin(req: Request): boolean
16
18
  redirectToApp(req: Request): Response
17
- handleLogin(req: Request, nextPath: string): Promise<Response>
18
- handleLogout(req: Request): Response
19
- handleStatus(req: Request): Response
19
+ handleLogin(req: Request, nextPath: string, clientIp: string | null): Promise<Response>
20
+ handleLogout(req: Request): Promise<Response>
21
+ handleStatus(req: Request): Promise<Response>
20
22
  dispose(): Promise<void>
21
23
  }
22
24
 
@@ -68,26 +70,19 @@ function shouldUseSecureCookie(req: Request, trustProxy: boolean) {
68
70
  return new URL(req.url).protocol === "https:"
69
71
  }
70
72
 
71
- function buildCookie(
72
- name: string,
73
- value: string,
74
- req: Request,
75
- trustProxy: boolean,
76
- maxAgeSeconds: number,
77
- ) {
78
- const parts = [
73
+ function isLoopbackAddress(ip: string | null) {
74
+ if (!ip) return false
75
+ return ip === "::1" || ip.startsWith("127.") || ip.startsWith("::ffff:127.")
76
+ }
77
+
78
+ function buildCookie(name: string, value: string, maxAgeSeconds: number) {
79
+ return [
79
80
  `${name}=${encodeURIComponent(value)}`,
80
81
  "Path=/",
81
82
  "HttpOnly",
82
83
  "SameSite=Strict",
83
84
  `Max-Age=${maxAgeSeconds}`,
84
- ]
85
-
86
- if (shouldUseSecureCookie(req, trustProxy)) {
87
- parts.push("Secure")
88
- }
89
-
90
- return parts.join("; ")
85
+ ].join("; ")
91
86
  }
92
87
 
93
88
  async function readLoginForm(req: Request) {
@@ -112,8 +107,6 @@ async function readLoginForm(req: Request) {
112
107
 
113
108
  export interface AuthManagerOptions {
114
109
  trustProxy?: boolean
115
- sessionStore: AuthSessionStore
116
- getMaxAgeMs: () => number
117
110
  }
118
111
 
119
112
  export function isPublicSharePath(url: string): boolean {
@@ -129,35 +122,38 @@ export function isPublicSharePath(url: string): boolean {
129
122
  || pathname.startsWith("/assets/share-view/")
130
123
  }
131
124
 
132
- export function createAuthManager(password: string, options: AuthManagerOptions): AuthManager {
133
- const expectedPassword = Buffer.from(password)
125
+ export function createAuthManager(backend: AuthBackend, options: AuthManagerOptions = {}): AuthManager {
134
126
  const trustProxy = options.trustProxy ?? false
135
- const sessionStore = options.sessionStore
136
- const getMaxAgeMs = options.getMaxAgeMs
137
- const lastTouchedAt = new Map<string, number>()
138
127
 
139
- function getSessionToken(req: Request) {
140
- return parseCookies(req.headers.get("cookie")).get(SESSION_COOKIE_NAME) ?? null
128
+ function sessionHeaders(req: Request) {
129
+ const headers = new Headers()
130
+ const cookie = req.headers.get("cookie")
131
+ if (cookie) headers.set("cookie", cookie)
132
+ return headers
141
133
  }
142
134
 
143
- function maybeTouchSession(token: string) {
144
- const now = Date.now()
145
- const last = lastTouchedAt.get(token)
146
- if (last !== undefined && now - last < TOUCH_THROTTLE_MS) return
147
- lastTouchedAt.set(token, now)
148
- sessionStore.touch(token, getMaxAgeMs())
135
+ function hasSessionCookie(req: Request) {
136
+ return parseCookies(req.headers.get("cookie")).has(SESSION_COOKIE_NAME)
149
137
  }
150
138
 
151
- function isAuthenticated(req: Request) {
152
- const sessionToken = getSessionToken(req)
153
- if (!sessionToken) return false
154
- const session = sessionStore.validate(sessionToken)
155
- if (!session) {
156
- lastTouchedAt.delete(sessionToken)
157
- return false
139
+ function withSecureAttribute(req: Request, setCookie: string) {
140
+ return shouldUseSecureCookie(req, trustProxy) ? `${setCookie}; Secure` : setCookie
141
+ }
142
+
143
+ function appendCookies(response: Response, req: Request, setCookies: string[]) {
144
+ for (const setCookie of setCookies) {
145
+ response.headers.append("Set-Cookie", withSecureAttribute(req, setCookie))
158
146
  }
159
- maybeTouchSession(sessionToken)
160
- return true
147
+ return response
148
+ }
149
+
150
+ async function readSession(req: Request) {
151
+ if (!hasSessionCookie(req)) return { authenticated: false, setCookies: [] }
152
+ return backend.getSession(sessionHeaders(req))
153
+ }
154
+
155
+ async function isAuthenticated(req: Request) {
156
+ return (await readSession(req)).authenticated
161
157
  }
162
158
 
163
159
  function validateOrigin(req: Request) {
@@ -168,35 +164,37 @@ export function createAuthManager(password: string, options: AuthManagerOptions)
168
164
  return origin === effectiveOrigin(req, trustProxy)
169
165
  }
170
166
 
171
- function createSessionCookie(req: Request) {
172
- const sessionToken = randomBytes(32).toString("base64url")
173
- const maxAgeMs = getMaxAgeMs()
174
- sessionStore.create(sessionToken, maxAgeMs)
175
- return buildCookie(SESSION_COOKIE_NAME, sessionToken, req, trustProxy, Math.floor(maxAgeMs / 1000))
176
- }
177
-
178
- function clearSessionCookie(req: Request) {
179
- const sessionToken = getSessionToken(req)
180
- if (sessionToken) {
181
- sessionStore.revoke(sessionToken)
182
- lastTouchedAt.delete(sessionToken)
183
- }
184
- return buildCookie(SESSION_COOKIE_NAME, "", req, trustProxy, 0)
185
- }
186
-
187
- function verifyPassword(candidate: string) {
188
- const actual = Buffer.from(candidate)
189
- if (actual.length !== expectedPassword.length) {
190
- return false
191
- }
192
- return timingSafeEqual(actual, expectedPassword)
193
- }
194
-
195
- function handleStatus(req: Request) {
196
- return Response.json({
167
+ function resolveClientIp(req: Request, socketIp: string | null) {
168
+ if (!trustProxy || !isLoopbackAddress(socketIp)) return socketIp
169
+ const cloudflare = req.headers.get("cf-connecting-ip")?.trim()
170
+ if (cloudflare) return cloudflare
171
+ const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
172
+ return forwarded || socketIp
173
+ }
174
+
175
+ function buildSignInRequest(req: Request, password: string, clientIp: string | null) {
176
+ const origin = effectiveOrigin(req, trustProxy)
177
+ const headers = new Headers({
178
+ "content-type": "application/json",
179
+ origin,
180
+ })
181
+ const userAgent = req.headers.get("user-agent")
182
+ if (userAgent) headers.set("user-agent", userAgent)
183
+ if (clientIp) headers.set(CLIENT_IP_HEADER, clientIp)
184
+ return new Request(`${origin}${SIGN_IN_PATH}`, {
185
+ method: "POST",
186
+ headers,
187
+ body: JSON.stringify({ email: OWNER_EMAIL, password, rememberMe: true }),
188
+ })
189
+ }
190
+
191
+ async function handleStatus(req: Request) {
192
+ const session = await readSession(req)
193
+ const response = Response.json({
197
194
  enabled: true,
198
- authenticated: isAuthenticated(req),
195
+ authenticated: session.authenticated,
199
196
  } satisfies AuthStatusPayload)
197
+ return appendCookies(response, req, session.setCookies)
200
198
  }
201
199
 
202
200
  function redirectToApp(req: Request) {
@@ -204,35 +202,40 @@ export function createAuthManager(password: string, options: AuthManagerOptions)
204
202
  return Response.redirect(new URL(sanitizeNextPath(currentUrl.searchParams.get("next")), effectiveOrigin(req, trustProxy)), 302)
205
203
  }
206
204
 
207
- async function handleLogin(req: Request, fallbackNextPath: string) {
205
+ async function handleLogin(req: Request, fallbackNextPath: string, socketIp: string | null) {
208
206
  if (!validateOrigin(req)) {
209
207
  return Response.json({ error: "Forbidden" }, { status: 403 })
210
208
  }
211
209
 
212
- const { password: candidate, nextPath } = await readLoginForm(req)
213
- if (!verifyPassword(candidate)) {
210
+ const { password, nextPath } = await readLoginForm(req)
211
+ const upstream = await backend.signIn(buildSignInRequest(req, password, resolveClientIp(req, socketIp)))
212
+
213
+ if (upstream.status === 429) {
214
+ const limited = Response.json({ error: "Too many attempts" }, { status: 429 })
215
+ const retryAfter = upstream.headers.get("retry-after")
216
+ if (retryAfter) limited.headers.set("Retry-After", retryAfter)
217
+ return limited
218
+ }
219
+ if (!upstream.ok) {
214
220
  return Response.json({ error: "Invalid password" }, { status: 401 })
215
221
  }
216
222
 
217
223
  const response = Response.json({ ok: true, nextPath: sanitizeNextPath(nextPath || fallbackNextPath) })
218
-
219
- response.headers.set("Set-Cookie", createSessionCookie(req))
220
- return response
224
+ return appendCookies(response, req, upstream.headers.getSetCookie())
221
225
  }
222
226
 
223
- function handleLogout(req: Request) {
227
+ async function handleLogout(req: Request) {
224
228
  if (!validateOrigin(req)) {
225
229
  return Response.json({ error: "Forbidden" }, { status: 403 })
226
230
  }
227
231
 
228
- const response = Response.json({ ok: true })
229
- response.headers.set("Set-Cookie", clearSessionCookie(req))
230
- return response
232
+ const issued = hasSessionCookie(req) ? await backend.signOut(sessionHeaders(req)) : []
233
+ const clearing = issued.length > 0 ? issued : [buildCookie(SESSION_COOKIE_NAME, "", 0)]
234
+ return appendCookies(Response.json({ ok: true }), req, clearing)
231
235
  }
232
236
 
233
237
  async function dispose() {
234
- lastTouchedAt.clear()
235
- await sessionStore.dispose()
238
+ await backend.dispose()
236
239
  }
237
240
 
238
241
  return {
@@ -0,0 +1,197 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { randomBytes } from "node:crypto"
3
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises"
4
+ import path from "node:path"
5
+ import { betterAuth, type BetterAuthOptions } from "better-auth"
6
+ import { getMigrations } from "better-auth/db/migration"
7
+ import { isErrnoException, toError } from "../shared/errors"
8
+ import { log } from "../shared/log"
9
+ import type { AppSettingsManager } from "./app-settings"
10
+ import { createAuthManager, type AuthManager } from "./auth"
11
+ import { CLIENT_IP_HEADER, OWNER_EMAIL, SESSION_COOKIE_NAME, type AuthBackend } from "./auth-backend"
12
+
13
+ const SECRET_FILE = "auth-secret"
14
+ const DATABASE_FILE = "auth.db"
15
+ const LEGACY_SESSIONS_FILE = "sessions.json"
16
+ const SECONDS_PER_DAY = 86_400
17
+ const SESSION_UPDATE_AGE_SECONDS = SECONDS_PER_DAY
18
+ const AUTH_BASE_PATH = "/api/auth"
19
+ const INTERNAL_BASE_URL = "http://localhost"
20
+ const MAX_PASSWORD_LENGTH = 4096
21
+ const SECRET_BYTES = 32
22
+
23
+ async function loadOrCreateSecret(filePath: string): Promise<string> {
24
+ try {
25
+ const existing = (await readFile(filePath, "utf8")).trim()
26
+ if (existing) return existing
27
+ } catch (error) {
28
+ if (!isErrnoException(error) || error.code !== "ENOENT") throw error
29
+ }
30
+ const secret = randomBytes(SECRET_BYTES).toString("base64url")
31
+ await writeFile(filePath, secret, { mode: 0o600 })
32
+ return secret
33
+ }
34
+
35
+ async function removeLegacySessions(dataDir: string) {
36
+ await rm(path.join(dataDir, LEGACY_SESSIONS_FILE), { force: true })
37
+ }
38
+
39
+ function authOptions(database: Database, secret: string, sessionMaxAgeDays: number) {
40
+ return {
41
+ secret,
42
+ database,
43
+ baseURL: INTERNAL_BASE_URL,
44
+ basePath: AUTH_BASE_PATH,
45
+ emailAndPassword: {
46
+ enabled: true,
47
+ disableSignUp: true,
48
+ minPasswordLength: 1,
49
+ maxPasswordLength: MAX_PASSWORD_LENGTH,
50
+ },
51
+ session: {
52
+ expiresIn: Math.round(sessionMaxAgeDays * SECONDS_PER_DAY),
53
+ updateAge: SESSION_UPDATE_AGE_SECONDS,
54
+ },
55
+ advanced: {
56
+ cookies: { session_token: { name: SESSION_COOKIE_NAME } },
57
+ defaultCookieAttributes: { sameSite: "strict" },
58
+ useSecureCookies: false,
59
+ ipAddress: { ipAddressHeaders: [CLIENT_IP_HEADER] },
60
+ },
61
+ rateLimit: { enabled: true, storage: "database" },
62
+ trustedOrigins: (request) => (request ? [new URL(request.url).origin] : []),
63
+ logger: {
64
+ level: "warn",
65
+ log: (level, message, ...args) => log[level](`[auth] ${message}`, ...args),
66
+ },
67
+ } satisfies BetterAuthOptions
68
+ }
69
+
70
+ function buildAuth(database: Database, secret: string, sessionMaxAgeDays: number) {
71
+ return betterAuth(authOptions(database, secret, sessionMaxAgeDays))
72
+ }
73
+
74
+ type KannaAuthInstance = ReturnType<typeof buildAuth>
75
+
76
+ async function ensureOwner(instance: KannaAuthInstance, password: string) {
77
+ const context = await instance.$context
78
+ const found = await context.internalAdapter.findUserByEmail(OWNER_EMAIL, { includeAccounts: true })
79
+ if (!found) {
80
+ const hash = await context.password.hash(password)
81
+ const user = await context.internalAdapter.createUser(
82
+ { email: OWNER_EMAIL, name: "Owner", emailVerified: true },
83
+ { method: "email-password" },
84
+ )
85
+ await context.internalAdapter.linkAccount({
86
+ userId: user.id,
87
+ providerId: "credential",
88
+ accountId: user.id,
89
+ password: hash,
90
+ })
91
+ return
92
+ }
93
+ const credential = found.accounts.find((account) => account.providerId === "credential")
94
+ const unchanged = credential?.password
95
+ ? await context.password.verify({ hash: credential.password, password })
96
+ : false
97
+ if (unchanged) return
98
+ const hash = await context.password.hash(password)
99
+ if (credential) {
100
+ await context.internalAdapter.updatePassword(found.user.id, hash)
101
+ } else {
102
+ await context.internalAdapter.linkAccount({
103
+ userId: found.user.id,
104
+ providerId: "credential",
105
+ accountId: found.user.id,
106
+ password: hash,
107
+ })
108
+ }
109
+ await context.internalAdapter.deleteUserSessions(found.user.id)
110
+ }
111
+
112
+ export interface BetterAuthBackendOptions {
113
+ dataDir: string
114
+ password: string
115
+ getSessionMaxAgeDays: () => number
116
+ }
117
+
118
+ export interface BetterAuthBackend extends AuthBackend {
119
+ refreshSessionLifetime(): void
120
+ }
121
+
122
+ export async function createBetterAuthBackend(options: BetterAuthBackendOptions): Promise<BetterAuthBackend> {
123
+ await mkdir(options.dataDir, { recursive: true })
124
+ const secret = await loadOrCreateSecret(path.join(options.dataDir, SECRET_FILE))
125
+ const databasePath = path.join(options.dataDir, DATABASE_FILE)
126
+ const database = new Database(databasePath, { create: true })
127
+ await chmod(databasePath, 0o600)
128
+ let configuredDays = options.getSessionMaxAgeDays()
129
+ let current: KannaAuthInstance
130
+
131
+ try {
132
+ const { runMigrations } = await getMigrations(authOptions(database, secret, configuredDays))
133
+ await runMigrations()
134
+ current = buildAuth(database, secret, configuredDays)
135
+ await ensureOwner(current, options.password)
136
+ await removeLegacySessions(options.dataDir)
137
+ } catch (error) {
138
+ database.close()
139
+ throw toError(error)
140
+ }
141
+
142
+ return {
143
+ refreshSessionLifetime() {
144
+ const days = options.getSessionMaxAgeDays()
145
+ if (days === configuredDays) return
146
+ configuredDays = days
147
+ current = buildAuth(database, secret, days)
148
+ },
149
+ async signIn(request) {
150
+ return current.handler(request)
151
+ },
152
+ async getSession(headers) {
153
+ const result = await current.api.getSession({ headers, returnHeaders: true })
154
+ return {
155
+ authenticated: result.response !== null,
156
+ setCookies: result.headers.getSetCookie(),
157
+ }
158
+ },
159
+ async signOut(headers) {
160
+ try {
161
+ const result = await current.api.signOut({ headers, returnHeaders: true })
162
+ return result.headers.getSetCookie()
163
+ } catch (error) {
164
+ log.warn("[auth] sign-out failed", toError(error).message)
165
+ return []
166
+ }
167
+ },
168
+ async dispose() {
169
+ database.close()
170
+ },
171
+ }
172
+ }
173
+
174
+ export interface PasswordAuthOptions {
175
+ dataDir: string
176
+ password: string
177
+ trustProxy: boolean
178
+ appSettings: AppSettingsManager
179
+ }
180
+
181
+ export async function createPasswordAuth(options: PasswordAuthOptions): Promise<AuthManager> {
182
+ const { appSettings } = options
183
+ const backend = await createBetterAuthBackend({
184
+ dataDir: options.dataDir,
185
+ password: options.password,
186
+ getSessionMaxAgeDays: () => appSettings.getSnapshot().auth.sessionMaxAgeDays,
187
+ })
188
+ const unsubscribe = appSettings.onChange(() => backend.refreshSessionLifetime())
189
+ const manager = createAuthManager(backend, { trustProxy: options.trustProxy })
190
+ return {
191
+ ...manager,
192
+ async dispose() {
193
+ unsubscribe()
194
+ await manager.dispose()
195
+ },
196
+ }
197
+ }
@@ -111,7 +111,7 @@ export function createHttpDispatcher(
111
111
 
112
112
  if (url.pathname === "/auth/status") {
113
113
  return auth
114
- ? auth.handleStatus(req)
114
+ ? await auth.handleStatus(req)
115
115
  : Response.json({ enabled: false, authenticated: true })
116
116
  }
117
117
 
@@ -119,7 +119,7 @@ export function createHttpDispatcher(
119
119
  if (req.method !== "POST") {
120
120
  return new Response(null, { status: 405, headers: { Allow: "POST" } })
121
121
  }
122
- return auth ? auth.handleLogout(req) : Response.json({ ok: true })
122
+ return auth ? await auth.handleLogout(req) : Response.json({ ok: true })
123
123
  }
124
124
 
125
125
  if (url.pathname.startsWith("/api/share/")) {
@@ -129,14 +129,14 @@ export function createHttpDispatcher(
129
129
  if (auth) {
130
130
  if (url.pathname === "/auth/login") {
131
131
  if (req.method === "GET") return auth.redirectToApp(req)
132
- if (req.method === "POST") return auth.handleLogin(req, "/")
132
+ if (req.method === "POST") return auth.handleLogin(req, "/", server.requestIP(req)?.address ?? null)
133
133
  return new Response(null, { status: 405, headers: { Allow: "GET, POST" } })
134
134
  }
135
135
 
136
136
  if (url.pathname === "/ws") {
137
137
  if (!auth.validateOrigin(req)) return new Response("Forbidden", { status: 403 })
138
- if (!auth.isAuthenticated(req)) return new Response("Unauthorized", { status: 401 })
139
- } else if (url.pathname.startsWith("/api/") && !auth.isAuthenticated(req)) {
138
+ if (!(await auth.isAuthenticated(req))) return new Response("Unauthorized", { status: 401 })
139
+ } else if (url.pathname.startsWith("/api/") && !(await auth.isAuthenticated(req))) {
140
140
  return Response.json({ error: "Unauthorized" }, { status: 401 })
141
141
  }
142
142
  }
@@ -9,8 +9,8 @@ import {
9
9
  type AppSettingsSnapshot,
10
10
  } from "../shared/types"
11
11
  import type { ShareMode } from "../shared/share"
12
- import { createAuthManager } from "./auth"
13
- import { createAuthSessionStore } from "./auth-session-store.adapter"
12
+ import type { AuthManager } from "./auth"
13
+ import { createPasswordAuth } from "./better-auth.adapter"
14
14
  import { EventStore } from "./event-store"
15
15
  import { PushManager, realWebPushSender } from "./push/push-manager"
16
16
  import { loadOrGenerateVapidKeys } from "./push/vapid.adapter"
@@ -159,7 +159,7 @@ export interface StartKannaServerOptions {
159
159
  interface ApplicationServices {
160
160
  store: EventStore
161
161
  diffStore: DiffStore
162
- auth: ReturnType<typeof createAuthManager> | null
162
+ auth: AuthManager | null
163
163
  analytics: KannaAnalyticsReporter
164
164
  terminals: TerminalManager
165
165
  updateManager: UpdateManager | null
@@ -274,12 +274,11 @@ async function createApplicationServices(options: StartKannaServerOptions): Prom
274
274
  const snapshotSweepHandle = startSnapshotSweep(sessionShareService, 24 * 60 * 60 * 1000)
275
275
  await keybindings.initialize()
276
276
  const auth = options.password
277
- ? createAuthManager(options.password, {
277
+ ? await createPasswordAuth({
278
+ dataDir: store.dataDir,
279
+ password: options.password,
278
280
  trustProxy: options.trustProxy ?? false,
279
- sessionStore: await createAuthSessionStore({
280
- filePath: path.join(store.dataDir, "sessions.json"),
281
- }),
282
- getMaxAgeMs: () => appSettings.getSnapshot().auth.sessionMaxAgeDays * 86_400_000,
281
+ appSettings,
283
282
  })
284
283
  : null
285
284
  const analytics = new KannaAnalyticsReporter({
@@ -1 +0,0 @@
1
- import{f as e}from"./pendingActionsStore-CWNaCyjS.js";var t={name:`chevron-left`,size:24,node:[[`path`,{d:`m15 18-6-6 6-6`,key:`1wnfg3`}]]};t.node;var n=e(t);export{n as t};
@@ -1 +0,0 @@
1
- import{i as e,m as t,t as n}from"./log-Ch_SNBXH.js";function r(r,i){i.then(void 0,t(t=>n.error(e,`${r} failed`,t)))}export{r as t};