@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,17 @@
|
|
|
1
|
+
import { createRedisStorage } from "./redis.js";
|
|
2
|
+
import { createSqliteStorage } from "./sqlite.js";
|
|
3
|
+
|
|
4
|
+
const backend = process.env.STORAGE_BACKEND || "sqlite";
|
|
5
|
+
|
|
6
|
+
let storage = null;
|
|
7
|
+
|
|
8
|
+
if (backend === "redis") {
|
|
9
|
+
storage = createRedisStorage();
|
|
10
|
+
} else if (backend === "sqlite") {
|
|
11
|
+
storage = createSqliteStorage();
|
|
12
|
+
} else {
|
|
13
|
+
throw new Error(`Unsupported STORAGE_BACKEND: ${backend}.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const storageBackend = backend;
|
|
17
|
+
export default storage;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createClient } from "redis";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PREFIX = "v80";
|
|
4
|
+
|
|
5
|
+
const buildKey = (prefix, ...parts) => [prefix, ...parts].join(":");
|
|
6
|
+
|
|
7
|
+
const toJson = (value) => JSON.stringify(value);
|
|
8
|
+
const fromJson = (value) => {
|
|
9
|
+
if (!value) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(value);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const sanitizeSessionData = (data) => {
|
|
18
|
+
if (!data || typeof data !== "object") {
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
const payload = { ...data };
|
|
22
|
+
delete payload.providers;
|
|
23
|
+
return payload;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const createRedisStorage = () => {
|
|
27
|
+
const url = process.env.REDIS_URL;
|
|
28
|
+
if (!url) {
|
|
29
|
+
throw new Error("REDIS_URL is required when STORAGE_BACKEND=redis.");
|
|
30
|
+
}
|
|
31
|
+
const prefix = process.env.REDIS_KEY_PREFIX || DEFAULT_PREFIX;
|
|
32
|
+
const client = createClient({ url });
|
|
33
|
+
|
|
34
|
+
const sessionKey = (sessionId) => buildKey(prefix, "session", sessionId);
|
|
35
|
+
const sessionWorktreesKey = (sessionId) =>
|
|
36
|
+
buildKey(prefix, "session", sessionId, "worktrees");
|
|
37
|
+
const workspaceSessionsKey = (workspaceId) =>
|
|
38
|
+
buildKey(prefix, "workspace", workspaceId, "sessions");
|
|
39
|
+
const worktreeKey = (worktreeId) => buildKey(prefix, "worktree", worktreeId);
|
|
40
|
+
const worktreeMessagesKey = (worktreeId) =>
|
|
41
|
+
buildKey(prefix, "worktree", worktreeId, "messages");
|
|
42
|
+
const worktreeMessageIndexKey = (worktreeId) =>
|
|
43
|
+
buildKey(prefix, "worktree", worktreeId, "messageIndex");
|
|
44
|
+
const worktreeMessageSeqKey = (worktreeId) =>
|
|
45
|
+
buildKey(prefix, "worktree", worktreeId, "messageSeq");
|
|
46
|
+
const workspaceUserIdsKey = (workspaceId) =>
|
|
47
|
+
buildKey(prefix, "workspaceUserIds", workspaceId);
|
|
48
|
+
const workspaceKey = (workspaceId) => buildKey(prefix, "workspace", workspaceId);
|
|
49
|
+
const workspaceAuditEventsKey = (workspaceId) =>
|
|
50
|
+
buildKey(prefix, "workspace", workspaceId, "auditEvents");
|
|
51
|
+
const workspaceUidSeqKey = () => buildKey(prefix, "workspaceUidSeq");
|
|
52
|
+
const refreshTokenKey = (tokenHash) => buildKey(prefix, "refreshToken", tokenHash);
|
|
53
|
+
const globalSessionsKey = () => buildKey(prefix, "sessions");
|
|
54
|
+
|
|
55
|
+
const sessionTtlMs = Number.parseInt(process.env.SESSION_MAX_TTL_MS, 10) || 0;
|
|
56
|
+
const workspaceUidMin =
|
|
57
|
+
Number.parseInt(process.env.WORKSPACE_UID_MIN, 10) || 200000;
|
|
58
|
+
const workspaceUidMax =
|
|
59
|
+
Number.parseInt(process.env.WORKSPACE_UID_MAX, 10) || 999999999;
|
|
60
|
+
|
|
61
|
+
const ensureConnected = async () => {
|
|
62
|
+
if (client.isOpen) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await client.connect();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const setWithTtl = async (key, value, ttlMs) => {
|
|
69
|
+
if (ttlMs && ttlMs > 0) {
|
|
70
|
+
await client.set(key, value, { PX: ttlMs });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await client.set(key, value);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const touchTtl = async (key, ttlMs) => {
|
|
77
|
+
if (ttlMs && ttlMs > 0) {
|
|
78
|
+
await client.pExpire(key, ttlMs);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const saveSession = async (sessionId, data) => {
|
|
83
|
+
await ensureConnected();
|
|
84
|
+
const sessionData = sanitizeSessionData(data);
|
|
85
|
+
const key = sessionKey(sessionId);
|
|
86
|
+
await setWithTtl(key, toJson(sessionData), sessionTtlMs);
|
|
87
|
+
await client.sAdd(globalSessionsKey(), sessionId);
|
|
88
|
+
if (sessionData?.workspaceId) {
|
|
89
|
+
await client.sAdd(workspaceSessionsKey(sessionData.workspaceId), sessionId);
|
|
90
|
+
}
|
|
91
|
+
if (sessionData?.workspaceId) {
|
|
92
|
+
await touchTtl(workspaceSessionsKey(sessionData.workspaceId), sessionTtlMs);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getSession = async (sessionId) => {
|
|
97
|
+
await ensureConnected();
|
|
98
|
+
const raw = await client.get(sessionKey(sessionId));
|
|
99
|
+
return fromJson(raw);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const deleteSession = async (sessionId, workspaceId = null) => {
|
|
103
|
+
await ensureConnected();
|
|
104
|
+
const worktreeIds = await client.sMembers(sessionWorktreesKey(sessionId));
|
|
105
|
+
if (worktreeIds.length) {
|
|
106
|
+
const keys = worktreeIds.flatMap((id) => [
|
|
107
|
+
worktreeKey(id),
|
|
108
|
+
worktreeMessagesKey(id),
|
|
109
|
+
worktreeMessageIndexKey(id),
|
|
110
|
+
worktreeMessageSeqKey(id),
|
|
111
|
+
]);
|
|
112
|
+
await client.del(keys);
|
|
113
|
+
}
|
|
114
|
+
await client.del(sessionWorktreesKey(sessionId));
|
|
115
|
+
await client.del(sessionKey(sessionId));
|
|
116
|
+
await client.sRem(globalSessionsKey(), sessionId);
|
|
117
|
+
if (workspaceId) {
|
|
118
|
+
await client.sRem(workspaceSessionsKey(workspaceId), sessionId);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const listSessions = async (workspaceId) => {
|
|
123
|
+
await ensureConnected();
|
|
124
|
+
const ids = workspaceId
|
|
125
|
+
? await client.sMembers(workspaceSessionsKey(workspaceId))
|
|
126
|
+
: await client.sMembers(globalSessionsKey());
|
|
127
|
+
if (!ids.length) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
const keys = ids.map((id) => sessionKey(id));
|
|
131
|
+
const raw = await client.mGet(keys);
|
|
132
|
+
return raw.map(fromJson).filter(Boolean);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const touchSession = async (sessionId, workspaceId = null) => {
|
|
136
|
+
await ensureConnected();
|
|
137
|
+
await touchTtl(sessionKey(sessionId), sessionTtlMs);
|
|
138
|
+
await touchTtl(sessionWorktreesKey(sessionId), sessionTtlMs);
|
|
139
|
+
if (workspaceId) {
|
|
140
|
+
await touchTtl(workspaceSessionsKey(workspaceId), sessionTtlMs);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const saveWorktree = async (sessionId, worktreeId, data) => {
|
|
145
|
+
await ensureConnected();
|
|
146
|
+
await setWithTtl(worktreeKey(worktreeId), toJson(data), sessionTtlMs);
|
|
147
|
+
await client.sAdd(sessionWorktreesKey(sessionId), worktreeId);
|
|
148
|
+
await touchTtl(sessionWorktreesKey(sessionId), sessionTtlMs);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const getWorktree = async (worktreeId) => {
|
|
152
|
+
await ensureConnected();
|
|
153
|
+
const raw = await client.get(worktreeKey(worktreeId));
|
|
154
|
+
return fromJson(raw);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const deleteWorktree = async (sessionId, worktreeId) => {
|
|
158
|
+
await ensureConnected();
|
|
159
|
+
await client.del(
|
|
160
|
+
worktreeKey(worktreeId),
|
|
161
|
+
worktreeMessagesKey(worktreeId),
|
|
162
|
+
worktreeMessageIndexKey(worktreeId),
|
|
163
|
+
worktreeMessageSeqKey(worktreeId)
|
|
164
|
+
);
|
|
165
|
+
await client.sRem(sessionWorktreesKey(sessionId), worktreeId);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const listWorktrees = async (sessionId) => {
|
|
169
|
+
await ensureConnected();
|
|
170
|
+
const ids = await client.sMembers(sessionWorktreesKey(sessionId));
|
|
171
|
+
if (!ids.length) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const keys = ids.map((id) => worktreeKey(id));
|
|
175
|
+
const raw = await client.mGet(keys);
|
|
176
|
+
return raw.map(fromJson).filter(Boolean);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const appendWorktreeMessage = async (sessionId, worktreeId, message) => {
|
|
180
|
+
await ensureConnected();
|
|
181
|
+
const messageId = message?.id;
|
|
182
|
+
if (!messageId) {
|
|
183
|
+
throw new Error("Message id is required.");
|
|
184
|
+
}
|
|
185
|
+
const seq = await client.incr(worktreeMessageSeqKey(worktreeId));
|
|
186
|
+
await client.hSet(worktreeMessageIndexKey(worktreeId), messageId, seq);
|
|
187
|
+
await client.rPush(worktreeMessagesKey(worktreeId), toJson(message));
|
|
188
|
+
await touchTtl(worktreeMessagesKey(worktreeId), sessionTtlMs);
|
|
189
|
+
await touchTtl(worktreeMessageIndexKey(worktreeId), sessionTtlMs);
|
|
190
|
+
await touchTtl(worktreeMessageSeqKey(worktreeId), sessionTtlMs);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getWorktreeMessages = async (
|
|
194
|
+
sessionId,
|
|
195
|
+
worktreeId,
|
|
196
|
+
{ limit = null, beforeMessageId = null } = {}
|
|
197
|
+
) => {
|
|
198
|
+
await ensureConnected();
|
|
199
|
+
const listKey = worktreeMessagesKey(worktreeId);
|
|
200
|
+
const listLength = await client.lLen(listKey);
|
|
201
|
+
if (!listLength) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let startIndex = 0;
|
|
206
|
+
let endIndex = listLength - 1;
|
|
207
|
+
|
|
208
|
+
if (beforeMessageId) {
|
|
209
|
+
const seqValue = await client.hGet(
|
|
210
|
+
worktreeMessageIndexKey(worktreeId),
|
|
211
|
+
beforeMessageId
|
|
212
|
+
);
|
|
213
|
+
const seq = Number.parseInt(seqValue, 10);
|
|
214
|
+
if (!seq || Number.isNaN(seq)) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
startIndex = seq;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (limit && Number.isFinite(limit)) {
|
|
221
|
+
const minStart = Math.max(0, listLength - limit);
|
|
222
|
+
startIndex = Math.max(startIndex, minStart);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (startIndex > endIndex) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const raw = await client.lRange(listKey, startIndex, endIndex);
|
|
230
|
+
return raw.map(fromJson).filter(Boolean);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const clearWorktreeMessages = async (sessionId, worktreeId) => {
|
|
234
|
+
await ensureConnected();
|
|
235
|
+
await client.del(
|
|
236
|
+
worktreeMessagesKey(worktreeId),
|
|
237
|
+
worktreeMessageIndexKey(worktreeId),
|
|
238
|
+
worktreeMessageSeqKey(worktreeId)
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const saveWorkspaceUserIds = async (workspaceId, data) => {
|
|
243
|
+
await ensureConnected();
|
|
244
|
+
await setWithTtl(workspaceUserIdsKey(workspaceId), toJson(data), sessionTtlMs);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const getWorkspaceUserIds = async (workspaceId) => {
|
|
248
|
+
await ensureConnected();
|
|
249
|
+
const raw = await client.get(workspaceUserIdsKey(workspaceId));
|
|
250
|
+
return fromJson(raw);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const saveWorkspaceRefreshToken = async (
|
|
254
|
+
workspaceId,
|
|
255
|
+
tokenHash,
|
|
256
|
+
expiresAt,
|
|
257
|
+
ttlMs,
|
|
258
|
+
_options = {}
|
|
259
|
+
) => {
|
|
260
|
+
await ensureConnected();
|
|
261
|
+
const tokenPayload = {
|
|
262
|
+
workspaceId,
|
|
263
|
+
tokenHash,
|
|
264
|
+
expiresAt,
|
|
265
|
+
consumedAt: null,
|
|
266
|
+
replacedByHash: null,
|
|
267
|
+
};
|
|
268
|
+
if (ttlMs && ttlMs > 0) {
|
|
269
|
+
await client.set(refreshTokenKey(tokenHash), toJson(tokenPayload), { PX: ttlMs });
|
|
270
|
+
} else {
|
|
271
|
+
await client.set(refreshTokenKey(tokenHash), toJson(tokenPayload));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const getWorkspaceRefreshToken = async (tokenHash) => {
|
|
276
|
+
await ensureConnected();
|
|
277
|
+
const raw = await client.get(refreshTokenKey(tokenHash));
|
|
278
|
+
return fromJson(raw);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const rotateWorkspaceRefreshToken = async (
|
|
282
|
+
tokenHash,
|
|
283
|
+
nextTokenHash,
|
|
284
|
+
nextExpiresAt,
|
|
285
|
+
nextTtlMs
|
|
286
|
+
) => {
|
|
287
|
+
await ensureConnected();
|
|
288
|
+
const key = refreshTokenKey(tokenHash);
|
|
289
|
+
const nextKey = refreshTokenKey(nextTokenHash);
|
|
290
|
+
const attempts = 8;
|
|
291
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
292
|
+
await client.watch(key);
|
|
293
|
+
try {
|
|
294
|
+
const raw = await client.get(key);
|
|
295
|
+
const record = fromJson(raw);
|
|
296
|
+
if (!record?.workspaceId) {
|
|
297
|
+
await client.unwatch();
|
|
298
|
+
return { ok: false, code: "invalid_refresh_token" };
|
|
299
|
+
}
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
if (record.consumedAt) {
|
|
302
|
+
await client.unwatch();
|
|
303
|
+
return { ok: false, code: "refresh_token_reused" };
|
|
304
|
+
}
|
|
305
|
+
if (record.expiresAt && record.expiresAt <= now) {
|
|
306
|
+
const expireTx = client.multi();
|
|
307
|
+
expireTx.del(key);
|
|
308
|
+
await expireTx.exec();
|
|
309
|
+
return { ok: false, code: "refresh_token_expired" };
|
|
310
|
+
}
|
|
311
|
+
const oldRemainingTtlMs = Math.max(1, (record.expiresAt || now) - now);
|
|
312
|
+
const updated = {
|
|
313
|
+
...record,
|
|
314
|
+
consumedAt: now,
|
|
315
|
+
replacedByHash: nextTokenHash,
|
|
316
|
+
};
|
|
317
|
+
const nextPayload = {
|
|
318
|
+
workspaceId: record.workspaceId,
|
|
319
|
+
tokenHash: nextTokenHash,
|
|
320
|
+
expiresAt: nextExpiresAt,
|
|
321
|
+
consumedAt: null,
|
|
322
|
+
replacedByHash: null,
|
|
323
|
+
};
|
|
324
|
+
const tx = client.multi();
|
|
325
|
+
tx.set(key, toJson(updated), { PX: oldRemainingTtlMs });
|
|
326
|
+
if (nextTtlMs && nextTtlMs > 0) {
|
|
327
|
+
tx.set(nextKey, toJson(nextPayload), { PX: nextTtlMs });
|
|
328
|
+
} else {
|
|
329
|
+
tx.set(nextKey, toJson(nextPayload));
|
|
330
|
+
}
|
|
331
|
+
const execResult = await tx.exec();
|
|
332
|
+
if (execResult) {
|
|
333
|
+
return { ok: true, workspaceId: record.workspaceId };
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
await client.unwatch();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw new Error("Unable to rotate refresh token.");
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const deleteWorkspaceRefreshToken = async (tokenHash) => {
|
|
343
|
+
await ensureConnected();
|
|
344
|
+
await client.del(refreshTokenKey(tokenHash));
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const cleanupWorkspaceRefreshTokens = async () => {
|
|
348
|
+
// TTL-based cleanup handled by Redis expiration.
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const getNextWorkspaceUid = async () => {
|
|
352
|
+
await ensureConnected();
|
|
353
|
+
const key = workspaceUidSeqKey();
|
|
354
|
+
const current = await client.get(key);
|
|
355
|
+
if (current === null) {
|
|
356
|
+
await client.set(key, String(workspaceUidMin - 1));
|
|
357
|
+
}
|
|
358
|
+
const next = await client.incr(key);
|
|
359
|
+
if (next > workspaceUidMax) {
|
|
360
|
+
throw new Error("Workspace UID range exhausted.");
|
|
361
|
+
}
|
|
362
|
+
return Number(next);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const saveWorkspace = async (workspaceId, data) => {
|
|
366
|
+
await ensureConnected();
|
|
367
|
+
await client.set(workspaceKey(workspaceId), toJson(data));
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const getWorkspace = async (workspaceId) => {
|
|
371
|
+
await ensureConnected();
|
|
372
|
+
const raw = await client.get(workspaceKey(workspaceId));
|
|
373
|
+
return fromJson(raw);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const appendWorkspaceAuditEvent = async (workspaceId, data) => {
|
|
377
|
+
await ensureConnected();
|
|
378
|
+
await client.rPush(workspaceAuditEventsKey(workspaceId), toJson(data));
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
init: ensureConnected,
|
|
383
|
+
close: async () => {
|
|
384
|
+
if (client.isOpen) {
|
|
385
|
+
await client.quit();
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
saveSession,
|
|
389
|
+
getSession,
|
|
390
|
+
deleteSession,
|
|
391
|
+
listSessions,
|
|
392
|
+
touchSession,
|
|
393
|
+
saveWorktree,
|
|
394
|
+
getWorktree,
|
|
395
|
+
deleteWorktree,
|
|
396
|
+
listWorktrees,
|
|
397
|
+
appendWorktreeMessage,
|
|
398
|
+
getWorktreeMessages,
|
|
399
|
+
clearWorktreeMessages,
|
|
400
|
+
saveWorkspaceUserIds,
|
|
401
|
+
getWorkspaceUserIds,
|
|
402
|
+
saveWorkspaceRefreshToken,
|
|
403
|
+
getWorkspaceRefreshToken,
|
|
404
|
+
rotateWorkspaceRefreshToken,
|
|
405
|
+
deleteWorkspaceRefreshToken,
|
|
406
|
+
cleanupWorkspaceRefreshTokens,
|
|
407
|
+
getNextWorkspaceUid,
|
|
408
|
+
saveWorkspace,
|
|
409
|
+
getWorkspace,
|
|
410
|
+
appendWorkspaceAuditEvent,
|
|
411
|
+
};
|
|
412
|
+
};
|