collabmd 0.1.31 → 0.1.32
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/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-Br4qyh3r.js} +1 -1
- package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
- package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
- package/dist/client/assets/index-CxSbUTs2.css +1 -0
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-BjHHMlv0.js +1249 -0
- package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +4 -1
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +176 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +2 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +67 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +26 -7
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +103 -28
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/bases/base-definition.js +255 -0
- package/src/server/domain/bases/base-expression-runtime.js +1159 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
- package/src/server/domain/bases/base-query-results.js +205 -0
- package/src/server/domain/bases/base-query-service.js +68 -1965
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +109 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Bia2hI25.js} +0 -0
|
@@ -1,552 +1,31 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import * as oidc from 'openid-client';
|
|
4
|
-
|
|
5
1
|
import { createSessionCookieManager, createSignedCookieManager } from './session-cookie.js';
|
|
6
|
-
|
|
7
|
-
export const AUTH_STRATEGY_NONE = 'none';
|
|
8
|
-
export const AUTH_STRATEGY_PASSWORD = 'password';
|
|
9
|
-
export const AUTH_STRATEGY_OIDC = 'oidc';
|
|
10
|
-
|
|
11
|
-
export const SUPPORTED_AUTH_STRATEGIES = new Set([
|
|
2
|
+
import {
|
|
12
3
|
AUTH_STRATEGY_NONE,
|
|
4
|
+
AUTH_STRATEGY_OIDC,
|
|
13
5
|
AUTH_STRATEGY_PASSWORD,
|
|
6
|
+
SUPPORTED_AUTH_STRATEGIES,
|
|
7
|
+
createRandomAuthPassword,
|
|
8
|
+
createRandomSessionSecret,
|
|
9
|
+
} from './auth-constants.js';
|
|
10
|
+
import {
|
|
11
|
+
buildClientConfig,
|
|
12
|
+
createResponse,
|
|
13
|
+
createUnauthorizedBody,
|
|
14
|
+
} from './auth-common.js';
|
|
15
|
+
import {
|
|
16
|
+
createNoneStrategy,
|
|
17
|
+
createOidcStrategy,
|
|
18
|
+
createPasswordStrategy,
|
|
19
|
+
} from './auth-strategies.js';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
AUTH_STRATEGY_NONE,
|
|
14
23
|
AUTH_STRATEGY_OIDC,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function createRandomAuthPassword(length = 18) {
|
|
21
|
-
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
|
22
|
-
const bytes = randomBytes(length);
|
|
23
|
-
let password = '';
|
|
24
|
-
|
|
25
|
-
for (let index = 0; index < length; index += 1) {
|
|
26
|
-
password += alphabet[bytes[index] % alphabet.length];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return password;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function createRandomSessionSecret() {
|
|
33
|
-
return randomBytes(32).toString('base64url');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function hashPassword(value) {
|
|
37
|
-
return createHash('sha256').update(String(value ?? ''), 'utf8').digest();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function createResponse(statusCode, body, options = {}) {
|
|
41
|
-
return {
|
|
42
|
-
body,
|
|
43
|
-
kind: options.kind || 'response',
|
|
44
|
-
redirectTo: options.redirectTo || '',
|
|
45
|
-
setCookie: options.setCookie || null,
|
|
46
|
-
statusCode,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function createUnauthorizedBody(clientConfig, {
|
|
51
|
-
error = 'Authentication required',
|
|
52
|
-
code = 'AUTH_REQUIRED',
|
|
53
|
-
} = {}) {
|
|
54
|
-
return {
|
|
55
|
-
auth: clientConfig,
|
|
56
|
-
code,
|
|
57
|
-
error,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function prependBasePath(basePath, pathname) {
|
|
62
|
-
if (!basePath) {
|
|
63
|
-
return pathname;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return pathname === '/' ? basePath : `${basePath}${pathname}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function appendHashParam(path, key, value, publicBaseUrl) {
|
|
70
|
-
const url = new URL(path, publicBaseUrl);
|
|
71
|
-
const hashParams = new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash);
|
|
72
|
-
hashParams.set(key, value);
|
|
73
|
-
url.hash = hashParams.toString();
|
|
74
|
-
return `${url.pathname}${url.search}${url.hash}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getDefaultReturnTo(basePath) {
|
|
78
|
-
return basePath ? `${basePath}/` : '/';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function sanitizeReturnTo(rawValue, { basePath = '', publicBaseUrl = '' } = {}) {
|
|
82
|
-
const fallback = getDefaultReturnTo(basePath);
|
|
83
|
-
const candidate = String(rawValue ?? '').trim();
|
|
84
|
-
if (!candidate) {
|
|
85
|
-
return fallback;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
const resolved = new URL(candidate, `${publicBaseUrl || 'http://localhost'}/`);
|
|
90
|
-
const publicOrigin = publicBaseUrl ? new URL(publicBaseUrl).origin : resolved.origin;
|
|
91
|
-
if (resolved.origin !== publicOrigin) {
|
|
92
|
-
return fallback;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (basePath) {
|
|
96
|
-
const withinBasePath = resolved.pathname === basePath || resolved.pathname.startsWith(`${basePath}/`);
|
|
97
|
-
if (!withinBasePath) {
|
|
98
|
-
return fallback;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
|
103
|
-
} catch {
|
|
104
|
-
return fallback;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isNonEmptyString(value) {
|
|
109
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function normalizeEmailAddress(value) {
|
|
113
|
-
return String(value ?? '').trim().toLowerCase();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getEmailDomain(email) {
|
|
117
|
-
const normalized = normalizeEmailAddress(email);
|
|
118
|
-
const atIndex = normalized.lastIndexOf('@');
|
|
119
|
-
if (atIndex <= 0 || atIndex === normalized.length - 1) {
|
|
120
|
-
return '';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return normalized.slice(atIndex + 1);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isOidcUserAllowed(oidcConfig, email) {
|
|
127
|
-
const normalizedEmail = normalizeEmailAddress(email);
|
|
128
|
-
const emailDomain = getEmailDomain(normalizedEmail);
|
|
129
|
-
const allowedEmails = Array.isArray(oidcConfig?.allowedEmails) ? oidcConfig.allowedEmails : [];
|
|
130
|
-
const allowedDomains = Array.isArray(oidcConfig?.allowedDomains) ? oidcConfig.allowedDomains : [];
|
|
131
|
-
|
|
132
|
-
if (allowedEmails.length === 0 && allowedDomains.length === 0) {
|
|
133
|
-
return { allowed: true };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (allowedEmails.includes(normalizedEmail)) {
|
|
137
|
-
return { allowed: true, reason: 'email' };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (allowedDomains.includes(emailDomain)) {
|
|
141
|
-
return { allowed: true, reason: 'domain' };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
allowed: false,
|
|
146
|
-
error: allowedEmails.length > 0 && allowedDomains.length > 0
|
|
147
|
-
? 'This Google account is not in the allowed email or domain list.'
|
|
148
|
-
: allowedEmails.length > 0
|
|
149
|
-
? 'This Google account is not in the allowed email list.'
|
|
150
|
-
: 'This Google account is not in an allowed domain.',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function readAuthenticatedOidcSession(sessionCookieManager, req) {
|
|
155
|
-
const session = sessionCookieManager.readSession(req);
|
|
156
|
-
if (session?.strategy !== AUTH_STRATEGY_OIDC) {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const user = session.user;
|
|
161
|
-
if (
|
|
162
|
-
!user
|
|
163
|
-
|| typeof user !== 'object'
|
|
164
|
-
|| !isNonEmptyString(user.sub)
|
|
165
|
-
|| !isNonEmptyString(user.email)
|
|
166
|
-
|| !isNonEmptyString(user.name)
|
|
167
|
-
) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (hasExpiredSession(session)) {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
...session,
|
|
177
|
-
user: {
|
|
178
|
-
email: user.email,
|
|
179
|
-
emailVerified: user.emailVerified === true,
|
|
180
|
-
name: user.name,
|
|
181
|
-
picture: typeof user.picture === 'string' ? user.picture : '',
|
|
182
|
-
sub: user.sub,
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function hasExpiredSession(session) {
|
|
188
|
-
const expiresAt = Number(session?.expiresAt);
|
|
189
|
-
return Number.isFinite(expiresAt) && expiresAt <= Date.now();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function resolveSessionExpiresAt(sessionMaxAgeMs, fallbackExpiresAt = null) {
|
|
193
|
-
if (Number.isFinite(sessionMaxAgeMs) && sessionMaxAgeMs > 0) {
|
|
194
|
-
return Date.now() + sessionMaxAgeMs;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return Number.isFinite(fallbackExpiresAt) ? fallbackExpiresAt : null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function createSessionCookieOptions(expiresAt) {
|
|
201
|
-
if (!Number.isFinite(expiresAt)) {
|
|
202
|
-
return {};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
expires: new Date(expiresAt),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function buildClientConfig(authConfig, { basePath = '' } = {}) {
|
|
211
|
-
const loginEndpoint = prependBasePath(basePath, '/api/auth/oidc/login');
|
|
212
|
-
const sessionEndpoint = prependBasePath(basePath, '/api/auth/session');
|
|
213
|
-
const statusEndpoint = prependBasePath(basePath, '/api/auth/status');
|
|
214
|
-
|
|
215
|
-
if (authConfig.strategy === AUTH_STRATEGY_NONE) {
|
|
216
|
-
return {
|
|
217
|
-
enabled: false,
|
|
218
|
-
implemented: true,
|
|
219
|
-
requiresLogin: false,
|
|
220
|
-
sessionEndpoint,
|
|
221
|
-
statusEndpoint,
|
|
222
|
-
strategy: AUTH_STRATEGY_NONE,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (authConfig.strategy === AUTH_STRATEGY_PASSWORD) {
|
|
227
|
-
return {
|
|
228
|
-
enabled: true,
|
|
229
|
-
implemented: true,
|
|
230
|
-
passwordLabel: 'Host password',
|
|
231
|
-
requiresLogin: true,
|
|
232
|
-
sessionEndpoint,
|
|
233
|
-
statusEndpoint,
|
|
234
|
-
strategy: AUTH_STRATEGY_PASSWORD,
|
|
235
|
-
submitLabel: 'Join session',
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
enabled: true,
|
|
241
|
-
implemented: true,
|
|
242
|
-
loginEndpoint,
|
|
243
|
-
provider: OIDC_PROVIDER_GOOGLE,
|
|
244
|
-
requiresLogin: true,
|
|
245
|
-
sessionEndpoint,
|
|
246
|
-
statusEndpoint,
|
|
247
|
-
strategy: AUTH_STRATEGY_OIDC,
|
|
248
|
-
submitLabel: 'Continue with Google',
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig) {
|
|
253
|
-
const expectedPasswordHash = hashPassword(authConfig.password);
|
|
254
|
-
|
|
255
|
-
function hasValidSession(req) {
|
|
256
|
-
const session = sessionCookieManager.readSession(req);
|
|
257
|
-
return session?.strategy === AUTH_STRATEGY_PASSWORD && !hasExpiredSession(session);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
clearSession(req) {
|
|
262
|
-
return createResponse(200, { ok: true }, {
|
|
263
|
-
kind: 'logout_ok',
|
|
264
|
-
setCookie: sessionCookieManager.clearSession(req),
|
|
265
|
-
});
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
createSession(req, body = {}) {
|
|
269
|
-
if (typeof body.password !== 'string' || !body.password) {
|
|
270
|
-
return createResponse(400, { error: 'Missing password' }, { kind: 'invalid_request' });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const submittedPasswordHash = hashPassword(body.password);
|
|
274
|
-
const isValidPassword = submittedPasswordHash.length === expectedPasswordHash.length
|
|
275
|
-
&& timingSafeEqual(submittedPasswordHash, expectedPasswordHash);
|
|
276
|
-
|
|
277
|
-
if (!isValidPassword) {
|
|
278
|
-
return createResponse(401, {
|
|
279
|
-
auth: clientConfig,
|
|
280
|
-
code: 'AUTH_INVALID_CREDENTIALS',
|
|
281
|
-
error: 'Incorrect password',
|
|
282
|
-
}, { kind: 'invalid_credentials' });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const expiresAt = resolveSessionExpiresAt(authConfig.sessionMaxAgeMs);
|
|
286
|
-
|
|
287
|
-
return createResponse(200, {
|
|
288
|
-
auth: clientConfig,
|
|
289
|
-
authenticated: true,
|
|
290
|
-
ok: true,
|
|
291
|
-
user: null,
|
|
292
|
-
}, {
|
|
293
|
-
kind: 'session_created',
|
|
294
|
-
setCookie: sessionCookieManager.createSessionCookie(req, {
|
|
295
|
-
authenticatedAt: Date.now(),
|
|
296
|
-
...(Number.isFinite(expiresAt) ? { expiresAt } : {}),
|
|
297
|
-
strategy: AUTH_STRATEGY_PASSWORD,
|
|
298
|
-
}, createSessionCookieOptions(expiresAt)),
|
|
299
|
-
});
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
getAuthenticatedUser() {
|
|
303
|
-
return null;
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
getStatus(req) {
|
|
307
|
-
return createResponse(200, {
|
|
308
|
-
authenticated: hasValidSession(req),
|
|
309
|
-
auth: clientConfig,
|
|
310
|
-
user: null,
|
|
311
|
-
}, { kind: 'status' });
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
isAuthenticated(req) {
|
|
315
|
-
return hasValidSession(req);
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function createNoneStrategy(clientConfig) {
|
|
321
|
-
return {
|
|
322
|
-
clearSession() {
|
|
323
|
-
return createResponse(200, { ok: true, user: null }, { kind: 'logout_ok' });
|
|
324
|
-
},
|
|
325
|
-
|
|
326
|
-
createSession() {
|
|
327
|
-
return createResponse(405, { error: 'Authentication is disabled' }, { kind: 'disabled' });
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
getAuthenticatedUser() {
|
|
331
|
-
return null;
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
getStatus() {
|
|
335
|
-
return createResponse(200, {
|
|
336
|
-
authenticated: true,
|
|
337
|
-
auth: clientConfig,
|
|
338
|
-
user: null,
|
|
339
|
-
}, { kind: 'status' });
|
|
340
|
-
},
|
|
341
|
-
|
|
342
|
-
isAuthenticated() {
|
|
343
|
-
return true;
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function createOidcStrategy(authConfig, sessionCookieManager, flowCookieManager, clientConfig, { basePath = '' } = {}) {
|
|
349
|
-
let oidcConfigPromise = null;
|
|
350
|
-
|
|
351
|
-
function clearOidcCookies(req, includeSession = true) {
|
|
352
|
-
const cookies = [flowCookieManager.clear(req)];
|
|
353
|
-
if (includeSession) {
|
|
354
|
-
cookies.unshift(sessionCookieManager.clearSession(req));
|
|
355
|
-
}
|
|
356
|
-
return cookies;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function createErrorRedirect(req, message, flowPayload = null) {
|
|
360
|
-
const returnTo = sanitizeReturnTo(flowPayload?.returnTo, {
|
|
361
|
-
basePath,
|
|
362
|
-
publicBaseUrl: authConfig.oidc.publicBaseUrl,
|
|
363
|
-
});
|
|
364
|
-
return createResponse(302, null, {
|
|
365
|
-
kind: 'redirect',
|
|
366
|
-
redirectTo: appendHashParam(returnTo, 'auth_error', message, authConfig.oidc.publicBaseUrl),
|
|
367
|
-
setCookie: clearOidcCookies(req),
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function getOidcConfiguration() {
|
|
372
|
-
if (!oidcConfigPromise) {
|
|
373
|
-
const allowInsecureRequests = new URL(authConfig.oidc.issuer).protocol !== 'https:';
|
|
374
|
-
oidcConfigPromise = oidc.discovery(
|
|
375
|
-
new URL(authConfig.oidc.issuer),
|
|
376
|
-
authConfig.oidc.clientId,
|
|
377
|
-
authConfig.oidc.clientSecret,
|
|
378
|
-
undefined,
|
|
379
|
-
allowInsecureRequests
|
|
380
|
-
? { execute: [oidc.allowInsecureRequests] }
|
|
381
|
-
: undefined,
|
|
382
|
-
).catch((error) => {
|
|
383
|
-
oidcConfigPromise = null;
|
|
384
|
-
throw error;
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return oidcConfigPromise;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function getAuthenticatedSession(req) {
|
|
392
|
-
return readAuthenticatedOidcSession(sessionCookieManager, req);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function getAuthenticatedUser(req) {
|
|
396
|
-
return getAuthenticatedSession(req)?.user ?? null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
async beginLogin(req, requestUrl) {
|
|
401
|
-
const config = await getOidcConfiguration();
|
|
402
|
-
const returnTo = sanitizeReturnTo(requestUrl.searchParams.get('returnTo'), {
|
|
403
|
-
basePath,
|
|
404
|
-
publicBaseUrl: authConfig.oidc.publicBaseUrl,
|
|
405
|
-
});
|
|
406
|
-
const codeVerifier = oidc.randomPKCECodeVerifier();
|
|
407
|
-
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
|
|
408
|
-
const nonce = oidc.randomNonce();
|
|
409
|
-
const state = oidc.randomState();
|
|
410
|
-
const redirectTo = oidc.buildAuthorizationUrl(config, {
|
|
411
|
-
code_challenge: codeChallenge,
|
|
412
|
-
code_challenge_method: 'S256',
|
|
413
|
-
...(authConfig.oidc.allowedDomains.length === 1 ? { hd: authConfig.oidc.allowedDomains[0] } : {}),
|
|
414
|
-
nonce,
|
|
415
|
-
redirect_uri: authConfig.oidc.callbackUrl,
|
|
416
|
-
response_type: 'code',
|
|
417
|
-
scope: 'openid email profile',
|
|
418
|
-
state,
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
return createResponse(302, null, {
|
|
422
|
-
kind: 'redirect',
|
|
423
|
-
redirectTo: redirectTo.href,
|
|
424
|
-
setCookie: flowCookieManager.create(req, {
|
|
425
|
-
createdAt: Date.now(),
|
|
426
|
-
nonce,
|
|
427
|
-
pkceCodeVerifier: codeVerifier,
|
|
428
|
-
returnTo,
|
|
429
|
-
state,
|
|
430
|
-
}, {
|
|
431
|
-
expires: new Date(Date.now() + OIDC_FLOW_TTL_MS),
|
|
432
|
-
}),
|
|
433
|
-
});
|
|
434
|
-
},
|
|
435
|
-
|
|
436
|
-
async completeLogin(req, requestUrl) {
|
|
437
|
-
const flowPayload = flowCookieManager.read(req);
|
|
438
|
-
if (!flowPayload) {
|
|
439
|
-
return createErrorRedirect(req, 'Authentication session expired. Try again.');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (requestUrl.searchParams.get('error')) {
|
|
443
|
-
const errorMessage = requestUrl.searchParams.get('error_description')
|
|
444
|
-
|| requestUrl.searchParams.get('error')
|
|
445
|
-
|| 'Authentication failed';
|
|
446
|
-
return createErrorRedirect(req, errorMessage, flowPayload);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
!isNonEmptyString(flowPayload.state)
|
|
451
|
-
|| !isNonEmptyString(flowPayload.nonce)
|
|
452
|
-
|| !isNonEmptyString(flowPayload.pkceCodeVerifier)
|
|
453
|
-
) {
|
|
454
|
-
return createErrorRedirect(req, 'Authentication session expired. Try again.', flowPayload);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
try {
|
|
458
|
-
const config = await getOidcConfiguration();
|
|
459
|
-
const callbackUrl = new URL(authConfig.oidc.callbackUrl);
|
|
460
|
-
callbackUrl.search = requestUrl.search;
|
|
461
|
-
const tokens = await oidc.authorizationCodeGrant(config, callbackUrl, {
|
|
462
|
-
expectedNonce: flowPayload.nonce,
|
|
463
|
-
expectedState: flowPayload.state,
|
|
464
|
-
idTokenExpected: true,
|
|
465
|
-
pkceCodeVerifier: flowPayload.pkceCodeVerifier,
|
|
466
|
-
});
|
|
467
|
-
const claims = tokens.claims();
|
|
468
|
-
|
|
469
|
-
if (
|
|
470
|
-
!claims
|
|
471
|
-
|| !isNonEmptyString(claims.sub)
|
|
472
|
-
|| !isNonEmptyString(claims.email)
|
|
473
|
-
|| claims.email_verified !== true
|
|
474
|
-
|| !isNonEmptyString(claims.name)
|
|
475
|
-
) {
|
|
476
|
-
return createErrorRedirect(req, 'Google account is missing a verified email or name.', flowPayload);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const accessDecision = isOidcUserAllowed(authConfig.oidc, claims.email);
|
|
480
|
-
if (!accessDecision.allowed) {
|
|
481
|
-
return createErrorRedirect(req, accessDecision.error, flowPayload);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const tokenExpiresAt = Number.isFinite(claims.exp)
|
|
485
|
-
? claims.exp * 1000
|
|
486
|
-
: Date.now() + (Math.max(Number(tokens.expires_in) || 3600, 1) * 1000);
|
|
487
|
-
const expiresAt = resolveSessionExpiresAt(authConfig.sessionMaxAgeMs, tokenExpiresAt);
|
|
488
|
-
const returnTo = sanitizeReturnTo(flowPayload.returnTo, {
|
|
489
|
-
basePath,
|
|
490
|
-
publicBaseUrl: authConfig.oidc.publicBaseUrl,
|
|
491
|
-
});
|
|
492
|
-
const user = {
|
|
493
|
-
email: claims.email,
|
|
494
|
-
emailVerified: true,
|
|
495
|
-
name: claims.name,
|
|
496
|
-
picture: typeof claims.picture === 'string' ? claims.picture : '',
|
|
497
|
-
sub: claims.sub,
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
return createResponse(302, null, {
|
|
501
|
-
kind: 'redirect',
|
|
502
|
-
redirectTo: returnTo,
|
|
503
|
-
setCookie: [
|
|
504
|
-
sessionCookieManager.createSessionCookie(req, {
|
|
505
|
-
authenticatedAt: Date.now(),
|
|
506
|
-
expiresAt,
|
|
507
|
-
strategy: AUTH_STRATEGY_OIDC,
|
|
508
|
-
user,
|
|
509
|
-
}, createSessionCookieOptions(expiresAt)),
|
|
510
|
-
flowCookieManager.clear(req),
|
|
511
|
-
],
|
|
512
|
-
});
|
|
513
|
-
} catch (error) {
|
|
514
|
-
console.error('[auth] OIDC callback failed:', error.message);
|
|
515
|
-
return createErrorRedirect(req, 'Google sign-in failed. Try again.', flowPayload);
|
|
516
|
-
}
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
clearSession(req) {
|
|
520
|
-
return createResponse(200, { ok: true, user: null }, {
|
|
521
|
-
kind: 'logout_ok',
|
|
522
|
-
setCookie: clearOidcCookies(req),
|
|
523
|
-
});
|
|
524
|
-
},
|
|
525
|
-
|
|
526
|
-
createSession() {
|
|
527
|
-
return createResponse(405, {
|
|
528
|
-
auth: clientConfig,
|
|
529
|
-
code: 'AUTH_LOGIN_REDIRECT_REQUIRED',
|
|
530
|
-
error: 'Use the OIDC login endpoint to authenticate.',
|
|
531
|
-
}, { kind: 'invalid_request' });
|
|
532
|
-
},
|
|
533
|
-
|
|
534
|
-
getAuthenticatedUser,
|
|
535
|
-
|
|
536
|
-
getStatus(req) {
|
|
537
|
-
const user = getAuthenticatedUser(req);
|
|
538
|
-
return createResponse(200, {
|
|
539
|
-
authenticated: Boolean(user),
|
|
540
|
-
auth: clientConfig,
|
|
541
|
-
user,
|
|
542
|
-
}, { kind: 'status' });
|
|
543
|
-
},
|
|
544
|
-
|
|
545
|
-
isAuthenticated(req) {
|
|
546
|
-
return Boolean(getAuthenticatedSession(req));
|
|
547
|
-
},
|
|
548
|
-
};
|
|
549
|
-
}
|
|
24
|
+
AUTH_STRATEGY_PASSWORD,
|
|
25
|
+
SUPPORTED_AUTH_STRATEGIES,
|
|
26
|
+
createRandomAuthPassword,
|
|
27
|
+
createRandomSessionSecret,
|
|
28
|
+
};
|
|
550
29
|
|
|
551
30
|
export function createAuthService(config) {
|
|
552
31
|
const authConfig = config.auth ?? { strategy: AUTH_STRATEGY_NONE };
|