@openchamber/web 1.0.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/README.md +34 -0
- package/bin/cli.js +561 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +18 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MonacoDiffViewer-J2AIDXvs.js +1 -0
- package/dist/assets/ToolOutputDialog-B0y5ge-3.js +5 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-iDfKTtMQ.css +1 -0
- package/dist/assets/index-kNntYPVa.js +2 -0
- package/dist/assets/main-BEJ2XliY.css +1 -0
- package/dist/assets/main-Ba339xde.js +59 -0
- package/dist/assets/vendor--B3aGWKBE.css +32 -0
- package/dist/assets/vendor-.pnpm-B1ce5n1Z.js +3192 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/index.html +197 -0
- package/dist/logo-dark.svg +4 -0
- package/dist/logo-light.svg +4 -0
- package/dist/site.webmanifest +36 -0
- package/dist/vite.svg +1 -0
- package/package.json +92 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +18 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/logo-dark.svg +4 -0
- package/public/logo-light.svg +4 -0
- package/public/site.webmanifest +36 -0
- package/public/vite.svg +1 -0
- package/server/index.d.ts +28 -0
- package/server/index.js +3038 -0
- package/server/lib/git-identity-storage.js +108 -0
- package/server/lib/git-service.js +899 -0
- package/server/lib/opencode-config.js +471 -0
- package/server/lib/opencode-config.js.d.ts +12 -0
- package/server/lib/ui-auth.js +266 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
const SESSION_COOKIE_NAME = 'oc_ui_session';
|
|
4
|
+
const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
5
|
+
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
const isSecureRequest = (req) => {
|
|
8
|
+
if (req.secure) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
12
|
+
if (typeof forwardedProto === 'string') {
|
|
13
|
+
const firstProto = forwardedProto.split(',')[0]?.trim().toLowerCase();
|
|
14
|
+
return firstProto === 'https';
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parseCookies = (cookieHeader) => {
|
|
20
|
+
if (!cookieHeader || typeof cookieHeader !== 'string') {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return cookieHeader.split(';').reduce((acc, segment) => {
|
|
25
|
+
const [name, ...rest] = segment.split('=');
|
|
26
|
+
if (!name) {
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
const key = name.trim();
|
|
30
|
+
if (!key) {
|
|
31
|
+
return acc;
|
|
32
|
+
}
|
|
33
|
+
const value = rest.join('=').trim();
|
|
34
|
+
acc[key] = decodeURIComponent(value || '');
|
|
35
|
+
return acc;
|
|
36
|
+
}, {});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const buildCookie = ({
|
|
40
|
+
name,
|
|
41
|
+
value,
|
|
42
|
+
maxAge,
|
|
43
|
+
secure,
|
|
44
|
+
}) => {
|
|
45
|
+
const attributes = [
|
|
46
|
+
`${name}=${value}`,
|
|
47
|
+
'Path=/',
|
|
48
|
+
'HttpOnly',
|
|
49
|
+
'SameSite=Strict',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
if (typeof maxAge === 'number') {
|
|
53
|
+
attributes.push(`Max-Age=${Math.max(0, Math.floor(maxAge))}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const expires = maxAge === 0
|
|
57
|
+
? 'Thu, 01 Jan 1970 00:00:00 GMT'
|
|
58
|
+
: new Date(Date.now() + maxAge * 1000).toUTCString();
|
|
59
|
+
|
|
60
|
+
attributes.push(`Expires=${expires}`);
|
|
61
|
+
|
|
62
|
+
if (secure) {
|
|
63
|
+
attributes.push('Secure');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return attributes.join('; ');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const normalizePassword = (candidate) => {
|
|
70
|
+
if (typeof candidate !== 'string') {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
return candidate.normalize().trim();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const createUiAuth = ({
|
|
77
|
+
password,
|
|
78
|
+
cookieName = SESSION_COOKIE_NAME,
|
|
79
|
+
sessionTtlMs = SESSION_TTL_MS,
|
|
80
|
+
} = {}) => {
|
|
81
|
+
const normalizedPassword = normalizePassword(password);
|
|
82
|
+
|
|
83
|
+
if (!normalizedPassword) {
|
|
84
|
+
return {
|
|
85
|
+
enabled: false,
|
|
86
|
+
requireAuth: (_req, _res, next) => next(),
|
|
87
|
+
handleSessionStatus: (_req, res) => {
|
|
88
|
+
res.json({ authenticated: true, disabled: true });
|
|
89
|
+
},
|
|
90
|
+
handleSessionCreate: (_req, res) => {
|
|
91
|
+
res.status(400).json({ error: 'UI password not configured' });
|
|
92
|
+
},
|
|
93
|
+
dispose: () => {
|
|
94
|
+
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const salt = crypto.randomBytes(16);
|
|
100
|
+
const expectedHash = crypto.scryptSync(normalizedPassword, salt, 64);
|
|
101
|
+
const sessions = new Map();
|
|
102
|
+
|
|
103
|
+
let cleanupTimer = null;
|
|
104
|
+
|
|
105
|
+
const getTokenFromRequest = (req) => {
|
|
106
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
107
|
+
if (cookies[cookieName]) {
|
|
108
|
+
return cookies[cookieName];
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const dropSession = (token) => {
|
|
114
|
+
if (token) {
|
|
115
|
+
sessions.delete(token);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const setSessionCookie = (req, res, token) => {
|
|
120
|
+
const secure = isSecureRequest(req);
|
|
121
|
+
const maxAgeSeconds = Math.floor(sessionTtlMs / 1000);
|
|
122
|
+
const header = buildCookie({
|
|
123
|
+
name: cookieName,
|
|
124
|
+
value: encodeURIComponent(token),
|
|
125
|
+
maxAge: maxAgeSeconds,
|
|
126
|
+
secure,
|
|
127
|
+
});
|
|
128
|
+
res.setHeader('Set-Cookie', header);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const clearSessionCookie = (req, res) => {
|
|
132
|
+
const secure = isSecureRequest(req);
|
|
133
|
+
const header = buildCookie({
|
|
134
|
+
name: cookieName,
|
|
135
|
+
value: '',
|
|
136
|
+
maxAge: 0,
|
|
137
|
+
secure,
|
|
138
|
+
});
|
|
139
|
+
res.setHeader('Set-Cookie', header);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const verifyPassword = (candidate) => {
|
|
143
|
+
if (!candidate) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const normalizedCandidate = normalizePassword(candidate);
|
|
147
|
+
if (!normalizedCandidate) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const candidateHash = crypto.scryptSync(normalizedCandidate, salt, 64);
|
|
152
|
+
return crypto.timingSafeEqual(candidateHash, expectedHash);
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const isSessionValid = (token) => {
|
|
159
|
+
if (!token) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const record = sessions.get(token);
|
|
163
|
+
if (!record) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (Date.now() - record.lastSeen > sessionTtlMs) {
|
|
167
|
+
sessions.delete(token);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
record.lastSeen = Date.now();
|
|
171
|
+
return true;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const issueSession = (req, res) => {
|
|
175
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
sessions.set(token, { createdAt: now, lastSeen: now });
|
|
178
|
+
setSessionCookie(req, res, token);
|
|
179
|
+
return token;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const cleanupStaleSessions = () => {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
for (const [token, record] of sessions.entries()) {
|
|
185
|
+
if (now - record.lastSeen > sessionTtlMs) {
|
|
186
|
+
sessions.delete(token);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const startCleanup = () => {
|
|
192
|
+
if (!cleanupTimer) {
|
|
193
|
+
cleanupTimer = setInterval(cleanupStaleSessions, CLEANUP_INTERVAL_MS);
|
|
194
|
+
if (cleanupTimer && typeof cleanupTimer.unref === 'function') {
|
|
195
|
+
cleanupTimer.unref();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
startCleanup();
|
|
201
|
+
|
|
202
|
+
const respondUnauthorized = (req, res) => {
|
|
203
|
+
res.status(401);
|
|
204
|
+
const acceptsJson = req.headers.accept?.includes('application/json');
|
|
205
|
+
if (acceptsJson || req.path.startsWith('/api')) {
|
|
206
|
+
res.json({ error: 'UI authentication required', locked: true });
|
|
207
|
+
} else {
|
|
208
|
+
res.type('text/plain').send('Authentication required');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const requireAuth = (req, res, next) => {
|
|
213
|
+
if (req.method === 'OPTIONS') {
|
|
214
|
+
return next();
|
|
215
|
+
}
|
|
216
|
+
const token = getTokenFromRequest(req);
|
|
217
|
+
if (isSessionValid(token)) {
|
|
218
|
+
return next();
|
|
219
|
+
}
|
|
220
|
+
clearSessionCookie(req, res);
|
|
221
|
+
return respondUnauthorized(req, res);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleSessionStatus = (req, res) => {
|
|
225
|
+
const token = getTokenFromRequest(req);
|
|
226
|
+
if (isSessionValid(token)) {
|
|
227
|
+
res.json({ authenticated: true });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
clearSessionCookie(req, res);
|
|
231
|
+
res.status(401).json({ authenticated: false, locked: true });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handleSessionCreate = (req, res) => {
|
|
235
|
+
const candidate = typeof req.body?.password === 'string' ? req.body.password : '';
|
|
236
|
+
if (!verifyPassword(candidate)) {
|
|
237
|
+
clearSessionCookie(req, res);
|
|
238
|
+
res.status(401).json({ error: 'Invalid password', locked: true });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const previousToken = getTokenFromRequest(req);
|
|
243
|
+
if (previousToken) {
|
|
244
|
+
dropSession(previousToken);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
issueSession(req, res);
|
|
248
|
+
res.json({ authenticated: true });
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const dispose = () => {
|
|
252
|
+
if (cleanupTimer) {
|
|
253
|
+
clearInterval(cleanupTimer);
|
|
254
|
+
cleanupTimer = null;
|
|
255
|
+
}
|
|
256
|
+
sessions.clear();
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
enabled: true,
|
|
261
|
+
requireAuth,
|
|
262
|
+
handleSessionStatus,
|
|
263
|
+
handleSessionCreate,
|
|
264
|
+
dispose,
|
|
265
|
+
};
|
|
266
|
+
};
|