@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.
- package/LICENSE +201 -0
- package/README.md +52 -0
- package/bin/vibe80.js +176 -0
- package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
- package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
- package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
- package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
- package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
- package/client/dist/assets/browser-e3WgtMs-.js +8 -0
- package/client/dist/assets/index-CgqGyssr.css +32 -0
- package/client/dist/assets/index-DnwKjoj7.js +706 -0
- package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
- package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +35 -0
- package/client/dist/index.html +14 -0
- package/client/index.html +16 -0
- package/client/package.json +34 -0
- package/client/public/favicon.ico +0 -0
- package/client/public/favicon.png +0 -0
- package/client/public/favicon.svg +35 -0
- package/client/public/pwa-192x192.png +0 -0
- package/client/public/pwa-512x512.png +0 -0
- package/client/src/App.jsx +3131 -0
- package/client/src/assets/logo_small.png +0 -0
- package/client/src/assets/vibe80_dark.svg +51 -0
- package/client/src/assets/vibe80_light.svg +50 -0
- package/client/src/components/Chat/ChatComposer.jsx +228 -0
- package/client/src/components/Chat/ChatMessages.jsx +811 -0
- package/client/src/components/Chat/ChatToolbar.jsx +109 -0
- package/client/src/components/Chat/useChatComposer.js +462 -0
- package/client/src/components/Diff/DiffPanel.jsx +129 -0
- package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
- package/client/src/components/Logs/LogsPanel.jsx +80 -0
- package/client/src/components/SessionGate/SessionGate.jsx +874 -0
- package/client/src/components/Settings/SettingsPanel.jsx +212 -0
- package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
- package/client/src/components/Topbar/Topbar.jsx +101 -0
- package/client/src/components/WorktreeTabs.css +419 -0
- package/client/src/components/WorktreeTabs.jsx +604 -0
- package/client/src/hooks/useAttachments.jsx +125 -0
- package/client/src/hooks/useBacklog.js +254 -0
- package/client/src/hooks/useChatClear.js +90 -0
- package/client/src/hooks/useChatCollapse.js +42 -0
- package/client/src/hooks/useChatCommands.js +294 -0
- package/client/src/hooks/useChatExport.js +144 -0
- package/client/src/hooks/useChatMessagesState.js +69 -0
- package/client/src/hooks/useChatSend.js +158 -0
- package/client/src/hooks/useChatSocket.js +1239 -0
- package/client/src/hooks/useDiffNavigation.js +19 -0
- package/client/src/hooks/useExplorerActions.js +1184 -0
- package/client/src/hooks/useGitIdentity.js +114 -0
- package/client/src/hooks/useLayoutMode.js +31 -0
- package/client/src/hooks/useLocalPreferences.js +131 -0
- package/client/src/hooks/useMessageSync.js +30 -0
- package/client/src/hooks/useNotifications.js +132 -0
- package/client/src/hooks/usePaneNavigation.js +67 -0
- package/client/src/hooks/usePanelState.js +13 -0
- package/client/src/hooks/useProviderSelection.js +70 -0
- package/client/src/hooks/useRepoBranchesModels.js +218 -0
- package/client/src/hooks/useRepoStatus.js +350 -0
- package/client/src/hooks/useRpcLogActions.js +19 -0
- package/client/src/hooks/useRpcLogView.js +58 -0
- package/client/src/hooks/useSessionHandoff.js +97 -0
- package/client/src/hooks/useSessionLifecycle.js +287 -0
- package/client/src/hooks/useSessionReset.js +63 -0
- package/client/src/hooks/useSessionResync.js +77 -0
- package/client/src/hooks/useTerminalSession.js +328 -0
- package/client/src/hooks/useToolbarExport.js +27 -0
- package/client/src/hooks/useTurnInterrupt.js +43 -0
- package/client/src/hooks/useVibe80Forms.js +128 -0
- package/client/src/hooks/useWorkspaceAuth.js +932 -0
- package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
- package/client/src/hooks/useWorktrees.js +396 -0
- package/client/src/i18n.jsx +87 -0
- package/client/src/index.css +5147 -0
- package/client/src/locales/en.json +37 -0
- package/client/src/locales/fr.json +321 -0
- package/client/src/main.jsx +16 -0
- package/client/vite.config.js +62 -0
- package/docs/api/asyncapi.json +1511 -0
- package/docs/api/openapi.json +3242 -0
- package/git_hooks/prepare-commit-msg +35 -0
- package/package.json +36 -0
- package/server/package.json +29 -0
- package/server/scripts/rotate-workspace-secret.js +101 -0
- package/server/src/claudeClient.js +454 -0
- package/server/src/clientEvents.js +594 -0
- package/server/src/clientFactory.js +164 -0
- package/server/src/codexClient.js +468 -0
- package/server/src/config.js +27 -0
- package/server/src/helpers.js +138 -0
- package/server/src/index.js +1641 -0
- package/server/src/middleware/auth.js +93 -0
- package/server/src/middleware/debug.js +89 -0
- package/server/src/middleware/errorTypes.js +60 -0
- package/server/src/providerLogger.js +60 -0
- package/server/src/routes/files.js +114 -0
- package/server/src/routes/git.js +183 -0
- package/server/src/routes/health.js +13 -0
- package/server/src/routes/sessions.js +407 -0
- package/server/src/routes/workspaces.js +296 -0
- package/server/src/routes/worktrees.js +993 -0
- package/server/src/runAs.js +458 -0
- package/server/src/runtimeStore.js +32 -0
- package/server/src/services/auth.js +157 -0
- package/server/src/services/claudeThreadDirectory.js +33 -0
- package/server/src/services/session.js +918 -0
- package/server/src/services/workspace.js +858 -0
- package/server/src/storage/index.js +17 -0
- package/server/src/storage/redis.js +412 -0
- package/server/src/storage/sqlite.js +649 -0
- package/server/src/worktreeManager.js +717 -0
- package/server/tests/README.md +13 -0
- package/server/tests/factories/workspaceFactory.js +13 -0
- package/server/tests/fixtures/workspaceCredentials.json +4 -0
- package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
- package/server/tests/setup/env.js +9 -0
- package/server/tests/unit/helpers.test.js +95 -0
- package/server/tests/unit/services/auth.test.js +181 -0
- package/server/tests/unit/services/workspace.test.js +115 -0
- 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
|
+
};
|