ocean-brain 0.2.1 → 0.3.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.
- package/dist/index.js +73 -41
- package/dist/mcp.js +587 -26
- package/package.json +6 -1
- package/scripts/postinstall-prisma.mjs +41 -0
- package/server/client/dist/assets/Calendar-DRv2aEl8.js +28 -0
- package/server/client/dist/assets/Callout-B2kPcDrq.js +1 -0
- package/server/client/dist/assets/Graph-DotbTrZR.js +1 -0
- package/server/client/dist/assets/Image-B5R7gF2P.js +1 -0
- package/server/client/dist/assets/Image.es-Dld9r9MJ.js +1 -0
- package/server/client/dist/assets/ModalActionRow-CES9L_Gy.js +1 -0
- package/server/client/dist/assets/Note-SK4hDhEM.css +1 -0
- package/server/client/dist/assets/Note-cIbXJc1v.js +1 -0
- package/server/client/dist/assets/Reminders-4LunlZRb.js +1 -0
- package/server/client/dist/assets/Search-Cp17DGR6.js +1 -0
- package/server/client/dist/assets/SurfaceCard-yzVU4Qkx.js +1 -0
- package/server/client/dist/assets/Tag-CwmuMPmo.js +1 -0
- package/server/client/dist/assets/TagNotes-gGroWMtT.js +1 -0
- package/server/client/dist/assets/Trash.es-B3KWD8_C.js +1 -0
- package/server/client/dist/assets/color-wpu7wT_s.js +1 -0
- package/server/client/dist/assets/graph-vendor-CUxe67Lr.js +46 -0
- package/server/client/dist/assets/image.api-DJgOBt8S.js +17 -0
- package/server/client/dist/assets/index-40Y2UsVr.js +205 -0
- package/server/client/dist/assets/index-BnxpTwtL.css +1 -0
- package/server/client/dist/assets/index-DYUg2N-6.js +1 -0
- package/server/client/dist/assets/manage-image-ByY1mRZx.js +1 -0
- package/server/client/dist/assets/manage-image-detail-BVaDZHRu.js +1 -0
- package/server/client/dist/assets/mcp-Jj3061aI.js +1 -0
- package/server/client/dist/assets/{note-core-DTIu0anW.js → note-core-BCgMq5QA.js} +7 -7
- package/server/client/dist/assets/note-vendor-BofYbzmZ.js +54 -0
- package/server/client/dist/assets/placeholder-4P_GK7fV.js +34 -0
- package/server/client/dist/assets/trash-CPBwY07S.js +1 -0
- package/server/client/dist/assets/useReminderMutate-1YVndM2T.js +82 -0
- package/server/client/dist/index.html +5 -4
- package/server/dist/app.js +97 -9
- package/server/dist/app.js.map +1 -1
- package/server/dist/main.js +16 -2
- package/server/dist/main.js.map +1 -1
- package/server/dist/modules/auth-guard.js +73 -0
- package/server/dist/modules/auth-guard.js.map +1 -0
- package/server/dist/modules/auth-mode.js +51 -0
- package/server/dist/modules/auth-mode.js.map +1 -0
- package/server/dist/modules/auth.js +9 -0
- package/server/dist/modules/auth.js.map +1 -1
- package/server/dist/modules/blocknote.js +7 -1
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/modules/error-handler.js +41 -0
- package/server/dist/modules/error-handler.js.map +1 -0
- package/server/dist/modules/image-upload.js +114 -0
- package/server/dist/modules/image-upload.js.map +1 -0
- package/server/dist/modules/mcp-admin.js +88 -0
- package/server/dist/modules/mcp-admin.js.map +1 -0
- package/server/dist/modules/mcp-auth.js +71 -0
- package/server/dist/modules/mcp-auth.js.map +1 -0
- package/server/dist/modules/mcp-token.js +28 -0
- package/server/dist/modules/mcp-token.js.map +1 -0
- package/server/dist/modules/note-authoring.js +140 -0
- package/server/dist/modules/note-authoring.js.map +1 -0
- package/server/dist/modules/note-cleanup.js +207 -0
- package/server/dist/modules/note-cleanup.js.map +1 -0
- package/server/dist/modules/note-snapshot.js +223 -0
- package/server/dist/modules/note-snapshot.js.map +1 -0
- package/server/dist/modules/note-trash.js +262 -0
- package/server/dist/modules/note-trash.js.map +1 -0
- package/server/dist/modules/prisma-runtime.js +46 -0
- package/server/dist/modules/prisma-runtime.js.map +1 -0
- package/server/dist/modules/recovery-retention.js +26 -0
- package/server/dist/modules/recovery-retention.js.map +1 -0
- package/server/dist/modules/remote-image.js +176 -0
- package/server/dist/modules/remote-image.js.map +1 -0
- package/server/dist/modules/tag-organization.js +66 -0
- package/server/dist/modules/tag-organization.js.map +1 -0
- package/server/dist/modules/use-async.js +2 -4
- package/server/dist/modules/use-async.js.map +1 -1
- package/server/dist/schema/note/index.js +150 -6
- package/server/dist/schema/note/index.js.map +1 -1
- package/server/dist/start.js +11 -0
- package/server/dist/start.js.map +1 -0
- package/server/dist/types/index.js.map +1 -1
- package/server/dist/urls.js +3 -2
- package/server/dist/urls.js.map +1 -1
- package/server/dist/views/auth.js +305 -0
- package/server/dist/views/auth.js.map +1 -0
- package/server/dist/views/image.js +24 -81
- package/server/dist/views/image.js.map +1 -1
- package/server/dist/views/index.js +4 -0
- package/server/dist/views/index.js.map +1 -1
- package/server/dist/views/mcp-admin.js +42 -0
- package/server/dist/views/mcp-admin.js.map +1 -0
- package/server/dist/views/note.js +124 -0
- package/server/dist/views/note.js.map +1 -0
- package/server/dist/views/tag.js +23 -0
- package/server/dist/views/tag.js.map +1 -0
- package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
- package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
- package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
- package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
- package/server/prisma/schema.prisma +66 -1
- package/server/client/dist/assets/index-BJgBFWwL.css +0 -1
- package/server/client/dist/assets/index-DvNRv9YC.js +0 -391
- package/server/client/dist/assets/note-vendor-CUVHa9LU.js +0 -54
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { compareSharedSecret } from "../modules/auth.js";
|
|
2
|
+
import { createAppError } from "../modules/error-handler.js";
|
|
3
|
+
const buildSessionResponse = (authConfig, req) => ({
|
|
4
|
+
mode: authConfig.mode,
|
|
5
|
+
authRequired: authConfig.mode === "password",
|
|
6
|
+
authenticated: authConfig.mode === "password" ? Boolean(req.session?.authenticated) : false
|
|
7
|
+
});
|
|
8
|
+
const regenerateSession = async (req) => {
|
|
9
|
+
await new Promise((resolve, reject) => {
|
|
10
|
+
req.session.regenerate((error) => {
|
|
11
|
+
if (error) {
|
|
12
|
+
reject(error);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
resolve();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const destroySession = async (req) => {
|
|
20
|
+
if (!req.session) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
req.session.destroy((error) => {
|
|
25
|
+
if (error) {
|
|
26
|
+
reject(error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const sanitizeRedirectPath = (value) => {
|
|
34
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
35
|
+
return "/";
|
|
36
|
+
}
|
|
37
|
+
if (value.startsWith("/")) {
|
|
38
|
+
if (value.startsWith("//") || value.startsWith("/auth/login")) {
|
|
39
|
+
return "/";
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const redirectUrl = new URL(value);
|
|
45
|
+
const hostname = redirectUrl.hostname.toLowerCase();
|
|
46
|
+
if (!["http:", "https:"].includes(redirectUrl.protocol)) {
|
|
47
|
+
return "/";
|
|
48
|
+
}
|
|
49
|
+
if (!["localhost", "127.0.0.1", "::1", "[::1]"].includes(hostname)) {
|
|
50
|
+
return "/";
|
|
51
|
+
}
|
|
52
|
+
if (redirectUrl.pathname.startsWith("/auth/login")) {
|
|
53
|
+
return "/";
|
|
54
|
+
}
|
|
55
|
+
return `${redirectUrl.origin}${redirectUrl.pathname}${redirectUrl.search}${redirectUrl.hash}`;
|
|
56
|
+
} catch {
|
|
57
|
+
return "/";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
61
|
+
const renderLoginPage = ({
|
|
62
|
+
nextPath,
|
|
63
|
+
errorMessage
|
|
64
|
+
}) => {
|
|
65
|
+
const escapedNextPath = escapeHtml(nextPath);
|
|
66
|
+
const escapedErrorMessage = errorMessage ? escapeHtml(errorMessage) : "";
|
|
67
|
+
return `<!doctype html>
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8" />
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
72
|
+
<title>Ocean Brain Sign In</title>
|
|
73
|
+
<style>
|
|
74
|
+
:root {
|
|
75
|
+
color-scheme: light;
|
|
76
|
+
font-family: "Pretendard Variable", "Segoe UI", sans-serif;
|
|
77
|
+
background: #eef2f6;
|
|
78
|
+
color: #111827;
|
|
79
|
+
}
|
|
80
|
+
body {
|
|
81
|
+
margin: 0;
|
|
82
|
+
min-height: 100vh;
|
|
83
|
+
display: grid;
|
|
84
|
+
place-items: center;
|
|
85
|
+
background:
|
|
86
|
+
radial-gradient(circle at top, rgba(148, 163, 184, 0.14), transparent 34%),
|
|
87
|
+
linear-gradient(180deg, #f7fafc 0%, #eef2f6 100%);
|
|
88
|
+
padding: 24px;
|
|
89
|
+
}
|
|
90
|
+
.card {
|
|
91
|
+
width: min(420px, calc(100vw - 32px));
|
|
92
|
+
padding: 28px;
|
|
93
|
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
94
|
+
border-radius: 22px;
|
|
95
|
+
background: rgba(255, 255, 255, 0.94);
|
|
96
|
+
box-shadow:
|
|
97
|
+
0 24px 48px -32px rgba(15, 23, 42, 0.24),
|
|
98
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
|
99
|
+
backdrop-filter: blur(10px);
|
|
100
|
+
}
|
|
101
|
+
.eyebrow {
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
margin-bottom: 16px;
|
|
104
|
+
color: #64748b;
|
|
105
|
+
font-size: 12px;
|
|
106
|
+
font-weight: 700;
|
|
107
|
+
letter-spacing: 0.12em;
|
|
108
|
+
text-transform: uppercase;
|
|
109
|
+
}
|
|
110
|
+
h1 {
|
|
111
|
+
margin: 0 0 10px;
|
|
112
|
+
font-size: clamp(28px, 4vw, 32px);
|
|
113
|
+
line-height: 1.08;
|
|
114
|
+
letter-spacing: -0.03em;
|
|
115
|
+
}
|
|
116
|
+
p {
|
|
117
|
+
margin: 0 0 22px;
|
|
118
|
+
line-height: 1.6;
|
|
119
|
+
color: #475569;
|
|
120
|
+
}
|
|
121
|
+
label {
|
|
122
|
+
display: block;
|
|
123
|
+
margin-bottom: 10px;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
letter-spacing: 0.02em;
|
|
127
|
+
color: #334155;
|
|
128
|
+
}
|
|
129
|
+
form {
|
|
130
|
+
display: grid;
|
|
131
|
+
gap: 16px;
|
|
132
|
+
}
|
|
133
|
+
.field {
|
|
134
|
+
display: grid;
|
|
135
|
+
}
|
|
136
|
+
input {
|
|
137
|
+
width: 100%;
|
|
138
|
+
box-sizing: border-box;
|
|
139
|
+
padding: 14px 16px;
|
|
140
|
+
border: 1px solid #cbd5e1;
|
|
141
|
+
border-radius: 14px;
|
|
142
|
+
background: rgba(248, 250, 252, 0.96);
|
|
143
|
+
font-size: 16px;
|
|
144
|
+
color: #0f172a;
|
|
145
|
+
transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
|
|
146
|
+
}
|
|
147
|
+
input:focus {
|
|
148
|
+
outline: none;
|
|
149
|
+
border-color: #3b82f6;
|
|
150
|
+
background: #ffffff;
|
|
151
|
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);
|
|
152
|
+
}
|
|
153
|
+
.hint {
|
|
154
|
+
margin-top: 10px;
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
color: #64748b;
|
|
157
|
+
}
|
|
158
|
+
button {
|
|
159
|
+
width: 100%;
|
|
160
|
+
border: 1px solid #1d4ed8;
|
|
161
|
+
border-radius: 14px;
|
|
162
|
+
background: #2563eb;
|
|
163
|
+
color: #eff6ff;
|
|
164
|
+
padding: 14px 18px;
|
|
165
|
+
font-size: 16px;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
box-shadow: 0 14px 28px -20px rgba(29, 78, 216, 0.45);
|
|
169
|
+
transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
|
|
170
|
+
}
|
|
171
|
+
button:hover {
|
|
172
|
+
background: #1d4ed8;
|
|
173
|
+
box-shadow: 0 18px 32px -24px rgba(29, 78, 216, 0.46);
|
|
174
|
+
}
|
|
175
|
+
button:active {
|
|
176
|
+
transform: translateY(1px);
|
|
177
|
+
}
|
|
178
|
+
button:focus-visible {
|
|
179
|
+
outline: none;
|
|
180
|
+
box-shadow:
|
|
181
|
+
0 0 0 4px rgba(59, 130, 246, 0.14),
|
|
182
|
+
0 16px 28px -22px rgba(29, 78, 216, 0.42);
|
|
183
|
+
}
|
|
184
|
+
.error {
|
|
185
|
+
margin-bottom: 18px;
|
|
186
|
+
padding: 13px 14px;
|
|
187
|
+
border: 1px solid rgba(239, 68, 68, 0.24);
|
|
188
|
+
border-radius: 14px;
|
|
189
|
+
background: rgba(254, 242, 242, 0.92);
|
|
190
|
+
color: #b91c1c;
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
font-weight: 700;
|
|
193
|
+
}
|
|
194
|
+
@media (max-width: 640px) {
|
|
195
|
+
body {
|
|
196
|
+
place-items: stretch;
|
|
197
|
+
padding: 18px;
|
|
198
|
+
}
|
|
199
|
+
.card {
|
|
200
|
+
width: auto;
|
|
201
|
+
padding: 24px;
|
|
202
|
+
margin: auto 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<main class="card">
|
|
209
|
+
<div class="eyebrow">Workspace Locked</div>
|
|
210
|
+
<h1>Protected Workspace</h1>
|
|
211
|
+
<p>Password mode is enabled. Sign in before Ocean Brain serves the app.</p>
|
|
212
|
+
${escapedErrorMessage ? `<div class="error">${escapedErrorMessage}</div>` : ""}
|
|
213
|
+
<form method="post" action="/auth/login">
|
|
214
|
+
<input type="hidden" name="next" value="${escapedNextPath}" />
|
|
215
|
+
<div class="field">
|
|
216
|
+
<label for="password">Password</label>
|
|
217
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required />
|
|
218
|
+
</div>
|
|
219
|
+
<button type="submit">Sign in</button>
|
|
220
|
+
</form>
|
|
221
|
+
<div class="hint">This session must be authenticated before the workspace loads.</div>
|
|
222
|
+
</main>
|
|
223
|
+
</body>
|
|
224
|
+
</html>`;
|
|
225
|
+
};
|
|
226
|
+
const createLoginHandler = (authConfig) => {
|
|
227
|
+
return async (req, res) => {
|
|
228
|
+
if (authConfig.mode !== "password" || !authConfig.password) {
|
|
229
|
+
throw createAppError(409, "AUTH_DISABLED", "Login is unavailable while auth mode is disabled.");
|
|
230
|
+
}
|
|
231
|
+
const password = typeof req.body?.password === "string" ? req.body.password : "";
|
|
232
|
+
if (!password || !compareSharedSecret(authConfig.password, password)) {
|
|
233
|
+
throw createAppError(401, "UNAUTHORIZED", "Invalid password");
|
|
234
|
+
}
|
|
235
|
+
await regenerateSession(req);
|
|
236
|
+
req.session.authenticated = true;
|
|
237
|
+
res.status(200).json(buildSessionResponse(authConfig, req)).end();
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
const createLoginPageHandler = (authConfig) => {
|
|
241
|
+
return async (req, res) => {
|
|
242
|
+
if (authConfig.mode !== "password" || req.session?.authenticated) {
|
|
243
|
+
res.redirect(303, sanitizeRedirectPath(req.query.next));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const nextPath = sanitizeRedirectPath(req.query.next);
|
|
247
|
+
res.status(200).type("html").send(renderLoginPage({ nextPath })).end();
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
const createLoginPageSubmitHandler = (authConfig) => {
|
|
251
|
+
return async (req, res) => {
|
|
252
|
+
const nextPath = sanitizeRedirectPath(req.body?.next);
|
|
253
|
+
if (authConfig.mode !== "password" || !authConfig.password) {
|
|
254
|
+
res.redirect(303, nextPath);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const password = typeof req.body?.password === "string" ? req.body.password : "";
|
|
258
|
+
if (!password || !compareSharedSecret(authConfig.password, password)) {
|
|
259
|
+
res.status(401).type("html").send(renderLoginPage({
|
|
260
|
+
nextPath,
|
|
261
|
+
errorMessage: "Invalid password"
|
|
262
|
+
})).end();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await regenerateSession(req);
|
|
266
|
+
req.session.authenticated = true;
|
|
267
|
+
res.redirect(303, nextPath);
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
const createLogoutPageHandler = (authConfig) => {
|
|
271
|
+
return async (req, res) => {
|
|
272
|
+
if (authConfig.mode === "password") {
|
|
273
|
+
await destroySession(req);
|
|
274
|
+
res.redirect(303, "/auth/login");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
res.redirect(303, "/");
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const createLogoutHandler = (authConfig) => {
|
|
281
|
+
return async (req, res) => {
|
|
282
|
+
if (authConfig.mode === "password") {
|
|
283
|
+
await destroySession(req);
|
|
284
|
+
}
|
|
285
|
+
res.status(200).json({
|
|
286
|
+
mode: authConfig.mode,
|
|
287
|
+
authRequired: authConfig.mode === "password",
|
|
288
|
+
authenticated: false
|
|
289
|
+
}).end();
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const createSessionStatusHandler = (authConfig) => {
|
|
293
|
+
return async (req, res) => {
|
|
294
|
+
res.status(200).json(buildSessionResponse(authConfig, req)).end();
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
export {
|
|
298
|
+
createLoginHandler,
|
|
299
|
+
createLoginPageHandler,
|
|
300
|
+
createLoginPageSubmitHandler,
|
|
301
|
+
createLogoutHandler,
|
|
302
|
+
createLogoutPageHandler,
|
|
303
|
+
createSessionStatusHandler
|
|
304
|
+
};
|
|
305
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/views/auth.ts"],"sourcesContent":["import type { Request } from 'express';\n\nimport type { Controller } from '~/types/index.js';\nimport { compareSharedSecret } from '~/modules/auth.js';\nimport type { AuthConfig } from '~/modules/auth-mode.js';\nimport { createAppError } from '~/modules/error-handler.js';\n\nconst buildSessionResponse = (authConfig: AuthConfig, req: Request) => ({\n mode: authConfig.mode,\n authRequired: authConfig.mode === 'password',\n authenticated: authConfig.mode === 'password' ? Boolean(req.session?.authenticated) : false\n});\n\nconst regenerateSession = async (req: Request) => {\n await new Promise<void>((resolve, reject) => {\n req.session.regenerate((error) => {\n if (error) {\n reject(error);\n return;\n }\n\n resolve();\n });\n });\n};\n\nconst destroySession = async (req: Request) => {\n if (!req.session) {\n return;\n }\n\n await new Promise<void>((resolve, reject) => {\n req.session.destroy((error) => {\n if (error) {\n reject(error);\n return;\n }\n\n resolve();\n });\n });\n};\n\nconst sanitizeRedirectPath = (value: unknown) => {\n if (typeof value !== 'string' || value.length === 0) {\n return '/';\n }\n\n if (value.startsWith('/')) {\n if (value.startsWith('//') || value.startsWith('/auth/login')) {\n return '/';\n }\n\n return value;\n }\n\n try {\n const redirectUrl = new URL(value);\n const hostname = redirectUrl.hostname.toLowerCase();\n\n if (!['http:', 'https:'].includes(redirectUrl.protocol)) {\n return '/';\n }\n\n if (!['localhost', '127.0.0.1', '::1', '[::1]'].includes(hostname)) {\n return '/';\n }\n\n if (redirectUrl.pathname.startsWith('/auth/login')) {\n return '/';\n }\n\n return `${redirectUrl.origin}${redirectUrl.pathname}${redirectUrl.search}${redirectUrl.hash}`;\n } catch {\n return '/';\n }\n};\n\nconst escapeHtml = (value: string) => value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll('\\'', ''');\n\nconst renderLoginPage = ({\n nextPath,\n errorMessage\n}: {\n nextPath: string;\n errorMessage?: string;\n}) => {\n const escapedNextPath = escapeHtml(nextPath);\n const escapedErrorMessage = errorMessage ? escapeHtml(errorMessage) : '';\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Ocean Brain Sign In</title>\n <style>\n :root {\n color-scheme: light;\n font-family: \"Pretendard Variable\", \"Segoe UI\", sans-serif;\n background: #eef2f6;\n color: #111827;\n }\n body {\n margin: 0;\n min-height: 100vh;\n display: grid;\n place-items: center;\n background:\n radial-gradient(circle at top, rgba(148, 163, 184, 0.14), transparent 34%),\n linear-gradient(180deg, #f7fafc 0%, #eef2f6 100%);\n padding: 24px;\n }\n .card {\n width: min(420px, calc(100vw - 32px));\n padding: 28px;\n border: 1px solid rgba(148, 163, 184, 0.28);\n border-radius: 22px;\n background: rgba(255, 255, 255, 0.94);\n box-shadow:\n 0 24px 48px -32px rgba(15, 23, 42, 0.24),\n inset 0 1px 0 rgba(255, 255, 255, 0.7);\n backdrop-filter: blur(10px);\n }\n .eyebrow {\n display: inline-flex;\n margin-bottom: 16px;\n color: #64748b;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n h1 {\n margin: 0 0 10px;\n font-size: clamp(28px, 4vw, 32px);\n line-height: 1.08;\n letter-spacing: -0.03em;\n }\n p {\n margin: 0 0 22px;\n line-height: 1.6;\n color: #475569;\n }\n label {\n display: block;\n margin-bottom: 10px;\n font-size: 13px;\n font-weight: 700;\n letter-spacing: 0.02em;\n color: #334155;\n }\n form {\n display: grid;\n gap: 16px;\n }\n .field {\n display: grid;\n }\n input {\n width: 100%;\n box-sizing: border-box;\n padding: 14px 16px;\n border: 1px solid #cbd5e1;\n border-radius: 14px;\n background: rgba(248, 250, 252, 0.96);\n font-size: 16px;\n color: #0f172a;\n transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;\n }\n input:focus {\n outline: none;\n border-color: #3b82f6;\n background: #ffffff;\n box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);\n }\n .hint {\n margin-top: 10px;\n font-size: 13px;\n color: #64748b;\n }\n button {\n width: 100%;\n border: 1px solid #1d4ed8;\n border-radius: 14px;\n background: #2563eb;\n color: #eff6ff;\n padding: 14px 18px;\n font-size: 16px;\n font-weight: 700;\n cursor: pointer;\n box-shadow: 0 14px 28px -20px rgba(29, 78, 216, 0.45);\n transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;\n }\n button:hover {\n background: #1d4ed8;\n box-shadow: 0 18px 32px -24px rgba(29, 78, 216, 0.46);\n }\n button:active {\n transform: translateY(1px);\n }\n button:focus-visible {\n outline: none;\n box-shadow:\n 0 0 0 4px rgba(59, 130, 246, 0.14),\n 0 16px 28px -22px rgba(29, 78, 216, 0.42);\n }\n .error {\n margin-bottom: 18px;\n padding: 13px 14px;\n border: 1px solid rgba(239, 68, 68, 0.24);\n border-radius: 14px;\n background: rgba(254, 242, 242, 0.92);\n color: #b91c1c;\n font-size: 14px;\n font-weight: 700;\n }\n @media (max-width: 640px) {\n body {\n place-items: stretch;\n padding: 18px;\n }\n .card {\n width: auto;\n padding: 24px;\n margin: auto 0;\n }\n }\n </style>\n</head>\n<body>\n <main class=\"card\">\n <div class=\"eyebrow\">Workspace Locked</div>\n <h1>Protected Workspace</h1>\n <p>Password mode is enabled. Sign in before Ocean Brain serves the app.</p>\n ${escapedErrorMessage ? `<div class=\"error\">${escapedErrorMessage}</div>` : ''}\n <form method=\"post\" action=\"/auth/login\">\n <input type=\"hidden\" name=\"next\" value=\"${escapedNextPath}\" />\n <div class=\"field\">\n <label for=\"password\">Password</label>\n <input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required />\n </div>\n <button type=\"submit\">Sign in</button>\n </form>\n <div class=\"hint\">This session must be authenticated before the workspace loads.</div>\n </main>\n</body>\n</html>`;\n};\n\nexport const createLoginHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode !== 'password' || !authConfig.password) {\n throw createAppError(409, 'AUTH_DISABLED', 'Login is unavailable while auth mode is disabled.');\n }\n\n const password = typeof req.body?.password === 'string'\n ? req.body.password\n : '';\n\n if (!password || !compareSharedSecret(authConfig.password, password)) {\n throw createAppError(401, 'UNAUTHORIZED', 'Invalid password');\n }\n\n await regenerateSession(req);\n req.session.authenticated = true;\n\n res.status(200).json(buildSessionResponse(authConfig, req)).end();\n };\n};\n\nexport const createLoginPageHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode !== 'password' || req.session?.authenticated) {\n res.redirect(303, sanitizeRedirectPath(req.query.next));\n return;\n }\n\n const nextPath = sanitizeRedirectPath(req.query.next);\n res.status(200).type('html').send(renderLoginPage({ nextPath })).end();\n };\n};\n\nexport const createLoginPageSubmitHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n const nextPath = sanitizeRedirectPath(req.body?.next);\n\n if (authConfig.mode !== 'password' || !authConfig.password) {\n res.redirect(303, nextPath);\n return;\n }\n\n const password = typeof req.body?.password === 'string'\n ? req.body.password\n : '';\n\n if (!password || !compareSharedSecret(authConfig.password, password)) {\n res\n .status(401)\n .type('html')\n .send(renderLoginPage({\n nextPath,\n errorMessage: 'Invalid password'\n }))\n .end();\n return;\n }\n\n await regenerateSession(req);\n req.session.authenticated = true;\n\n res.redirect(303, nextPath);\n };\n};\n\nexport const createLogoutPageHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode === 'password') {\n await destroySession(req);\n res.redirect(303, '/auth/login');\n return;\n }\n\n res.redirect(303, '/');\n };\n};\n\nexport const createLogoutHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n if (authConfig.mode === 'password') {\n await destroySession(req);\n }\n\n res.status(200).json({\n mode: authConfig.mode,\n authRequired: authConfig.mode === 'password',\n authenticated: false\n }).end();\n };\n};\n\nexport const createSessionStatusHandler = (authConfig: AuthConfig): Controller => {\n return async (req, res) => {\n res.status(200).json(buildSessionResponse(authConfig, req)).end();\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1,98 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import models from "../models.js";
|
|
5
|
-
import { paths } from "../paths.js";
|
|
6
|
-
const imageDir = paths.imageDir;
|
|
7
|
-
function ensureDirExists(dirPath) {
|
|
8
|
-
if (!fs.existsSync(dirPath)) {
|
|
9
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
-
}
|
|
11
|
-
}
|
|
1
|
+
import { createAppError } from "../modules/error-handler.js";
|
|
2
|
+
import { persistUploadedImage } from "../modules/image-upload.js";
|
|
3
|
+
import { fetchRemoteImage, RemoteImageFetchError } from "../modules/remote-image.js";
|
|
12
4
|
const uploadImage = async (req, res) => {
|
|
13
5
|
const { image } = req.body;
|
|
14
6
|
if (!image || !image.match(/data:image\/\s*(\w+);base64,/)) {
|
|
15
|
-
|
|
16
|
-
return;
|
|
7
|
+
throw createAppError(400, "INVALID_IMAGE_UPLOAD", "No image uploaded");
|
|
17
8
|
}
|
|
18
9
|
const [info, data] = image.split(",");
|
|
19
|
-
const hash = crpyto.createHash("sha512").update(data).digest("hex");
|
|
20
|
-
const exists = await models.image.findFirst({ where: { hash } });
|
|
21
|
-
if (exists) {
|
|
22
|
-
res.status(200).json({
|
|
23
|
-
id: exists.id,
|
|
24
|
-
url: exists.url
|
|
25
|
-
}).end();
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
10
|
const buffer = Buffer.from(data, "base64");
|
|
29
|
-
const currentPath = [
|
|
30
|
-
(/* @__PURE__ */ new Date()).getFullYear().toString(),
|
|
31
|
-
((/* @__PURE__ */ new Date()).getMonth() + 1).toString(),
|
|
32
|
-
(/* @__PURE__ */ new Date()).getDate().toString()
|
|
33
|
-
];
|
|
34
|
-
const targetDir = path.resolve(imageDir, ...currentPath);
|
|
35
|
-
ensureDirExists(targetDir);
|
|
36
11
|
const ext = info.split(";")[0].split("/")[1];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
res.status(500).json({ error: err }).end();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const url = "/assets/images/" + currentPath.join("/") + "/" + fileName;
|
|
44
|
-
const image2 = await models.image.create({
|
|
45
|
-
data: {
|
|
46
|
-
hash,
|
|
47
|
-
url
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
res.status(200).json({
|
|
51
|
-
id: image2.id,
|
|
52
|
-
url: image2.url
|
|
53
|
-
}).end();
|
|
12
|
+
const uploadedImage = await persistUploadedImage({
|
|
13
|
+
buffer,
|
|
14
|
+
extension: ext
|
|
54
15
|
});
|
|
16
|
+
res.status(200).json({
|
|
17
|
+
id: uploadedImage.id,
|
|
18
|
+
url: uploadedImage.url
|
|
19
|
+
}).end();
|
|
55
20
|
};
|
|
56
21
|
const uploadImageFromSrc = async (req, res) => {
|
|
57
22
|
const { src } = req.body;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
res.status(200).json({
|
|
64
|
-
id: exists.id,
|
|
65
|
-
url: exists.url
|
|
66
|
-
}).end();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const buffer = Buffer.from(data, "base64");
|
|
70
|
-
const currentPath = [
|
|
71
|
-
(/* @__PURE__ */ new Date()).getFullYear().toString(),
|
|
72
|
-
((/* @__PURE__ */ new Date()).getMonth() + 1).toString(),
|
|
73
|
-
(/* @__PURE__ */ new Date()).getDate().toString()
|
|
74
|
-
];
|
|
75
|
-
const targetDir = path.resolve(imageDir, ...currentPath);
|
|
76
|
-
ensureDirExists(targetDir);
|
|
77
|
-
const ext = "png";
|
|
78
|
-
const fileName = `${Date.now()}.${ext}`;
|
|
79
|
-
fs.writeFile(path.resolve(targetDir, fileName), buffer, async (err) => {
|
|
80
|
-
if (err) {
|
|
81
|
-
res.status(500).json({ error: err }).end();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const url = "/assets/images/" + currentPath.join("/") + "/" + fileName;
|
|
85
|
-
const image = await models.image.create({
|
|
86
|
-
data: {
|
|
87
|
-
hash,
|
|
88
|
-
url
|
|
89
|
-
}
|
|
23
|
+
try {
|
|
24
|
+
const remoteImage = await fetchRemoteImage(String(src ?? ""));
|
|
25
|
+
const uploadedImage = await persistUploadedImage({
|
|
26
|
+
buffer: remoteImage.buffer,
|
|
27
|
+
extension: remoteImage.extension
|
|
90
28
|
});
|
|
91
29
|
res.status(200).json({
|
|
92
|
-
id:
|
|
93
|
-
url:
|
|
30
|
+
id: uploadedImage.id,
|
|
31
|
+
url: uploadedImage.url
|
|
94
32
|
}).end();
|
|
95
|
-
})
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof RemoteImageFetchError) {
|
|
35
|
+
throw createAppError(error.status, error.code, error.message);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
96
39
|
};
|
|
97
40
|
export {
|
|
98
41
|
uploadImage,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/views/image.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/views/image.ts"],"sourcesContent":["import type { Controller } from '~/types/index.js';\nimport { createAppError } from '~/modules/error-handler.js';\nimport { persistUploadedImage } from '~/modules/image-upload.js';\nimport { fetchRemoteImage, RemoteImageFetchError } from '~/modules/remote-image.js';\n\nexport const uploadImage: Controller = async (req, res) => {\n const { image } = req.body;\n\n if (!image || !image.match(/data:image\\/\\s*(\\w+);base64,/)) {\n throw createAppError(400, 'INVALID_IMAGE_UPLOAD', 'No image uploaded');\n }\n\n const [info, data] = image.split(',');\n const buffer = Buffer.from(data, 'base64');\n const ext = info.split(';')[0].split('/')[1];\n const uploadedImage = await persistUploadedImage({\n buffer,\n extension: ext\n });\n\n res.status(200).json({\n id: uploadedImage.id,\n url: uploadedImage.url\n }).end();\n};\n\nexport const uploadImageFromSrc: Controller = async (req, res) => {\n const { src } = req.body;\n\n try {\n const remoteImage = await fetchRemoteImage(String(src ?? ''));\n const uploadedImage = await persistUploadedImage({\n buffer: remoteImage.buffer,\n extension: remoteImage.extension\n });\n\n res.status(200).json({\n id: uploadedImage.id,\n url: uploadedImage.url\n }).end();\n } catch (error) {\n if (error instanceof RemoteImageFetchError) {\n throw createAppError(error.status, error.code, error.message);\n }\n\n throw error;\n }\n};\n"],"mappings":"AACA,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,kBAAkB,6BAA6B;AAEjD,MAAM,cAA0B,OAAO,KAAK,QAAQ;AACvD,QAAM,EAAE,MAAM,IAAI,IAAI;AAEtB,MAAI,CAAC,SAAS,CAAC,MAAM,MAAM,8BAA8B,GAAG;AACxD,UAAM,eAAe,KAAK,wBAAwB,mBAAmB;AAAA,EACzE;AAEA,QAAM,CAAC,MAAM,IAAI,IAAI,MAAM,MAAM,GAAG;AACpC,QAAM,SAAS,OAAO,KAAK,MAAM,QAAQ;AACzC,QAAM,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3C,QAAM,gBAAgB,MAAM,qBAAqB;AAAA,IAC7C;AAAA,IACA,WAAW;AAAA,EACf,CAAC;AAED,MAAI,OAAO,GAAG,EAAE,KAAK;AAAA,IACjB,IAAI,cAAc;AAAA,IAClB,KAAK,cAAc;AAAA,EACvB,CAAC,EAAE,IAAI;AACX;AAEO,MAAM,qBAAiC,OAAO,KAAK,QAAQ;AAC9D,QAAM,EAAE,IAAI,IAAI,IAAI;AAEpB,MAAI;AACA,UAAM,cAAc,MAAM,iBAAiB,OAAO,OAAO,EAAE,CAAC;AAC5D,UAAM,gBAAgB,MAAM,qBAAqB;AAAA,MAC7C,QAAQ,YAAY;AAAA,MACpB,WAAW,YAAY;AAAA,IAC3B,CAAC;AAED,QAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MACjB,IAAI,cAAc;AAAA,MAClB,KAAK,cAAc;AAAA,IACvB,CAAC,EAAE,IAAI;AAAA,EACX,SAAS,OAAO;AACZ,QAAI,iBAAiB,uBAAuB;AACxC,YAAM,eAAe,MAAM,QAAQ,MAAM,MAAM,MAAM,OAAO;AAAA,IAChE;AAEA,UAAM;AAAA,EACV;AACJ;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/views/index.ts"],"sourcesContent":["export * from './image.js';\n"],"mappings":"AAAA,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/views/index.ts"],"sourcesContent":["export * from './image.js';\nexport * from './auth.js';\nexport * from './note.js';\nexport * from './tag.js';\nexport * from './mcp-admin.js';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createAppError } from "../modules/error-handler.js";
|
|
2
|
+
import { createMcpAdminService } from "../modules/mcp-admin.js";
|
|
3
|
+
const createMcpAdminStatusHandler = (service = createMcpAdminService()) => {
|
|
4
|
+
return async (_req, res) => {
|
|
5
|
+
const status = await service.getStatus();
|
|
6
|
+
res.status(200).json(status).end();
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
const createMcpAdminSetEnabledHandler = (service = createMcpAdminService()) => {
|
|
10
|
+
return async (req, res) => {
|
|
11
|
+
const enabled = req.body?.enabled;
|
|
12
|
+
if (typeof enabled !== "boolean") {
|
|
13
|
+
throw createAppError(400, "INVALID_MCP_ENABLED", "enabled must be a boolean.");
|
|
14
|
+
}
|
|
15
|
+
await service.setEnabled(enabled);
|
|
16
|
+
const status = await service.getStatus();
|
|
17
|
+
res.status(200).json(status).end();
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const createMcpAdminRotateTokenHandler = (service = createMcpAdminService()) => {
|
|
21
|
+
return async (_req, res) => {
|
|
22
|
+
const result = await service.rotateToken();
|
|
23
|
+
res.status(200).json({
|
|
24
|
+
token: result.token,
|
|
25
|
+
message: "Save this token now. It is shown only once."
|
|
26
|
+
}).end();
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const createMcpAdminRevokeTokenHandler = (service = createMcpAdminService()) => {
|
|
30
|
+
return async (_req, res) => {
|
|
31
|
+
await service.revokeActiveToken();
|
|
32
|
+
const status = await service.getStatus();
|
|
33
|
+
res.status(200).json(status).end();
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
createMcpAdminRevokeTokenHandler,
|
|
38
|
+
createMcpAdminRotateTokenHandler,
|
|
39
|
+
createMcpAdminSetEnabledHandler,
|
|
40
|
+
createMcpAdminStatusHandler
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=mcp-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/views/mcp-admin.ts"],"sourcesContent":["import { createAppError } from '~/modules/error-handler.js';\nimport { createMcpAdminService, type McpAdminService } from '~/modules/mcp-admin.js';\nimport type { Controller } from '~/types/index.js';\n\ntype McpAdminControllerService = Pick<\nMcpAdminService,\n'getStatus' | 'setEnabled' | 'rotateToken' | 'revokeActiveToken'\n>;\n\nexport const createMcpAdminStatusHandler = (\n service: McpAdminControllerService = createMcpAdminService()\n): Controller => {\n return async (_req, res) => {\n const status = await service.getStatus();\n res.status(200).json(status).end();\n };\n};\n\nexport const createMcpAdminSetEnabledHandler = (\n service: McpAdminControllerService = createMcpAdminService()\n): Controller => {\n return async (req, res) => {\n const enabled = req.body?.enabled;\n if (typeof enabled !== 'boolean') {\n throw createAppError(400, 'INVALID_MCP_ENABLED', 'enabled must be a boolean.');\n }\n\n await service.setEnabled(enabled);\n const status = await service.getStatus();\n res.status(200).json(status).end();\n };\n};\n\nexport const createMcpAdminRotateTokenHandler = (\n service: McpAdminControllerService = createMcpAdminService()\n): Controller => {\n return async (_req, res) => {\n const result = await service.rotateToken();\n res.status(200).json({\n token: result.token,\n message: 'Save this token now. It is shown only once.'\n }).end();\n };\n};\n\nexport const createMcpAdminRevokeTokenHandler = (\n service: McpAdminControllerService = createMcpAdminService()\n): Controller => {\n return async (_req, res) => {\n await service.revokeActiveToken();\n const status = await service.getStatus();\n res.status(200).json(status).end();\n };\n};\n"],"mappings":"AAAA,SAAS,sBAAsB;AAC/B,SAAS,6BAAmD;AAQrD,MAAM,8BAA8B,CACvC,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,SAAS,MAAM,QAAQ,UAAU;AACvC,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;AAEO,MAAM,kCAAkC,CAC3C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,KAAK,QAAQ;AACvB,UAAM,UAAU,IAAI,MAAM;AAC1B,QAAI,OAAO,YAAY,WAAW;AAC9B,YAAM,eAAe,KAAK,uBAAuB,4BAA4B;AAAA,IACjF;AAEA,UAAM,QAAQ,WAAW,OAAO;AAChC,UAAM,SAAS,MAAM,QAAQ,UAAU;AACvC,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;AAEO,MAAM,mCAAmC,CAC5C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,SAAS,MAAM,QAAQ,YAAY;AACzC,QAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MACjB,OAAO,OAAO;AAAA,MACd,SAAS;AAAA,IACb,CAAC,EAAE,IAAI;AAAA,EACX;AACJ;AAEO,MAAM,mCAAmC,CAC5C,UAAqC,sBAAsB,MAC9C;AACb,SAAO,OAAO,MAAM,QAAQ;AACxB,UAAM,QAAQ,kBAAkB;AAChC,UAAM,SAAS,MAAM,QAAQ,UAAU;AACvC,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,EAAE,IAAI;AAAA,EACrC;AACJ;","names":[]}
|