@vibe80/vibe80 0.1.1

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 (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. package/server/vitest.config.js +23 -0
@@ -0,0 +1,649 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import sqlite3 from "sqlite3";
4
+
5
+ const toJson = (value) => JSON.stringify(value);
6
+ const fromJson = (value) => {
7
+ if (!value) return null;
8
+ try {
9
+ return JSON.parse(value);
10
+ } catch {
11
+ return null;
12
+ }
13
+ };
14
+
15
+ const sanitizeSessionData = (data) => {
16
+ if (!data || typeof data !== "object") {
17
+ return data;
18
+ }
19
+ const payload = { ...data };
20
+ delete payload.providers;
21
+ return payload;
22
+ };
23
+
24
+ const openDatabase = (filename) =>
25
+ new Promise((resolve, reject) => {
26
+ const db = new sqlite3.Database(
27
+ filename,
28
+ sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
29
+ (err) => {
30
+ if (err) {
31
+ reject(err);
32
+ return;
33
+ }
34
+ resolve(db);
35
+ }
36
+ );
37
+ });
38
+
39
+ const run = (db, sql, params = []) =>
40
+ new Promise((resolve, reject) => {
41
+ db.run(sql, params, function onRun(err) {
42
+ if (err) {
43
+ reject(err);
44
+ return;
45
+ }
46
+ resolve(this);
47
+ });
48
+ });
49
+
50
+ const get = (db, sql, params = []) =>
51
+ new Promise((resolve, reject) => {
52
+ db.get(sql, params, (err, row) => {
53
+ if (err) {
54
+ reject(err);
55
+ return;
56
+ }
57
+ resolve(row || null);
58
+ });
59
+ });
60
+
61
+ const all = (db, sql, params = []) =>
62
+ new Promise((resolve, reject) => {
63
+ db.all(sql, params, (err, rows) => {
64
+ if (err) {
65
+ reject(err);
66
+ return;
67
+ }
68
+ resolve(rows || []);
69
+ });
70
+ });
71
+
72
+ export const createSqliteStorage = () => {
73
+ const dbPath = process.env.SQLITE_PATH || "/var/lib/vibe80/base.sqlite";
74
+ const resolvedPath = path.resolve(dbPath);
75
+ const dir = path.dirname(resolvedPath);
76
+ fs.mkdirSync(dir, { recursive: true, mode: 0o750 });
77
+
78
+ let db = null;
79
+
80
+ const ensureConnected = async () => {
81
+ if (db) return;
82
+ db = await openDatabase(resolvedPath);
83
+ await run(db, "PRAGMA journal_mode = WAL;");
84
+ await run(db, "PRAGMA busy_timeout = 5000;");
85
+ await run(db, "PRAGMA foreign_keys = ON;");
86
+ await run(
87
+ db,
88
+ `CREATE TABLE IF NOT EXISTS sessions (
89
+ sessionId TEXT PRIMARY KEY,
90
+ workspaceId TEXT,
91
+ createdAt INTEGER,
92
+ lastActivityAt INTEGER,
93
+ data TEXT NOT NULL
94
+ );`
95
+ );
96
+ await run(
97
+ db,
98
+ `CREATE INDEX IF NOT EXISTS sessions_workspace_idx
99
+ ON sessions (workspaceId);`
100
+ );
101
+ await run(
102
+ db,
103
+ `CREATE TABLE IF NOT EXISTS worktrees (
104
+ worktreeId TEXT PRIMARY KEY,
105
+ sessionId TEXT NOT NULL,
106
+ data TEXT NOT NULL,
107
+ FOREIGN KEY(sessionId) REFERENCES sessions(sessionId) ON DELETE CASCADE
108
+ );`
109
+ );
110
+ await run(
111
+ db,
112
+ `CREATE INDEX IF NOT EXISTS worktrees_session_idx
113
+ ON worktrees (sessionId);`
114
+ );
115
+ await run(
116
+ db,
117
+ `CREATE TABLE IF NOT EXISTS worktree_messages (
118
+ messageId TEXT NOT NULL,
119
+ sessionId TEXT NOT NULL,
120
+ worktreeId TEXT NOT NULL,
121
+ createdAt INTEGER NOT NULL,
122
+ data TEXT NOT NULL,
123
+ PRIMARY KEY (worktreeId, messageId),
124
+ FOREIGN KEY(sessionId) REFERENCES sessions(sessionId) ON DELETE CASCADE
125
+ );`
126
+ );
127
+ await run(
128
+ db,
129
+ `CREATE INDEX IF NOT EXISTS worktree_messages_session_idx
130
+ ON worktree_messages (sessionId, worktreeId, createdAt DESC);`
131
+ );
132
+ await run(
133
+ db,
134
+ `CREATE TABLE IF NOT EXISTS workspace_user_ids (
135
+ workspaceId TEXT PRIMARY KEY,
136
+ data TEXT NOT NULL
137
+ );`
138
+ );
139
+ await run(
140
+ db,
141
+ `CREATE TABLE IF NOT EXISTS workspace_refresh_tokens_v2 (
142
+ tokenHash TEXT PRIMARY KEY,
143
+ workspaceId TEXT NOT NULL,
144
+ expiresAt INTEGER NOT NULL,
145
+ consumedAt INTEGER,
146
+ replacedByHash TEXT
147
+ );`
148
+ );
149
+ await run(
150
+ db,
151
+ `CREATE TABLE IF NOT EXISTS workspaces (
152
+ workspaceId TEXT PRIMARY KEY,
153
+ data TEXT NOT NULL
154
+ );`
155
+ );
156
+ await run(
157
+ db,
158
+ `CREATE TABLE IF NOT EXISTS workspace_audit_events (
159
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
160
+ workspaceId TEXT NOT NULL,
161
+ createdAt INTEGER NOT NULL,
162
+ data TEXT NOT NULL
163
+ );`
164
+ );
165
+ await run(
166
+ db,
167
+ `CREATE INDEX IF NOT EXISTS workspace_audit_events_workspace_idx
168
+ ON workspace_audit_events (workspaceId, createdAt DESC);`
169
+ );
170
+ await run(
171
+ db,
172
+ `CREATE INDEX IF NOT EXISTS workspace_refresh_tokens_v2_workspace_idx
173
+ ON workspace_refresh_tokens_v2 (workspaceId);`
174
+ );
175
+ await run(
176
+ db,
177
+ `CREATE INDEX IF NOT EXISTS workspace_refresh_tokens_v2_expires_idx
178
+ ON workspace_refresh_tokens_v2 (expiresAt);`
179
+ );
180
+ await run(
181
+ db,
182
+ `CREATE TABLE IF NOT EXISTS workspace_uid_seq (
183
+ id INTEGER PRIMARY KEY CHECK (id = 1),
184
+ lastUid INTEGER NOT NULL
185
+ );`
186
+ );
187
+ const row = await get(db, "SELECT lastUid FROM workspace_uid_seq WHERE id = 1");
188
+ if (!row) {
189
+ const workspaceUidMin =
190
+ Number.parseInt(process.env.WORKSPACE_UID_MIN, 10) || 200000;
191
+ await run(
192
+ db,
193
+ "INSERT INTO workspace_uid_seq (id, lastUid) VALUES (1, ?)",
194
+ [workspaceUidMin - 1]
195
+ );
196
+ }
197
+ };
198
+
199
+ const saveSession = async (sessionId, data) => {
200
+ await ensureConnected();
201
+ const sessionData = sanitizeSessionData(data);
202
+ const createdAt =
203
+ typeof sessionData?.createdAt === "number" ? sessionData.createdAt : Date.now();
204
+ const lastActivityAt =
205
+ typeof sessionData?.lastActivityAt === "number"
206
+ ? sessionData.lastActivityAt
207
+ : Date.now();
208
+ await run(
209
+ db,
210
+ `INSERT INTO sessions (sessionId, workspaceId, createdAt, lastActivityAt, data)
211
+ VALUES (?, ?, ?, ?, ?)
212
+ ON CONFLICT(sessionId) DO UPDATE SET
213
+ workspaceId=excluded.workspaceId,
214
+ createdAt=excluded.createdAt,
215
+ lastActivityAt=excluded.lastActivityAt,
216
+ data=excluded.data;`,
217
+ [
218
+ sessionId,
219
+ sessionData?.workspaceId || null,
220
+ createdAt,
221
+ lastActivityAt,
222
+ toJson(sessionData),
223
+ ]
224
+ );
225
+ };
226
+
227
+ const getSession = async (sessionId) => {
228
+ await ensureConnected();
229
+ const row = await get(
230
+ db,
231
+ "SELECT data FROM sessions WHERE sessionId = ?",
232
+ [sessionId]
233
+ );
234
+ return fromJson(row?.data);
235
+ };
236
+
237
+ const deleteSession = async (sessionId) => {
238
+ await ensureConnected();
239
+ await run(db, "DELETE FROM worktree_messages WHERE sessionId = ?", [sessionId]);
240
+ await run(db, "DELETE FROM worktrees WHERE sessionId = ?", [sessionId]);
241
+ await run(db, "DELETE FROM sessions WHERE sessionId = ?", [sessionId]);
242
+ };
243
+
244
+ const listSessions = async (workspaceId) => {
245
+ await ensureConnected();
246
+ const rows = await all(
247
+ db,
248
+ workspaceId
249
+ ? "SELECT data FROM sessions WHERE workspaceId = ?"
250
+ : "SELECT data FROM sessions",
251
+ workspaceId ? [workspaceId] : []
252
+ );
253
+ return rows.map((row) => fromJson(row.data)).filter(Boolean);
254
+ };
255
+
256
+ const touchSession = async (sessionId) => {
257
+ await ensureConnected();
258
+ await run(
259
+ db,
260
+ "UPDATE sessions SET lastActivityAt = ? WHERE sessionId = ?",
261
+ [Date.now(), sessionId]
262
+ );
263
+ };
264
+
265
+ const saveWorktree = async (sessionId, worktreeId, data) => {
266
+ await ensureConnected();
267
+ await run(
268
+ db,
269
+ `INSERT INTO worktrees (worktreeId, sessionId, data)
270
+ VALUES (?, ?, ?)
271
+ ON CONFLICT(worktreeId) DO UPDATE SET
272
+ sessionId=excluded.sessionId,
273
+ data=excluded.data;`,
274
+ [worktreeId, sessionId, toJson(data)]
275
+ );
276
+ };
277
+
278
+ const getWorktree = async (worktreeId) => {
279
+ await ensureConnected();
280
+ const row = await get(
281
+ db,
282
+ "SELECT data FROM worktrees WHERE worktreeId = ?",
283
+ [worktreeId]
284
+ );
285
+ return fromJson(row?.data);
286
+ };
287
+
288
+ const deleteWorktree = async (sessionId, worktreeId) => {
289
+ await ensureConnected();
290
+ await run(
291
+ db,
292
+ "DELETE FROM worktree_messages WHERE worktreeId = ? AND sessionId = ?",
293
+ [worktreeId, sessionId]
294
+ );
295
+ await run(
296
+ db,
297
+ "DELETE FROM worktrees WHERE worktreeId = ? AND sessionId = ?",
298
+ [worktreeId, sessionId]
299
+ );
300
+ };
301
+
302
+ const listWorktrees = async (sessionId) => {
303
+ await ensureConnected();
304
+ const rows = await all(
305
+ db,
306
+ "SELECT data FROM worktrees WHERE sessionId = ?",
307
+ [sessionId]
308
+ );
309
+ return rows.map((row) => fromJson(row.data)).filter(Boolean);
310
+ };
311
+
312
+ const appendWorktreeMessage = async (sessionId, worktreeId, message) => {
313
+ await ensureConnected();
314
+ const messageId = message?.id;
315
+ if (!messageId) {
316
+ throw new Error("Message id is required.");
317
+ }
318
+ const createdAt =
319
+ typeof message?.createdAt === "number" ? message.createdAt : Date.now();
320
+ await run(
321
+ db,
322
+ `INSERT INTO worktree_messages (messageId, sessionId, worktreeId, createdAt, data)
323
+ VALUES (?, ?, ?, ?, ?)
324
+ ON CONFLICT(worktreeId, messageId) DO NOTHING;`,
325
+ [messageId, sessionId, worktreeId, createdAt, toJson(message)]
326
+ );
327
+ };
328
+
329
+ const getWorktreeMessages = async (
330
+ sessionId,
331
+ worktreeId,
332
+ { limit = null, beforeMessageId = null } = {}
333
+ ) => {
334
+ await ensureConnected();
335
+ let createdAfter = null;
336
+ if (beforeMessageId) {
337
+ const row = await get(
338
+ db,
339
+ `SELECT createdAt FROM worktree_messages
340
+ WHERE sessionId = ? AND worktreeId = ? AND messageId = ?`,
341
+ [sessionId, worktreeId, beforeMessageId]
342
+ );
343
+ if (!row) {
344
+ return [];
345
+ }
346
+ createdAfter = row.createdAt;
347
+ }
348
+
349
+ let rows;
350
+ if (createdAfter !== null) {
351
+ if (limit) {
352
+ rows = await all(
353
+ db,
354
+ `SELECT data FROM worktree_messages
355
+ WHERE sessionId = ? AND worktreeId = ? AND createdAt > ?
356
+ ORDER BY createdAt DESC
357
+ LIMIT ?`,
358
+ [sessionId, worktreeId, createdAfter, limit]
359
+ );
360
+ rows.reverse();
361
+ } else {
362
+ rows = await all(
363
+ db,
364
+ `SELECT data FROM worktree_messages
365
+ WHERE sessionId = ? AND worktreeId = ? AND createdAt > ?
366
+ ORDER BY createdAt ASC`,
367
+ [sessionId, worktreeId, createdAfter]
368
+ );
369
+ }
370
+ } else if (limit) {
371
+ rows = await all(
372
+ db,
373
+ `SELECT data FROM worktree_messages
374
+ WHERE sessionId = ? AND worktreeId = ?
375
+ ORDER BY createdAt DESC
376
+ LIMIT ?`,
377
+ [sessionId, worktreeId, limit]
378
+ );
379
+ rows.reverse();
380
+ } else {
381
+ rows = await all(
382
+ db,
383
+ `SELECT data FROM worktree_messages
384
+ WHERE sessionId = ? AND worktreeId = ?
385
+ ORDER BY createdAt ASC`,
386
+ [sessionId, worktreeId]
387
+ );
388
+ }
389
+
390
+ return rows.map((row) => fromJson(row.data)).filter(Boolean);
391
+ };
392
+
393
+ const clearWorktreeMessages = async (sessionId, worktreeId) => {
394
+ await ensureConnected();
395
+ await run(
396
+ db,
397
+ "DELETE FROM worktree_messages WHERE sessionId = ? AND worktreeId = ?",
398
+ [sessionId, worktreeId]
399
+ );
400
+ };
401
+
402
+ const saveWorkspaceUserIds = async (workspaceId, data) => {
403
+ await ensureConnected();
404
+ await run(
405
+ db,
406
+ `INSERT INTO workspace_user_ids (workspaceId, data)
407
+ VALUES (?, ?)
408
+ ON CONFLICT(workspaceId) DO UPDATE SET data=excluded.data;`,
409
+ [workspaceId, toJson(data)]
410
+ );
411
+ };
412
+
413
+ const getWorkspaceUserIds = async (workspaceId) => {
414
+ await ensureConnected();
415
+ const row = await get(
416
+ db,
417
+ "SELECT data FROM workspace_user_ids WHERE workspaceId = ?",
418
+ [workspaceId]
419
+ );
420
+ return fromJson(row?.data);
421
+ };
422
+
423
+ const saveWorkspaceRefreshToken = async (
424
+ workspaceId,
425
+ tokenHash,
426
+ expiresAt,
427
+ _ttlMs = null,
428
+ _options = {}
429
+ ) => {
430
+ await ensureConnected();
431
+ await run(
432
+ db,
433
+ `INSERT INTO workspace_refresh_tokens_v2 (
434
+ tokenHash,
435
+ workspaceId,
436
+ expiresAt,
437
+ consumedAt,
438
+ replacedByHash
439
+ )
440
+ VALUES (?, ?, ?, NULL, NULL);`,
441
+ [tokenHash, workspaceId, expiresAt]
442
+ );
443
+ };
444
+
445
+ const getWorkspaceRefreshToken = async (tokenHash) => {
446
+ await ensureConnected();
447
+ const row = await get(
448
+ db,
449
+ `SELECT
450
+ tokenHash,
451
+ workspaceId,
452
+ expiresAt,
453
+ consumedAt,
454
+ replacedByHash
455
+ FROM workspace_refresh_tokens_v2
456
+ WHERE tokenHash = ?`,
457
+ [tokenHash]
458
+ );
459
+ if (!row) return null;
460
+ return {
461
+ tokenHash: row.tokenHash,
462
+ workspaceId: row.workspaceId,
463
+ expiresAt: row.expiresAt,
464
+ consumedAt: row.consumedAt || null,
465
+ replacedByHash: row.replacedByHash || null,
466
+ };
467
+ };
468
+
469
+ const rotateWorkspaceRefreshToken = async (
470
+ tokenHash,
471
+ nextTokenHash,
472
+ nextExpiresAt,
473
+ _nextTtlMs = null
474
+ ) => {
475
+ await ensureConnected();
476
+ const now = Date.now();
477
+ await run(db, "BEGIN IMMEDIATE");
478
+ try {
479
+ const row = await get(
480
+ db,
481
+ `SELECT
482
+ tokenHash,
483
+ workspaceId,
484
+ expiresAt,
485
+ consumedAt
486
+ FROM workspace_refresh_tokens_v2
487
+ WHERE tokenHash = ?`,
488
+ [tokenHash]
489
+ );
490
+ if (!row) {
491
+ await run(db, "ROLLBACK");
492
+ return { ok: false, code: "invalid_refresh_token" };
493
+ }
494
+ if (row.consumedAt) {
495
+ await run(db, "ROLLBACK");
496
+ return { ok: false, code: "refresh_token_reused" };
497
+ }
498
+ if (row.expiresAt && row.expiresAt <= now) {
499
+ await run(
500
+ db,
501
+ "DELETE FROM workspace_refresh_tokens_v2 WHERE tokenHash = ?",
502
+ [tokenHash]
503
+ );
504
+ await run(db, "COMMIT");
505
+ return { ok: false, code: "refresh_token_expired" };
506
+ }
507
+ await run(
508
+ db,
509
+ `UPDATE workspace_refresh_tokens_v2
510
+ SET consumedAt = ?, replacedByHash = ?
511
+ WHERE tokenHash = ? AND consumedAt IS NULL`,
512
+ [now, nextTokenHash, tokenHash]
513
+ );
514
+ await run(
515
+ db,
516
+ `INSERT INTO workspace_refresh_tokens_v2 (
517
+ tokenHash,
518
+ workspaceId,
519
+ expiresAt,
520
+ consumedAt,
521
+ replacedByHash
522
+ )
523
+ VALUES (?, ?, ?, NULL, NULL)`,
524
+ [nextTokenHash, row.workspaceId, nextExpiresAt]
525
+ );
526
+ await run(db, "COMMIT");
527
+ return { ok: true, workspaceId: row.workspaceId };
528
+ } catch (error) {
529
+ await run(db, "ROLLBACK");
530
+ throw error;
531
+ }
532
+ };
533
+
534
+ const deleteWorkspaceRefreshToken = async (tokenHash) => {
535
+ await ensureConnected();
536
+ await run(
537
+ db,
538
+ "DELETE FROM workspace_refresh_tokens_v2 WHERE tokenHash = ?",
539
+ [tokenHash]
540
+ );
541
+ };
542
+
543
+ const cleanupWorkspaceRefreshTokens = async () => {
544
+ await ensureConnected();
545
+ await run(
546
+ db,
547
+ "DELETE FROM workspace_refresh_tokens_v2 WHERE expiresAt <= ?",
548
+ [Date.now()]
549
+ );
550
+ };
551
+
552
+ const getNextWorkspaceUid = async () => {
553
+ await ensureConnected();
554
+ const workspaceUidMin =
555
+ Number.parseInt(process.env.WORKSPACE_UID_MIN, 10) || 200000;
556
+ const workspaceUidMax =
557
+ Number.parseInt(process.env.WORKSPACE_UID_MAX, 10) || 999999999;
558
+ await run(db, "BEGIN IMMEDIATE");
559
+ try {
560
+ const row = await get(db, "SELECT lastUid FROM workspace_uid_seq WHERE id = 1");
561
+ const lastUid = Number(row?.lastUid ?? workspaceUidMin - 1);
562
+ const nextUid = lastUid + 1;
563
+ if (nextUid > workspaceUidMax) {
564
+ throw new Error("Workspace UID range exhausted.");
565
+ }
566
+ await run(db, "UPDATE workspace_uid_seq SET lastUid = ? WHERE id = 1", [
567
+ nextUid,
568
+ ]);
569
+ await run(db, "COMMIT");
570
+ return nextUid;
571
+ } catch (error) {
572
+ await run(db, "ROLLBACK");
573
+ throw error;
574
+ }
575
+ };
576
+
577
+ const saveWorkspace = async (workspaceId, data) => {
578
+ await ensureConnected();
579
+ await run(
580
+ db,
581
+ `INSERT INTO workspaces (workspaceId, data)
582
+ VALUES (?, ?)
583
+ ON CONFLICT(workspaceId) DO UPDATE SET data=excluded.data;`,
584
+ [workspaceId, toJson(data)]
585
+ );
586
+ };
587
+
588
+ const getWorkspace = async (workspaceId) => {
589
+ await ensureConnected();
590
+ const row = await get(
591
+ db,
592
+ "SELECT data FROM workspaces WHERE workspaceId = ?",
593
+ [workspaceId]
594
+ );
595
+ return fromJson(row?.data);
596
+ };
597
+
598
+ const appendWorkspaceAuditEvent = async (workspaceId, data) => {
599
+ await ensureConnected();
600
+ const createdAt =
601
+ typeof data?.ts === "number" && Number.isFinite(data.ts) ? data.ts : Date.now();
602
+ await run(
603
+ db,
604
+ `INSERT INTO workspace_audit_events (workspaceId, createdAt, data)
605
+ VALUES (?, ?, ?)`,
606
+ [workspaceId, createdAt, toJson(data)]
607
+ );
608
+ };
609
+
610
+ return {
611
+ init: ensureConnected,
612
+ close: async () => {
613
+ if (!db) return;
614
+ await new Promise((resolve, reject) => {
615
+ db.close((err) => {
616
+ if (err) {
617
+ reject(err);
618
+ return;
619
+ }
620
+ resolve();
621
+ });
622
+ });
623
+ db = null;
624
+ },
625
+ saveSession,
626
+ getSession,
627
+ deleteSession,
628
+ listSessions,
629
+ touchSession,
630
+ saveWorktree,
631
+ getWorktree,
632
+ deleteWorktree,
633
+ listWorktrees,
634
+ appendWorktreeMessage,
635
+ getWorktreeMessages,
636
+ clearWorktreeMessages,
637
+ saveWorkspaceUserIds,
638
+ getWorkspaceUserIds,
639
+ saveWorkspaceRefreshToken,
640
+ getWorkspaceRefreshToken,
641
+ rotateWorkspaceRefreshToken,
642
+ deleteWorkspaceRefreshToken,
643
+ cleanupWorkspaceRefreshTokens,
644
+ getNextWorkspaceUid,
645
+ saveWorkspace,
646
+ getWorkspace,
647
+ appendWorkspaceAuditEvent,
648
+ };
649
+ };