@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,224 +0,0 @@
1
- import { createHash } from "node:crypto"
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises"
3
- import path from "node:path"
4
- import { LOG_PREFIX } from "../shared/branding"
5
- import { log } from "../shared/log"
6
- import { isErrnoException } from "../shared/errors"
7
- import { isJsonArray, isJsonObject, safeJsonParse, type JsonValue } from "../shared/json"
8
-
9
- const FILE_VERSION = 1
10
- const PERSIST_DEBOUNCE_MS = 250
11
- const PERSIST_DRIFT_MS = 60 * 60 * 1000
12
- const SWEEP_INTERVAL_MS = 60 * 60 * 1000
13
-
14
- export interface PersistedSession {
15
- tokenHash: string
16
- createdAt: number
17
- lastSeenAt: number
18
- expiresAt: number
19
- }
20
-
21
- interface SessionsFile {
22
- version: number
23
- sessions: PersistedSession[]
24
- }
25
-
26
- export interface AuthSessionStore {
27
- create(token: string, maxAgeMs: number): PersistedSession
28
- validate(token: string): PersistedSession | null
29
- touch(token: string, maxAgeMs: number): PersistedSession | null
30
- revoke(token: string): void
31
- sweep(): void
32
- dispose(): Promise<void>
33
- }
34
-
35
- export interface CreateAuthSessionStoreOptions {
36
- filePath: string
37
- now?: () => number
38
- sweepIntervalMs?: number
39
- }
40
-
41
- function hashToken(token: string) {
42
- return createHash("sha256").update(token).digest("hex")
43
- }
44
-
45
- function isPersistedSession(value: JsonValue): value is JsonValue & PersistedSession {
46
- if (!isJsonObject(value)) return false
47
- return typeof value.tokenHash === "string"
48
- && typeof value.createdAt === "number"
49
- && typeof value.lastSeenAt === "number"
50
- && typeof value.expiresAt === "number"
51
- }
52
-
53
- async function loadSessionsFile(filePath: string): Promise<PersistedSession[]> {
54
- let text: string
55
- try {
56
- text = await readFile(filePath, "utf8")
57
- } catch (error) {
58
- if (isErrnoException(error) && error.code === "ENOENT") return []
59
- throw error
60
- }
61
- if (!text.trim()) return []
62
- const parsed = safeJsonParse(text)
63
- if (parsed === null) {
64
- log.warn(`${LOG_PREFIX} sessions.json is invalid JSON; ignoring.`)
65
- return []
66
- }
67
- if (!isJsonObject(parsed) || !isJsonArray(parsed.sessions)) return []
68
- return parsed.sessions.filter(isPersistedSession)
69
- }
70
-
71
- export async function createAuthSessionStore(options: CreateAuthSessionStoreOptions): Promise<AuthSessionStore> {
72
- const { filePath } = options
73
- const now = options.now ?? (() => Date.now())
74
- const sweepIntervalMs = options.sweepIntervalMs ?? SWEEP_INTERVAL_MS
75
-
76
- await mkdir(path.dirname(filePath), { recursive: true })
77
- const initialSessions = await loadSessionsFile(filePath)
78
- const sessions = new Map<string, PersistedSession>()
79
- const lastPersistedExpiry = new Map<string, number>()
80
- for (const session of initialSessions) {
81
- if (session.expiresAt > now()) {
82
- sessions.set(session.tokenHash, session)
83
- lastPersistedExpiry.set(session.tokenHash, session.expiresAt)
84
- }
85
- }
86
-
87
- let pendingPersist: ReturnType<typeof setTimeout> | null = null
88
- let activeWrite: Promise<void> | null = null
89
- let writeAgain = false
90
-
91
- async function writeFileAtomic() {
92
- const payload: SessionsFile = {
93
- version: FILE_VERSION,
94
- sessions: Array.from(sessions.values()),
95
- }
96
- const tmpPath = `${filePath}.tmp`
97
- await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8")
98
- await rename(tmpPath, filePath)
99
- for (const session of payload.sessions) {
100
- lastPersistedExpiry.set(session.tokenHash, session.expiresAt)
101
- }
102
- for (const tokenHash of [...lastPersistedExpiry.keys()]) {
103
- if (!sessions.has(tokenHash)) lastPersistedExpiry.delete(tokenHash)
104
- }
105
- }
106
-
107
- async function runWrite() {
108
- do {
109
- writeAgain = false
110
- try {
111
- await writeFileAtomic()
112
- } catch (error) {
113
- log.warn(`${LOG_PREFIX} Failed to persist sessions.json:`, String(error))
114
- }
115
- } while (writeAgain)
116
- activeWrite = null
117
- }
118
-
119
- function schedulePersist() {
120
- if (pendingPersist) return
121
- pendingPersist = setTimeout(() => {
122
- pendingPersist = null
123
- if (activeWrite) {
124
- writeAgain = true
125
- return
126
- }
127
- activeWrite = runWrite()
128
- }, PERSIST_DEBOUNCE_MS)
129
- }
130
-
131
- async function flushPersist() {
132
- if (pendingPersist) {
133
- clearTimeout(pendingPersist)
134
- pendingPersist = null
135
- activeWrite = activeWrite ?? runWrite()
136
- }
137
- if (activeWrite) await activeWrite
138
- }
139
-
140
- function shouldPersistTouch(tokenHash: string, expiresAt: number) {
141
- const last = lastPersistedExpiry.get(tokenHash)
142
- if (last === undefined) return true
143
- return Math.abs(expiresAt - last) >= PERSIST_DRIFT_MS
144
- }
145
-
146
- function create(token: string, maxAgeMs: number): PersistedSession {
147
- const tokenHash = hashToken(token)
148
- const timestamp = now()
149
- const session: PersistedSession = {
150
- tokenHash,
151
- createdAt: timestamp,
152
- lastSeenAt: timestamp,
153
- expiresAt: timestamp + maxAgeMs,
154
- }
155
- sessions.set(tokenHash, session)
156
- schedulePersist()
157
- return session
158
- }
159
-
160
- function validate(token: string): PersistedSession | null {
161
- const tokenHash = hashToken(token)
162
- const session = sessions.get(tokenHash)
163
- if (!session) return null
164
- if (session.expiresAt <= now()) {
165
- sessions.delete(tokenHash)
166
- schedulePersist()
167
- return null
168
- }
169
- return session
170
- }
171
-
172
- function touch(token: string, maxAgeMs: number): PersistedSession | null {
173
- const tokenHash = hashToken(token)
174
- const session = sessions.get(tokenHash)
175
- if (!session) return null
176
- const timestamp = now()
177
- if (session.expiresAt <= timestamp) {
178
- sessions.delete(tokenHash)
179
- schedulePersist()
180
- return null
181
- }
182
- const next: PersistedSession = {
183
- ...session,
184
- lastSeenAt: timestamp,
185
- expiresAt: timestamp + maxAgeMs,
186
- }
187
- sessions.set(tokenHash, next)
188
- if (shouldPersistTouch(tokenHash, next.expiresAt)) {
189
- schedulePersist()
190
- }
191
- return next
192
- }
193
-
194
- function revoke(token: string) {
195
- const tokenHash = hashToken(token)
196
- if (sessions.delete(tokenHash)) {
197
- schedulePersist()
198
- }
199
- }
200
-
201
- function sweep() {
202
- const cutoff = now()
203
- let changed = false
204
- for (const [tokenHash, session] of sessions) {
205
- if (session.expiresAt <= cutoff) {
206
- sessions.delete(tokenHash)
207
- changed = true
208
- }
209
- }
210
- if (changed) schedulePersist()
211
- }
212
-
213
- const sweepHandle = setInterval(sweep, sweepIntervalMs)
214
- if (typeof sweepHandle === "object" && sweepHandle !== null && "unref" in sweepHandle) {
215
- sweepHandle.unref?.()
216
- }
217
-
218
- async function dispose() {
219
- clearInterval(sweepHandle)
220
- await flushPersist()
221
- }
222
-
223
- return { create, validate, touch, revoke, sweep, dispose }
224
- }
@@ -1,94 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test"
2
- import { createHash } from "node:crypto"
3
- import { mkdtemp, readFile, rm } from "node:fs/promises"
4
- import { tmpdir } from "node:os"
5
- import path from "node:path"
6
- import { createAuthSessionStore } from "./auth-session-store.adapter"
7
-
8
- const tempDirs: string[] = []
9
-
10
- afterEach(async () => {
11
- await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
12
- })
13
-
14
- async function makeFilePath() {
15
- const dir = await mkdtemp(path.join(tmpdir(), "kanna-sessions-"))
16
- tempDirs.push(dir)
17
- return path.join(dir, "sessions.json")
18
- }
19
-
20
- const DAY_MS = 86_400_000
21
-
22
- describe("auth-session-store", () => {
23
- test("persists only the sha256 hash of the token", async () => {
24
- const filePath = await makeFilePath()
25
- const store = await createAuthSessionStore({ filePath })
26
- store.create("super-secret-token", DAY_MS)
27
- await store.dispose()
28
-
29
- const text = await readFile(filePath, "utf8")
30
- expect(text).not.toContain("super-secret-token")
31
- const expectedHash = createHash("sha256").update("super-secret-token").digest("hex")
32
- expect(text).toContain(expectedHash)
33
- })
34
-
35
- test("hydrates persisted entries on construction", async () => {
36
- const filePath = await makeFilePath()
37
- const first = await createAuthSessionStore({ filePath })
38
- first.create("token-a", DAY_MS)
39
- await first.dispose()
40
-
41
- const second = await createAuthSessionStore({ filePath })
42
- expect(second.validate("token-a")).not.toBeNull()
43
- expect(second.validate("missing")).toBeNull()
44
- await second.dispose()
45
- })
46
-
47
- test("expired entries are dropped on validate and sweep", async () => {
48
- const filePath = await makeFilePath()
49
- let now = 1_000
50
- const store = await createAuthSessionStore({ filePath, now: () => now })
51
- store.create("token-x", 100)
52
- expect(store.validate("token-x")).not.toBeNull()
53
-
54
- now = 5_000
55
- expect(store.validate("token-x")).toBeNull()
56
-
57
- store.create("token-y", 100)
58
- now = 100_000
59
- store.sweep()
60
- expect(store.validate("token-y")).toBeNull()
61
- await store.dispose()
62
- })
63
-
64
- test("touch shifts expiresAt forward and survives reload", async () => {
65
- const filePath = await makeFilePath()
66
- let now = 1_000
67
- const first = await createAuthSessionStore({ filePath, now: () => now })
68
- first.create("token-z", DAY_MS)
69
-
70
- now = 1_000 + 12 * 60 * 60 * 1000
71
- const touched = first.touch("token-z", DAY_MS)
72
- expect(touched).not.toBeNull()
73
- expect(touched!.expiresAt).toBe(now + DAY_MS)
74
- await first.dispose()
75
-
76
- const second = await createAuthSessionStore({ filePath, now: () => now })
77
- const reloaded = second.validate("token-z")
78
- expect(reloaded).not.toBeNull()
79
- expect(reloaded!.expiresAt).toBe(now + DAY_MS)
80
- await second.dispose()
81
- })
82
-
83
- test("revoke removes entries from disk", async () => {
84
- const filePath = await makeFilePath()
85
- const store = await createAuthSessionStore({ filePath })
86
- store.create("token-r", DAY_MS)
87
- store.revoke("token-r")
88
- await store.dispose()
89
-
90
- const reloaded = await createAuthSessionStore({ filePath })
91
- expect(reloaded.validate("token-r")).toBeNull()
92
- await reloaded.dispose()
93
- })
94
- })