collabmd 0.1.31 → 0.1.33

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.
Files changed (99) hide show
  1. package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
  2. package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
  3. package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
  4. package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
  5. package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
  6. package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
  7. package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
  8. package/dist/client/assets/index-BIt5EZXz.css +1 -0
  9. package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
  10. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  18. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  19. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  20. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  21. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  22. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  23. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  24. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  25. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  26. package/dist/client/assets/main-DeowJOSp.js +1368 -0
  27. package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
  28. package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
  29. package/dist/client/drawio-editor.html +1 -1
  30. package/dist/client/excalidraw-editor.html +1 -1
  31. package/dist/client/export-document.html +2 -2
  32. package/dist/client/index.html +2 -2
  33. package/package.json +8 -5
  34. package/src/client/app/main-entry.js +2 -0
  35. package/src/client/application/app-shell/git-feature.js +28 -36
  36. package/src/client/application/app-shell/presence-feature.js +3 -1
  37. package/src/client/application/diagram-preview-hydrator.js +24 -15
  38. package/src/client/application/mermaid-preview-hydrator.js +121 -4
  39. package/src/client/application/plantuml-preview-hydrator.js +6 -16
  40. package/src/client/application/preview-renderer.js +11 -2
  41. package/src/client/bootstrap/collabmd-app-shell.js +182 -50
  42. package/src/client/domain/excalidraw-document-switch.js +3 -0
  43. package/src/client/excalidraw-editor.js +187 -84
  44. package/src/client/infrastructure/auth-client.js +4 -4
  45. package/src/client/infrastructure/backlinks-api-client.js +18 -0
  46. package/src/client/infrastructure/comment-thread-store.js +4 -0
  47. package/src/client/infrastructure/editor-session.js +6 -0
  48. package/src/client/infrastructure/editor-view-adapter.js +23 -0
  49. package/src/client/infrastructure/git-api-client.js +134 -0
  50. package/src/client/infrastructure/plantuml-api-client.js +21 -0
  51. package/src/client/infrastructure/vault-api-client.js +115 -0
  52. package/src/client/presentation/backlinks-panel.js +6 -9
  53. package/src/client/presentation/bases-preview-controller.js +1413 -34
  54. package/src/client/presentation/comments-panel.js +50 -5
  55. package/src/client/presentation/excalidraw-embed-controller.js +251 -15
  56. package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
  57. package/src/client/presentation/file-action-controller.js +97 -0
  58. package/src/client/presentation/file-explorer-controller.js +2 -0
  59. package/src/client/presentation/file-explorer-view.js +368 -0
  60. package/src/client/presentation/file-history-view-controller.js +41 -40
  61. package/src/client/presentation/git-diff-view-controller.js +70 -53
  62. package/src/client/presentation/git-panel-controller.js +12 -30
  63. package/src/client/styles/components/scrollbars.css +5 -0
  64. package/src/client/styles/export-document.css +45 -2
  65. package/src/client/styles/features/auth-gate.css +13 -24
  66. package/src/client/styles/features/comment-card.css +6 -0
  67. package/src/client/styles/features/comments-drawer.css +38 -18
  68. package/src/client/styles/features/diagram-preview.css +33 -0
  69. package/src/client/styles/features/embedded-preview.css +37 -11
  70. package/src/client/styles/features/preview-markdown.css +559 -39
  71. package/src/client/styles/foundation/themes.css +4 -0
  72. package/src/client/styles/foundation/tokens.css +4 -3
  73. package/src/client/styles/layout/sidebar.css +72 -0
  74. package/src/server/auth/auth-common.js +224 -0
  75. package/src/server/auth/auth-constants.js +30 -0
  76. package/src/server/auth/auth-strategies.js +320 -0
  77. package/src/server/auth/create-auth-service.js +24 -545
  78. package/src/server/domain/backlink-index.js +177 -5
  79. package/src/server/domain/bases/base-definition.js +382 -0
  80. package/src/server/domain/bases/base-expression-runtime.js +1196 -0
  81. package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
  82. package/src/server/domain/bases/base-query-metadata.js +242 -0
  83. package/src/server/domain/bases/base-query-results.js +215 -0
  84. package/src/server/domain/bases/base-query-service.js +406 -1992
  85. package/src/server/domain/bases/base-transform.js +116 -0
  86. package/src/server/domain/collaboration/collaboration-room.js +36 -1
  87. package/src/server/domain/collaboration/room-client-state-store.js +1 -0
  88. package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
  89. package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
  90. package/src/server/infrastructure/http/http-response.js +56 -0
  91. package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
  92. package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
  93. package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
  94. package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
  95. package/dist/client/assets/index-D69HOZHr.css +0 -1
  96. package/dist/client/assets/main-DSxNTtD-.js +0 -1243
  97. package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
  98. package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
  99. /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
@@ -0,0 +1,320 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+
3
+ import * as oidc from 'openid-client';
4
+
5
+ import {
6
+ AUTH_STRATEGY_OIDC,
7
+ AUTH_STRATEGY_PASSWORD,
8
+ OIDC_FLOW_TTL_MS,
9
+ } from './auth-constants.js';
10
+ import {
11
+ appendHashParam,
12
+ createResponse,
13
+ createSessionCookieOptions,
14
+ hasExpiredSession,
15
+ hashPassword,
16
+ isNonEmptyString,
17
+ isOidcUserAllowed,
18
+ readAuthenticatedOidcSession,
19
+ resolveSessionExpiresAt,
20
+ sanitizeReturnTo,
21
+ } from './auth-common.js';
22
+
23
+ export function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig) {
24
+ const expectedPasswordHash = hashPassword(authConfig.password);
25
+
26
+ function hasValidSession(req) {
27
+ const session = sessionCookieManager.readSession(req);
28
+ return session?.strategy === AUTH_STRATEGY_PASSWORD && !hasExpiredSession(session);
29
+ }
30
+
31
+ return {
32
+ clearSession(req) {
33
+ return createResponse(200, { ok: true }, {
34
+ kind: 'logout_ok',
35
+ setCookie: sessionCookieManager.clearSession(req),
36
+ });
37
+ },
38
+
39
+ createSession(req, body = {}) {
40
+ if (typeof body.password !== 'string' || !body.password) {
41
+ return createResponse(400, { error: 'Missing password' }, { kind: 'invalid_request' });
42
+ }
43
+
44
+ const submittedPasswordHash = hashPassword(body.password);
45
+ const isValidPassword = submittedPasswordHash.length === expectedPasswordHash.length
46
+ && timingSafeEqual(submittedPasswordHash, expectedPasswordHash);
47
+
48
+ if (!isValidPassword) {
49
+ return createResponse(401, {
50
+ auth: clientConfig,
51
+ code: 'AUTH_INVALID_CREDENTIALS',
52
+ error: 'Incorrect password',
53
+ }, { kind: 'invalid_credentials' });
54
+ }
55
+
56
+ const expiresAt = resolveSessionExpiresAt(authConfig.sessionMaxAgeMs);
57
+
58
+ return createResponse(200, {
59
+ auth: clientConfig,
60
+ authenticated: true,
61
+ ok: true,
62
+ user: null,
63
+ }, {
64
+ kind: 'session_created',
65
+ setCookie: sessionCookieManager.createSessionCookie(req, {
66
+ authenticatedAt: Date.now(),
67
+ ...(Number.isFinite(expiresAt) ? { expiresAt } : {}),
68
+ strategy: AUTH_STRATEGY_PASSWORD,
69
+ }, createSessionCookieOptions(expiresAt)),
70
+ });
71
+ },
72
+
73
+ getAuthenticatedUser() {
74
+ return null;
75
+ },
76
+
77
+ getStatus(req) {
78
+ return createResponse(200, {
79
+ authenticated: hasValidSession(req),
80
+ auth: clientConfig,
81
+ user: null,
82
+ }, { kind: 'status' });
83
+ },
84
+
85
+ isAuthenticated(req) {
86
+ return hasValidSession(req);
87
+ },
88
+ };
89
+ }
90
+
91
+ export function createNoneStrategy(clientConfig) {
92
+ return {
93
+ clearSession() {
94
+ return createResponse(200, { ok: true, user: null }, { kind: 'logout_ok' });
95
+ },
96
+
97
+ createSession() {
98
+ return createResponse(405, { error: 'Authentication is disabled' }, { kind: 'disabled' });
99
+ },
100
+
101
+ getAuthenticatedUser() {
102
+ return null;
103
+ },
104
+
105
+ getStatus() {
106
+ return createResponse(200, {
107
+ authenticated: true,
108
+ auth: clientConfig,
109
+ user: null,
110
+ }, { kind: 'status' });
111
+ },
112
+
113
+ isAuthenticated() {
114
+ return true;
115
+ },
116
+ };
117
+ }
118
+
119
+ export function createOidcStrategy(authConfig, sessionCookieManager, flowCookieManager, clientConfig, { basePath = '' } = {}) {
120
+ let oidcConfigPromise = null;
121
+
122
+ function clearOidcCookies(req, includeSession = true) {
123
+ const cookies = [flowCookieManager.clear(req)];
124
+ if (includeSession) {
125
+ cookies.unshift(sessionCookieManager.clearSession(req));
126
+ }
127
+ return cookies;
128
+ }
129
+
130
+ function createErrorRedirect(req, message, flowPayload = null) {
131
+ const returnTo = sanitizeReturnTo(flowPayload?.returnTo, {
132
+ basePath,
133
+ publicBaseUrl: authConfig.oidc.publicBaseUrl,
134
+ });
135
+ return createResponse(302, null, {
136
+ kind: 'redirect',
137
+ redirectTo: appendHashParam(returnTo, 'auth_error', message, authConfig.oidc.publicBaseUrl),
138
+ setCookie: clearOidcCookies(req),
139
+ });
140
+ }
141
+
142
+ async function getOidcConfiguration() {
143
+ if (!oidcConfigPromise) {
144
+ const allowInsecureRequests = new URL(authConfig.oidc.issuer).protocol !== 'https:';
145
+ oidcConfigPromise = oidc.discovery(
146
+ new URL(authConfig.oidc.issuer),
147
+ authConfig.oidc.clientId,
148
+ authConfig.oidc.clientSecret,
149
+ undefined,
150
+ allowInsecureRequests
151
+ ? { execute: [oidc.allowInsecureRequests] }
152
+ : undefined,
153
+ ).catch((error) => {
154
+ oidcConfigPromise = null;
155
+ throw error;
156
+ });
157
+ }
158
+
159
+ return oidcConfigPromise;
160
+ }
161
+
162
+ function getAuthenticatedSession(req) {
163
+ return readAuthenticatedOidcSession(sessionCookieManager, req);
164
+ }
165
+
166
+ function getAuthenticatedUser(req) {
167
+ return getAuthenticatedSession(req)?.user ?? null;
168
+ }
169
+
170
+ return {
171
+ async beginLogin(req, requestUrl) {
172
+ const config = await getOidcConfiguration();
173
+ const returnTo = sanitizeReturnTo(requestUrl.searchParams.get('returnTo'), {
174
+ basePath,
175
+ publicBaseUrl: authConfig.oidc.publicBaseUrl,
176
+ });
177
+ const codeVerifier = oidc.randomPKCECodeVerifier();
178
+ const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
179
+ const nonce = oidc.randomNonce();
180
+ const state = oidc.randomState();
181
+ const redirectTo = oidc.buildAuthorizationUrl(config, {
182
+ code_challenge: codeChallenge,
183
+ code_challenge_method: 'S256',
184
+ ...(authConfig.oidc.allowedDomains.length === 1 ? { hd: authConfig.oidc.allowedDomains[0] } : {}),
185
+ nonce,
186
+ redirect_uri: authConfig.oidc.callbackUrl,
187
+ response_type: 'code',
188
+ scope: 'openid email profile',
189
+ state,
190
+ });
191
+
192
+ return createResponse(302, null, {
193
+ kind: 'redirect',
194
+ redirectTo: redirectTo.href,
195
+ setCookie: flowCookieManager.create(req, {
196
+ createdAt: Date.now(),
197
+ nonce,
198
+ pkceCodeVerifier: codeVerifier,
199
+ returnTo,
200
+ state,
201
+ }, {
202
+ expires: new Date(Date.now() + OIDC_FLOW_TTL_MS),
203
+ }),
204
+ });
205
+ },
206
+
207
+ async completeLogin(req, requestUrl) {
208
+ const flowPayload = flowCookieManager.read(req);
209
+ if (!flowPayload) {
210
+ return createErrorRedirect(req, 'Authentication session expired. Try again.');
211
+ }
212
+
213
+ if (requestUrl.searchParams.get('error')) {
214
+ const errorMessage = requestUrl.searchParams.get('error_description')
215
+ || requestUrl.searchParams.get('error')
216
+ || 'Authentication failed';
217
+ return createErrorRedirect(req, errorMessage, flowPayload);
218
+ }
219
+
220
+ if (
221
+ !isNonEmptyString(flowPayload.state)
222
+ || !isNonEmptyString(flowPayload.nonce)
223
+ || !isNonEmptyString(flowPayload.pkceCodeVerifier)
224
+ ) {
225
+ return createErrorRedirect(req, 'Authentication session expired. Try again.', flowPayload);
226
+ }
227
+
228
+ try {
229
+ const config = await getOidcConfiguration();
230
+ const callbackUrl = new URL(authConfig.oidc.callbackUrl);
231
+ callbackUrl.search = requestUrl.search;
232
+ const tokens = await oidc.authorizationCodeGrant(config, callbackUrl, {
233
+ expectedNonce: flowPayload.nonce,
234
+ expectedState: flowPayload.state,
235
+ idTokenExpected: true,
236
+ pkceCodeVerifier: flowPayload.pkceCodeVerifier,
237
+ });
238
+ const claims = tokens.claims();
239
+
240
+ if (
241
+ !claims
242
+ || !isNonEmptyString(claims.sub)
243
+ || !isNonEmptyString(claims.email)
244
+ || claims.email_verified !== true
245
+ || !isNonEmptyString(claims.name)
246
+ ) {
247
+ return createErrorRedirect(req, 'Google account is missing a verified email or name.', flowPayload);
248
+ }
249
+
250
+ const accessDecision = isOidcUserAllowed(authConfig.oidc, claims.email);
251
+ if (!accessDecision.allowed) {
252
+ return createErrorRedirect(req, accessDecision.error, flowPayload);
253
+ }
254
+
255
+ const tokenExpiresAt = Number.isFinite(claims.exp)
256
+ ? claims.exp * 1000
257
+ : Date.now() + (Math.max(Number(tokens.expires_in) || 3600, 1) * 1000);
258
+ const expiresAt = resolveSessionExpiresAt(authConfig.sessionMaxAgeMs, tokenExpiresAt);
259
+ const returnTo = sanitizeReturnTo(flowPayload.returnTo, {
260
+ basePath,
261
+ publicBaseUrl: authConfig.oidc.publicBaseUrl,
262
+ });
263
+ const user = {
264
+ email: claims.email,
265
+ emailVerified: true,
266
+ name: claims.name,
267
+ picture: typeof claims.picture === 'string' ? claims.picture : '',
268
+ sub: claims.sub,
269
+ };
270
+
271
+ return createResponse(302, null, {
272
+ kind: 'redirect',
273
+ redirectTo: returnTo,
274
+ setCookie: [
275
+ sessionCookieManager.createSessionCookie(req, {
276
+ authenticatedAt: Date.now(),
277
+ expiresAt,
278
+ strategy: AUTH_STRATEGY_OIDC,
279
+ user,
280
+ }, createSessionCookieOptions(expiresAt)),
281
+ flowCookieManager.clear(req),
282
+ ],
283
+ });
284
+ } catch (error) {
285
+ console.error('[auth] OIDC callback failed:', error.message);
286
+ return createErrorRedirect(req, 'Google sign-in failed. Try again.', flowPayload);
287
+ }
288
+ },
289
+
290
+ clearSession(req) {
291
+ return createResponse(200, { ok: true, user: null }, {
292
+ kind: 'logout_ok',
293
+ setCookie: clearOidcCookies(req),
294
+ });
295
+ },
296
+
297
+ createSession() {
298
+ return createResponse(405, {
299
+ auth: clientConfig,
300
+ code: 'AUTH_LOGIN_REDIRECT_REQUIRED',
301
+ error: 'Use the OIDC login endpoint to authenticate.',
302
+ }, { kind: 'invalid_request' });
303
+ },
304
+
305
+ getAuthenticatedUser,
306
+
307
+ getStatus(req) {
308
+ const user = getAuthenticatedUser(req);
309
+ return createResponse(200, {
310
+ authenticated: Boolean(user),
311
+ auth: clientConfig,
312
+ user,
313
+ }, { kind: 'status' });
314
+ },
315
+
316
+ isAuthenticated(req) {
317
+ return Boolean(getAuthenticatedSession(req));
318
+ },
319
+ };
320
+ }