@cytario/web 2.1.3 → 2.1.5

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 (202) hide show
  1. package/README.md +20 -20
  2. package/app/.server/auth/README.md +8 -8
  3. package/app/.server/auth/authMiddleware.ts +9 -25
  4. package/app/.server/auth/exchangeAuthCode.ts +2 -6
  5. package/app/.server/auth/getS3Client.ts +3 -13
  6. package/app/.server/auth/getSessionCredentials.ts +6 -20
  7. package/app/.server/auth/getUserInfo.ts +2 -6
  8. package/app/.server/auth/keycloakAdmin/client.ts +2 -9
  9. package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
  10. package/app/.server/auth/keycloakAdmin/users.ts +7 -23
  11. package/app/.server/auth/oauthState.ts +4 -13
  12. package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
  13. package/app/.server/auth/refreshAuthTokens.ts +5 -19
  14. package/app/.server/auth/sessionMiddleware.ts +1 -4
  15. package/app/.server/auth/sessionStorage.ts +1 -4
  16. package/app/.server/auth/verifyIdToken.ts +1 -3
  17. package/app/.server/auth/wellKnownEndpoints.ts +1 -4
  18. package/app/.server/db/redis.ts +5 -1
  19. package/app/.server/logging.ts +1 -4
  20. package/app/.server/requestDurationMiddleware.ts +1 -4
  21. package/app/components/.client/ImageViewer/README.md +5 -5
  22. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
  23. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
  24. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
  25. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
  26. package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
  27. package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
  28. package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
  29. package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
  30. package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
  31. package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
  32. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
  33. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
  34. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
  35. package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
  36. package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
  37. package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
  38. package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
  39. package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
  40. package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
  41. package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
  42. package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
  43. package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
  44. package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
  45. package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
  46. package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
  47. package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
  48. package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
  49. package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
  50. package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
  51. package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
  52. package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
  53. package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
  54. package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
  55. package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
  56. package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
  57. package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
  58. package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
  59. package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
  60. package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
  61. package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
  62. package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
  63. package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
  64. package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
  65. package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
  66. package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
  67. package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
  68. package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
  69. package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
  70. package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
  71. package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
  72. package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
  73. package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
  74. package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
  75. package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
  76. package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
  77. package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
  78. package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
  79. package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
  80. package/app/components/AppHeader.tsx +1 -4
  81. package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
  82. package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
  83. package/app/components/ClientOnly.tsx +1 -1
  84. package/app/components/Container.tsx +3 -15
  85. package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
  86. package/app/components/DataGrid/DataGrid.tsx +7 -27
  87. package/app/components/DataGrid/WktSvg.tsx +2 -4
  88. package/app/components/DataGrid/getParquetSchema.ts +1 -4
  89. package/app/components/DescriptionList.tsx +1 -3
  90. package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
  91. package/app/components/DirectoryView/DirectoryView.tsx +10 -46
  92. package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
  93. package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
  94. package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
  95. package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
  96. package/app/components/DirectoryView/FilterBar.tsx +9 -48
  97. package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
  98. package/app/components/DirectoryView/filterNodes.ts +4 -11
  99. package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
  100. package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
  101. package/app/components/DirectoryView/useLayoutStore.ts +5 -25
  102. package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
  103. package/app/components/GlobalSearch/SearchBar.tsx +1 -7
  104. package/app/components/GlobalSearch/Suggestions.tsx +0 -1
  105. package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
  106. package/app/components/LavaLoader.tsx +4 -11
  107. package/app/components/Layout/Footer.tsx +1 -4
  108. package/app/components/Pills/ScopePill.tsx +2 -8
  109. package/app/components/Table/ColumnFilterInput.tsx +5 -20
  110. package/app/components/Table/ColumnResizeHandle.tsx +1 -5
  111. package/app/components/Table/ColumnSortButton.tsx +1 -5
  112. package/app/components/Table/SelectionFooter.tsx +3 -5
  113. package/app/components/Table/Table.tsx +5 -21
  114. package/app/components/Table/TableBodyRow.tsx +19 -31
  115. package/app/components/Table/TableHeaderRow.tsx +7 -28
  116. package/app/components/Table/TableMenu.tsx +4 -20
  117. package/app/components/Table/state/createTableStore.ts +3 -9
  118. package/app/components/Table/state/useTableStore.ts +1 -3
  119. package/app/components/Table/types.ts +4 -10
  120. package/app/components/Table/useColumnFilters.ts +1 -4
  121. package/app/components/Table/useColumnVisibility.ts +4 -11
  122. package/app/components/Table/useColumnWidths.ts +1 -3
  123. package/app/components/Table/useTableSorting.ts +4 -12
  124. package/app/components/Tooltip/Tooltip.tsx +4 -17
  125. package/app/components/Tooltip/TooltipSpan.tsx +5 -28
  126. package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
  127. package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
  128. package/app/components/Tooltip/useOverflowDetection.ts +2 -5
  129. package/app/components/UserMenu.tsx +2 -9
  130. package/app/entry.server.tsx +9 -19
  131. package/app/hooks/useSearchParam.ts +2 -4
  132. package/app/lib/bootstrapPluginsCore.ts +4 -9
  133. package/app/root.tsx +4 -15
  134. package/app/routes/admin/assertAdminScope.ts +1 -3
  135. package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
  136. package/app/routes/admin/assertGroupsInScope.ts +3 -11
  137. package/app/routes/admin/assertUsersInScope.ts +2 -8
  138. package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
  139. package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
  140. package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
  141. package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
  142. package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
  143. package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
  144. package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
  145. package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
  146. package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
  147. package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
  148. package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
  149. package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
  150. package/app/routes/admin/users/BulkActions.tsx +15 -38
  151. package/app/routes/admin/users/bulkUsers.action.ts +2 -9
  152. package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
  153. package/app/routes/admin/users/users.route.tsx +14 -63
  154. package/app/routes/api/cyberduck-profile.$name.ts +6 -2
  155. package/app/routes/auth/callback.route.tsx +8 -33
  156. package/app/routes/auth/login.route.tsx +8 -11
  157. package/app/routes/auth/logout.route.tsx +4 -14
  158. package/app/routes/config.route.tsx +1 -5
  159. package/app/routes/connections/connection.form.tsx +5 -14
  160. package/app/routes/connections/connection.schema.ts +4 -16
  161. package/app/routes/connections/connections.loader.ts +11 -23
  162. package/app/routes/connections/connections.route.tsx +1 -5
  163. package/app/routes/connections/connections.server.ts +1 -3
  164. package/app/routes/connections/createConnection.action.ts +2 -8
  165. package/app/routes/connections/createConnection.modal.tsx +3 -13
  166. package/app/routes/connections/deleteConnection.action.ts +1 -4
  167. package/app/routes/connections/updateConnection.action.ts +2 -8
  168. package/app/routes/connections/updateConnection.modal.tsx +4 -14
  169. package/app/routes/home/home.route.tsx +8 -33
  170. package/app/routes/layouts/ModalOutlet.tsx +6 -18
  171. package/app/routes/objects/objects.loader.ts +5 -19
  172. package/app/routes/objects/objects.route.tsx +11 -30
  173. package/app/routes/presign.route.tsx +5 -18
  174. package/app/routes/recent.route.tsx +1 -4
  175. package/app/routes/search.route.tsx +4 -17
  176. package/app/routes.ts +1 -4
  177. package/app/tailwind.css +17 -12
  178. package/app/types/cornerstone-codecs.d.ts +25 -29
  179. package/app/utils/connectionsStore/selectors.ts +2 -6
  180. package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
  181. package/app/utils/db/convertCsvToParquet.ts +1 -3
  182. package/app/utils/db/createDatabase.ts +1 -3
  183. package/app/utils/db/createSingleton.ts +1 -1
  184. package/app/utils/db/getBlobFromObjectNode.ts +3 -9
  185. package/app/utils/db/getGeomQuery.ts +1 -1
  186. package/app/utils/db/getMarkerInfoWasm.ts +1 -3
  187. package/app/utils/db/getTileBoundingBox.ts +1 -4
  188. package/app/utils/db/sqlQueries.ts +1 -4
  189. package/app/utils/fileType.ts +80 -10
  190. package/app/utils/filterObjects.ts +2 -5
  191. package/app/utils/localFilesStore/useFileStore.ts +7 -7
  192. package/app/utils/recentlyViewed.server.ts +1 -4
  193. package/app/utils/resourceId.ts +3 -11
  194. package/app/utils/s3Provider.ts +3 -7
  195. package/app/utils/signedFetch.ts +4 -13
  196. package/bin-src/codegen.ts +4 -1
  197. package/package.json +5 -1
  198. package/prisma/seed.ts +1 -2
  199. package/public/favicon/site.webmanifest +1 -1
  200. package/server.js +1 -4
  201. package/server.js.map +1 -1
  202. package/vite-plugins/cytario-plugins.ts +2 -8
package/README.md CHANGED
@@ -15,16 +15,16 @@ For the hosted product, see [cytario.com](https://www.cytario.com).
15
15
 
16
16
  ## Architecture
17
17
 
18
- | Layer | Technology |
19
- |-------|------------|
20
- | Framework | React Router v7 (SSR), React 19, Vite 6 |
21
- | Language | TypeScript (strict mode) |
22
- | Visualization | deck.gl, Viv, DuckDB-WASM, Apache Arrow |
23
- | Styling | Tailwind CSS, [@cytario/design](https://github.com/cytario/cytario-design) |
24
- | Auth | OAuth 2.0 via Keycloak, STS for S3 credentials |
25
- | Database | PostgreSQL (Prisma ORM), Redis/Valkey (sessions) |
26
- | Cloud | AWS SDK v3 (S3, STS), presigned URLs |
27
- | CI/CD | GitHub Actions, semantic-release, GHCR |
18
+ | Layer | Technology |
19
+ | ------------- | -------------------------------------------------------------------------- |
20
+ | Framework | React Router v7 (SSR), React 19, Vite 6 |
21
+ | Language | TypeScript (strict mode) |
22
+ | Visualization | deck.gl, Viv, DuckDB-WASM, Apache Arrow |
23
+ | Styling | Tailwind CSS, [@cytario/design](https://github.com/cytario/cytario-design) |
24
+ | Auth | OAuth 2.0 via Keycloak, STS for S3 credentials |
25
+ | Database | PostgreSQL (Prisma ORM), Redis/Valkey (sessions) |
26
+ | Cloud | AWS SDK v3 (S3, STS), presigned URLs |
27
+ | CI/CD | GitHub Actions, semantic-release, GHCR |
28
28
 
29
29
  ## Plugin model
30
30
 
@@ -133,11 +133,11 @@ package's default export must satisfy the
133
133
 
134
134
  ### CLI
135
135
 
136
- | Command | Behaviour |
137
- |---|---|
136
+ | Command | Behaviour |
137
+ | ------------------- | -------------------------------------------------------------------------------------------------------- |
138
138
  | `cytario-web build` | Codegen (Vite plugin reads `CYTARIO_PLUGINS`) + `react-router build` against the installed package root. |
139
- | `cytario-web dev` | Codegen + `react-router dev`. Extra args (`--port`, `--host`, …) are forwarded. |
140
- | `cytario-web start` | `NODE_ENV=production node server.ts` against the bundled `build/server/index.js`. |
139
+ | `cytario-web dev` | Codegen + `react-router dev`. Extra args (`--port`, `--host`, …) are forwarded. |
140
+ | `cytario-web start` | `NODE_ENV=production node server.ts` against the bundled `build/server/index.js`. |
141
141
 
142
142
  All subcommands operate against `@cytario/web`'s own install directory;
143
143
  the consumer never needs to know the on-disk layout.
@@ -213,12 +213,12 @@ cd devenv
213
213
  podman kube play local-deployment.yaml
214
214
  ```
215
215
 
216
- | Service | Port | Description |
217
- |---------|------|-------------|
218
- | Keycloak | 8080 | Identity provider (admin/admin) |
219
- | MinIO | 9000, 9001 | S3-compatible object storage |
220
- | PostgreSQL | 5433 | Application database |
221
- | Valkey | 6379 | Session cache (Redis-compatible) |
216
+ | Service | Port | Description |
217
+ | ---------- | ---------- | -------------------------------- |
218
+ | Keycloak | 8080 | Identity provider (admin/admin) |
219
+ | MinIO | 9000, 9001 | S3-compatible object storage |
220
+ | PostgreSQL | 5433 | Application database |
221
+ | Valkey | 6379 | Session cache (Redis-compatible) |
222
222
 
223
223
  To stop: `podman kube down devenv/local-deployment.yaml`
224
224
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  OAuth 2.0 Authorization Code Flow against Keycloak. Session cookies are httpOnly, secure, SameSite=Lax; session data lives in Redis/Valkey. STS `AssumeRoleWithWebIdentity` mints per-connection S3 credentials using the user's Keycloak idToken.
4
4
 
5
- | Module | Responsibility |
6
- |---|---|
7
- | `sessionMiddleware.ts` | Resolve session cookie → session store entry. |
8
- | `authMiddleware.ts` | Refresh tokens if needed; populate `authContext` for downstream loaders. |
9
- | `getSessionCredentials.ts` | Mint STS credentials per connection. |
10
- | `verifyIdToken.ts` | JWKS-based idToken signature/expiry verification. |
11
- | `getS3Client.ts` | Build an SDK-v3 S3 client with the connection's signed credentials. |
12
- | `keycloakAdmin/` | Service-account-backed admin API for user/group management. |
5
+ | Module | Responsibility |
6
+ | -------------------------- | ------------------------------------------------------------------------ |
7
+ | `sessionMiddleware.ts` | Resolve session cookie → session store entry. |
8
+ | `authMiddleware.ts` | Refresh tokens if needed; populate `authContext` for downstream loaders. |
9
+ | `getSessionCredentials.ts` | Mint STS credentials per connection. |
10
+ | `verifyIdToken.ts` | JWKS-based idToken signature/expiry verification. |
11
+ | `getS3Client.ts` | Build an SDK-v3 S3 client with the connection's signed credentials. |
12
+ | `keycloakAdmin/` | Service-account-backed admin API for user/group management. |
@@ -4,11 +4,7 @@ import { getSessionData } from "./getSession";
4
4
  import { getAllSessionCredentials } from "./getSessionCredentials";
5
5
  import { refreshAccessTokenWithLock } from "./refreshAuthTokens";
6
6
  import { sessionContext } from "./sessionMiddleware";
7
- import {
8
- type CytarioSession,
9
- type SessionData,
10
- sessionStorage,
11
- } from "./sessionStorage";
7
+ import { type CytarioSession, type SessionData, sessionStorage } from "./sessionStorage";
12
8
  import { verifyIdToken } from "./verifyIdToken";
13
9
  import { ConnectionConfig } from "~/.generated/client";
14
10
  import { createLabel } from "~/.server/logging";
@@ -52,10 +48,7 @@ const fetchAllCredentials = async (
52
48
  ): Promise<{ sessionData: SessionData; connectionConfigs: ConnectionConfig[] }> => {
53
49
  const connectionConfigs = await listConnections(sessionData.user);
54
50
 
55
- const newCredentials = await getAllSessionCredentials(
56
- sessionData,
57
- connectionConfigs,
58
- );
51
+ const newCredentials = await getAllSessionCredentials(sessionData, connectionConfigs);
59
52
 
60
53
  return {
61
54
  sessionData: {
@@ -72,18 +65,13 @@ const fetchAllCredentials = async (
72
65
  * Sets validated session data in authContext for downstream use.
73
66
  * Export this from protected routes that require authentication.
74
67
  */
75
- export const authMiddleware: MiddlewareFunction = async (
76
- { request, context },
77
- next,
78
- ) => {
68
+ export const authMiddleware: MiddlewareFunction = async ({ request, context }, next) => {
79
69
  console.info(`${label} ${request.method} ${request.url}`);
80
70
 
81
71
  const session = context.get(sessionContext);
82
72
 
83
73
  if (!session) {
84
- throw new Error(
85
- "Session not found in context. Ensure sessionMiddleware runs first.",
86
- );
74
+ throw new Error("Session not found in context. Ensure sessionMiddleware runs first.");
87
75
  }
88
76
 
89
77
  const sessionData = await getSessionData(session);
@@ -116,10 +104,7 @@ export const authMiddleware: MiddlewareFunction = async (
116
104
 
117
105
  let newAuthTokens;
118
106
  try {
119
- newAuthTokens = await refreshAccessTokenWithLock(
120
- session.id,
121
- authTokens.refreshToken,
122
- );
107
+ newAuthTokens = await refreshAccessTokenWithLock(session.id, authTokens.refreshToken);
123
108
  } catch (error) {
124
109
  console.error(`${label} Token refresh failed:`, error);
125
110
  }
@@ -127,11 +112,10 @@ export const authMiddleware: MiddlewareFunction = async (
127
112
  if (newAuthTokens) {
128
113
  session.set("authTokens", newAuthTokens);
129
114
 
130
- const { sessionData: withCredentials, connectionConfigs } =
131
- await fetchAllCredentials({
132
- ...updatedSessionData,
133
- authTokens: newAuthTokens,
134
- });
115
+ const { sessionData: withCredentials, connectionConfigs } = await fetchAllCredentials({
116
+ ...updatedSessionData,
117
+ authTokens: newAuthTokens,
118
+ });
135
119
  updatedSessionData = withCredentials;
136
120
 
137
121
  session.set("credentials", updatedSessionData.credentials);
@@ -19,9 +19,7 @@ export const exchangeAuthCode = async (
19
19
  method: "POST",
20
20
  headers: {
21
21
  "Content-Type": "application/x-www-form-urlencoded",
22
- Authorization: `Basic ${Buffer.from(
23
- `${clientId}:${clientSecret}`,
24
- ).toString("base64")}`,
22
+ Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
25
23
  },
26
24
  body: new URLSearchParams({
27
25
  grant_type: "authorization_code",
@@ -32,9 +30,7 @@ export const exchangeAuthCode = async (
32
30
  });
33
31
 
34
32
  if (!tokenResponse.ok) {
35
- throw new Error(
36
- `Token exchange failed: ${tokenResponse.status}`,
37
- );
33
+ throw new Error(`Token exchange failed: ${tokenResponse.status}`);
38
34
  }
39
35
 
40
36
  const json = await tokenResponse.json();
@@ -24,17 +24,11 @@ const s3ClientCache = new LRUCache<string, CacheEntry>({
24
24
  /**
25
25
  * Creates a unique cache key scoped to user, bucket, and credential identity.
26
26
  */
27
- const createCacheKey = (
28
- userId: string,
29
- bucketName: string,
30
- credentials: Credentials,
31
- ): string => {
27
+ const createCacheKey = (userId: string, bucketName: string, credentials: Credentials): string => {
32
28
  // Hash credentials to detect when they change (e.g., after refresh)
33
29
  const credHash = crypto
34
30
  .createHash("sha256")
35
- .update(
36
- `${credentials.AccessKeyId}:${credentials.SecretAccessKey}:${credentials.SessionToken}`,
37
- )
31
+ .update(`${credentials.AccessKeyId}:${credentials.SecretAccessKey}:${credentials.SessionToken}`)
38
32
  .digest("hex")
39
33
  .substring(0, 16);
40
34
 
@@ -89,10 +83,7 @@ export const getS3Client = async (
89
83
  };
90
84
 
91
85
  /** Remove all cached S3 clients for a given user + bucket (e.g. after config change). */
92
- export const invalidateS3ClientsForBucket = (
93
- userId: string,
94
- bucketName: string,
95
- ): void => {
86
+ export const invalidateS3ClientsForBucket = (userId: string, bucketName: string): void => {
96
87
  for (const key of s3ClientCache.keys()) {
97
88
  const entry = s3ClientCache.peek(key);
98
89
  if (entry && entry.userId === userId && entry.bucketName === bucketName) {
@@ -100,4 +91,3 @@ export const invalidateS3ClientsForBucket = (
100
91
  }
101
92
  }
102
93
  };
103
-
@@ -1,22 +1,13 @@
1
- import {
2
- AssumeRoleWithWebIdentityCommand,
3
- Credentials,
4
- STSClient,
5
- } from "@aws-sdk/client-sts";
6
-
7
- import {
8
- type ConnectionsCredentials,
9
- type SessionData,
10
- } from "./sessionStorage";
1
+ import { AssumeRoleWithWebIdentityCommand, Credentials, STSClient } from "@aws-sdk/client-sts";
2
+
3
+ import { type ConnectionsCredentials, type SessionData } from "./sessionStorage";
11
4
  import { ConnectionConfig } from "~/.generated/client";
12
5
  import { createLabel } from "~/.server/logging";
13
6
  import { getS3ProviderConfig } from "~/utils/s3Provider";
14
7
 
15
8
  const label = createLabel("credentials", "cyan");
16
9
 
17
- export const isValidCredentials = (
18
- credentials?: { Expiration?: Date },
19
- ): boolean => {
10
+ export const isValidCredentials = (credentials?: { Expiration?: Date }): boolean => {
20
11
  if (!credentials?.Expiration) return false;
21
12
 
22
13
  // Check if credentials are expired (with 5 minute buffer)
@@ -91,9 +82,7 @@ export const getAllSessionCredentials = async (
91
82
  return sessionData.credentials;
92
83
  }
93
84
 
94
- console.info(
95
- `${label} Fetching credentials for ${stale.length} connection(s)`,
96
- );
85
+ console.info(`${label} Fetching credentials for ${stale.length} connection(s)`);
97
86
 
98
87
  const roleSessionName = sanitizeRoleSessionName(sessionData.user.name);
99
88
 
@@ -114,10 +103,7 @@ export const getAllSessionCredentials = async (
114
103
  if (result.status === "fulfilled") {
115
104
  newCredentials[result.value.name] = result.value.credentials;
116
105
  } else {
117
- console.warn(
118
- `${label} Failed to fetch credentials for a connection:`,
119
- result.reason,
120
- );
106
+ console.warn(`${label} Failed to fetch credentials for a connection:`, result.reason);
121
107
  }
122
108
  }
123
109
 
@@ -48,9 +48,7 @@ function enrichUserProfile(raw: UserProfileRaw): UserProfile {
48
48
  }
49
49
 
50
50
  /** Retrieves and enriches user profile data from Keycloak. */
51
- export const getUserInfo = async (
52
- accessToken: string,
53
- ): Promise<UserProfile> => {
51
+ export const getUserInfo = async (accessToken: string): Promise<UserProfile> => {
54
52
  try {
55
53
  const wellKnownEndpoints = await getWellKnownEndpoints();
56
54
  const { userinfo_endpoint } = wellKnownEndpoints;
@@ -63,9 +61,7 @@ export const getUserInfo = async (
63
61
 
64
62
  if (!response.ok) {
65
63
  const errorText = await response.text();
66
- throw new Error(
67
- `UserInfo fetch failed: ${response.status} - ${errorText}`,
68
- );
64
+ throw new Error(`UserInfo fetch failed: ${response.status} - ${errorText}`);
69
65
  }
70
66
 
71
67
  const rawUserProfile = await response.json();
@@ -27,16 +27,9 @@ export class KeycloakAdminError extends Error {
27
27
  }
28
28
  }
29
29
 
30
- const adminApiBaseUrl = cytarioConfig.auth.baseUrl.replace(
31
- "/realms/",
32
- "/admin/realms/",
33
- );
30
+ const adminApiBaseUrl = cytarioConfig.auth.baseUrl.replace("/realms/", "/admin/realms/");
34
31
 
35
- async function adminRequest(
36
- method: string,
37
- path: string,
38
- body?: unknown,
39
- ): Promise<Response> {
32
+ async function adminRequest(method: string, path: string, body?: unknown): Promise<Response> {
40
33
  const accessToken = await getAdminToken();
41
34
 
42
35
  const response = await fetch(`${adminApiBaseUrl}${path}`, {
@@ -61,9 +61,7 @@ export function flattenGroups(groups: KeycloakGroup[]): string[] {
61
61
  * For each admin scope, finds the exact group by path and flattens its descendants.
62
62
  * Uses findGroupByPath instead of search to avoid returning unrelated groups.
63
63
  */
64
- export async function getManageableScopes(
65
- user: UserProfile,
66
- ): Promise<string[]> {
64
+ export async function getManageableScopes(user: UserProfile): Promise<string[]> {
67
65
  if (user.adminScopes.length === 0) return [];
68
66
 
69
67
  const allScopes = new Set<string>();
@@ -78,10 +76,7 @@ export async function getManageableScopes(
78
76
  }
79
77
  }
80
78
  } catch (error) {
81
- console.warn(
82
- `Failed to fetch group tree for admin scope "${adminScope}":`,
83
- error,
84
- );
79
+ console.warn(`Failed to fetch group tree for admin scope "${adminScope}":`, error);
85
80
  }
86
81
  }
87
82
 
@@ -91,9 +86,7 @@ export async function getManageableScopes(
91
86
  /**
92
87
  * Finds a Keycloak group by its normalized path (e.g. "cytario/lab").
93
88
  */
94
- export async function findGroupByPath(
95
- scope: string,
96
- ): Promise<KeycloakGroup | undefined> {
89
+ export async function findGroupByPath(scope: string): Promise<KeycloakGroup | undefined> {
97
90
  const topLevel = scope.split("/")[0];
98
91
  const groups = await fetchGroups(topLevel);
99
92
  const targetPath = `/${scope}`;
@@ -185,11 +178,7 @@ export async function createGroup(
185
178
  throw new KeycloakAdminError(404, `Parent group not found: ${parentScope}`);
186
179
  }
187
180
 
188
- const response = await adminMutate(
189
- "POST",
190
- `/groups/${parent.id}/children`,
191
- { name },
192
- );
181
+ const response = await adminMutate("POST", `/groups/${parent.id}/children`, { name });
193
182
 
194
183
  const location = response.headers.get("location");
195
184
  if (!location) {
@@ -202,11 +191,9 @@ export async function createGroup(
202
191
 
203
192
  let adminsGroupId: string;
204
193
  try {
205
- const adminsResponse = await adminMutate(
206
- "POST",
207
- `/groups/${newGroupId}/children`,
208
- { name: "admins" },
209
- );
194
+ const adminsResponse = await adminMutate("POST", `/groups/${newGroupId}/children`, {
195
+ name: "admins",
196
+ });
210
197
  const adminsLocation = adminsResponse.headers.get("location");
211
198
  adminsGroupId = adminsLocation?.split("/").pop() ?? "";
212
199
  if (!adminsGroupId) {
@@ -232,9 +219,7 @@ export async function createGroup(
232
219
  /**
233
220
  * Fetches members for a group and all its sub-groups, returning the tree structure.
234
221
  */
235
- export async function getGroupWithMembers(
236
- scope: string,
237
- ): Promise<GroupWithMembers | undefined> {
222
+ export async function getGroupWithMembers(scope: string): Promise<GroupWithMembers | undefined> {
238
223
  const group = await findGroupByPath(scope);
239
224
  if (!group) return undefined;
240
225
 
@@ -243,9 +228,7 @@ export async function getGroupWithMembers(
243
228
 
244
229
  await Promise.all(
245
230
  allIds.map(async (id) => {
246
- const members = await adminFetch<KeycloakUser[]>(
247
- `/groups/${id}/members?max=500`,
248
- );
231
+ const members = await adminFetch<KeycloakUser[]>(`/groups/${id}/members?max=500`);
249
232
  membersByGroupId.set(id, members);
250
233
  }),
251
234
  );
@@ -1,9 +1,4 @@
1
- import {
2
- adminFetch,
3
- adminMutate,
4
- KeycloakAdminError,
5
- type KeycloakUser,
6
- } from "./client";
1
+ import { adminFetch, adminMutate, KeycloakAdminError, type KeycloakUser } from "./client";
7
2
  import { findGroupByPath } from "./groups";
8
3
  import { cytarioConfig } from "~/config";
9
4
 
@@ -18,24 +13,15 @@ export async function updateUser(
18
13
  await adminMutate("PUT", `/users/${userId}`, data);
19
14
  }
20
15
 
21
- export async function addUserToGroup(
22
- userId: string,
23
- groupId: string,
24
- ): Promise<void> {
16
+ export async function addUserToGroup(userId: string, groupId: string): Promise<void> {
25
17
  await adminMutate("PUT", `/users/${userId}/groups/${groupId}`);
26
18
  }
27
19
 
28
- export async function removeUserFromGroup(
29
- userId: string,
30
- groupId: string,
31
- ): Promise<void> {
20
+ export async function removeUserFromGroup(userId: string, groupId: string): Promise<void> {
32
21
  await adminMutate("DELETE", `/users/${userId}/groups/${groupId}`);
33
22
  }
34
23
 
35
- export async function setUserEnabled(
36
- userId: string,
37
- enabled: boolean,
38
- ): Promise<void> {
24
+ export async function setUserEnabled(userId: string, enabled: boolean): Promise<void> {
39
25
  await adminMutate("PUT", `/users/${userId}`, { enabled });
40
26
  }
41
27
 
@@ -84,10 +70,8 @@ export async function inviteUser(
84
70
  client_id: cytarioConfig.auth.clientId,
85
71
  redirect_uri: cytarioConfig.endpoints.webapp,
86
72
  });
87
- await adminMutate(
88
- "PUT",
89
- `/users/${userId}/execute-actions-email?${params}`,
90
- ["UPDATE_PASSWORD"],
91
- );
73
+ await adminMutate("PUT", `/users/${userId}/execute-actions-email?${params}`, [
74
+ "UPDATE_PASSWORD",
75
+ ]);
92
76
  }
93
77
  }
@@ -23,8 +23,7 @@ export interface OAuthStateResult {
23
23
  * Generates a PKCE code verifier (RFC 7636 §4.1).
24
24
  * 43-char base64url string from 32 random bytes.
25
25
  */
26
- export const generateCodeVerifier = (): string =>
27
- randomBytes(32).toString("base64url");
26
+ export const generateCodeVerifier = (): string => randomBytes(32).toString("base64url");
28
27
 
29
28
  /**
30
29
  * Generates a PKCE code challenge (S256) from a code verifier (RFC 7636 §4.2).
@@ -59,9 +58,7 @@ export const validateRedirectTo = (redirectTo?: string): string => {
59
58
  * and stores it in the cache store (Redis/Valkey) with a short expiry time.
60
59
  * Returns state, codeChallenge, and nonce for the authorization URL.
61
60
  */
62
- export const generateOAuthState = async (
63
- redirectTo?: string,
64
- ): Promise<OAuthStateResult> => {
61
+ export const generateOAuthState = async (redirectTo?: string): Promise<OAuthStateResult> => {
65
62
  const state = randomBytes(32).toString("hex");
66
63
  const codeVerifier = generateCodeVerifier();
67
64
  const codeChallenge = generateCodeChallenge(codeVerifier);
@@ -75,11 +72,7 @@ export const generateOAuthState = async (
75
72
  nonce,
76
73
  };
77
74
 
78
- await redis.setex(
79
- `${STATE_PREFIX}${state}`,
80
- STATE_EXPIRY_SECONDS,
81
- JSON.stringify(stateData),
82
- );
75
+ await redis.setex(`${STATE_PREFIX}${state}`, STATE_EXPIRY_SECONDS, JSON.stringify(stateData));
83
76
 
84
77
  return { state, codeChallenge, nonce };
85
78
  };
@@ -89,9 +82,7 @@ export const generateOAuthState = async (
89
82
  * Returns the state data if valid, or null if invalid/expired.
90
83
  * Uses atomic GETDEL to prevent reuse (requires Redis 6.2+ / Valkey).
91
84
  */
92
- export const validateOAuthState = async (
93
- state: string,
94
- ): Promise<OAuthState | null> => {
85
+ export const validateOAuthState = async (state: string): Promise<OAuthState | null> => {
95
86
  const key = `${STATE_PREFIX}${state}`;
96
87
  const stateJson = await redis.getdel(key);
97
88
 
@@ -5,9 +5,7 @@ import { getSession, getSessionData } from "~/.server/auth/getSession";
5
5
  /**
6
6
  * Redirects the user to the profile page if they are already authenticated.
7
7
  */
8
- export const redirectIfAuthenticated = async ({
9
- request,
10
- }: LoaderFunctionArgs): Promise<void> => {
8
+ export const redirectIfAuthenticated = async ({ request }: LoaderFunctionArgs): Promise<void> => {
11
9
  const session = await getSession(request);
12
10
  const { user } = await getSessionData(session);
13
11
 
@@ -18,9 +18,7 @@ export interface AuthTokensResponse {
18
18
  /**
19
19
  * Refreshes the access token using the provided refresh token.
20
20
  */
21
- export async function refreshAccessToken(
22
- refreshToken: string,
23
- ): Promise<AuthTokens> {
21
+ export async function refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
24
22
  const wellKnownEndpoints = await getWellKnownEndpoints();
25
23
 
26
24
  const response = await fetch(wellKnownEndpoints.token_endpoint, {
@@ -34,8 +32,7 @@ export async function refreshAccessToken(
34
32
  }),
35
33
  });
36
34
  if (!response.ok) throw new Error("Failed to refresh token");
37
- const { access_token, id_token, refresh_token } =
38
- (await response.json()) as AuthTokensResponse;
35
+ const { access_token, id_token, refresh_token } = (await response.json()) as AuthTokensResponse;
39
36
 
40
37
  const authTokens: AuthTokens = {
41
38
  accessToken: access_token,
@@ -69,9 +66,7 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
69
66
  * Reads the current session auth tokens directly from Redis,
70
67
  * bypassing the in-memory LRU cache to get the freshest data.
71
68
  */
72
- async function readSessionTokensFromStore(
73
- sessionId: string,
74
- ): Promise<AuthTokens | null> {
69
+ async function readSessionTokensFromStore(sessionId: string): Promise<AuthTokens | null> {
75
70
  const data = await redis.hget(sessionId, "data");
76
71
  if (!data) return null;
77
72
 
@@ -96,13 +91,7 @@ export async function refreshAccessTokenWithLock(
96
91
  const lockValue = randomUUID();
97
92
 
98
93
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
99
- const acquired = await redis.set(
100
- lockKey,
101
- lockValue,
102
- "EX",
103
- LOCK_TTL_SECONDS,
104
- "NX",
105
- );
94
+ const acquired = await redis.set(lockKey, lockValue, "EX", LOCK_TTL_SECONDS, "NX");
106
95
 
107
96
  if (acquired === "OK") {
108
97
  try {
@@ -110,10 +99,7 @@ export async function refreshAccessTokenWithLock(
110
99
  // by a previous lock holder (handles refresh token rotation)
111
100
  const currentTokens = await readSessionTokensFromStore(sessionId);
112
101
 
113
- if (
114
- currentTokens &&
115
- currentTokens.refreshToken !== refreshToken
116
- ) {
102
+ if (currentTokens && currentTokens.refreshToken !== refreshToken) {
117
103
  return currentTokens;
118
104
  }
119
105
 
@@ -9,10 +9,7 @@ export const sessionContext = createContext<CytarioSession>();
9
9
  * Middleware that retrieves the session from the request
10
10
  * and sets it in the context for downstream use.
11
11
  */
12
- export const sessionMiddleware: MiddlewareFunction = async (
13
- { request, context },
14
- next
15
- ) => {
12
+ export const sessionMiddleware: MiddlewareFunction = async ({ request, context }, next) => {
16
13
  const session = await getSession(request);
17
14
  context.set(sessionContext, session);
18
15
  return next();
@@ -47,10 +47,7 @@ const sessionCache = new LRUCache<string, SessionData>({
47
47
  ttl: 5000, // 5 seconds TTL - short enough to keep data fresh
48
48
  });
49
49
 
50
- export const sessionStorage = createSessionStorage<
51
- SessionData,
52
- SessionFlashData
53
- >({
50
+ export const sessionStorage = createSessionStorage<SessionData, SessionFlashData>({
54
51
  cookie: {
55
52
  name: "__session",
56
53
  ...cookie,
@@ -20,9 +20,7 @@ const getJwks = async () => {
20
20
  * behavior varies by client configuration. Add after confirming the actual
21
21
  * `aud` claim value in production tokens.
22
22
  */
23
- export const verifyIdToken = async (
24
- token: string,
25
- ): Promise<JWTPayload | null> => {
23
+ export const verifyIdToken = async (token: string): Promise<JWTPayload | null> => {
26
24
  try {
27
25
  const jwks = await getJwks();
28
26
  const { issuer } = await getWellKnownEndpoints();
@@ -32,10 +32,7 @@ const fetchWellKnownEndpoints = async (): Promise<WellKnownEndpoints> => {
32
32
  cacheExpiresAt = Date.now() + CACHE_TTL_MS;
33
33
  return data;
34
34
  } catch (error) {
35
- console.error(
36
- `Error fetching well-known endpoints from ${endpointUrl}:`,
37
- error,
38
- );
35
+ console.error(`Error fetching well-known endpoints from ${endpointUrl}:`, error);
39
36
  cachedEndpoints = null;
40
37
  cacheExpiresAt = 0;
41
38
  throw error;
@@ -59,6 +59,10 @@ redis.on("error", (err) => {
59
59
  });
60
60
 
61
61
  redis.on("connect", () => {
62
- const authInfo = username ? ` (authenticated as ${username})` : password ? " (authenticated)" : "";
62
+ const authInfo = username
63
+ ? ` (authenticated as ${username})`
64
+ : password
65
+ ? " (authenticated)"
66
+ : "";
63
67
  console.log(`Connected to Redis/Valkey at ${host}:${port}${authInfo}`);
64
68
  });
@@ -9,8 +9,5 @@ const ansiColors = {
9
9
  gray: "\x1b[90m",
10
10
  };
11
11
 
12
- export const createLabel = (
13
- str: string,
14
- color: keyof typeof ansiColors = "white"
15
- ) =>
12
+ export const createLabel = (str: string, color: keyof typeof ansiColors = "white") =>
16
13
  `${ansiColors[color]}[${str.slice(0, 10).toUpperCase().padEnd(10, " ")}]\x1b[0m`;
@@ -8,10 +8,7 @@ const label = createLabel("duration", "red");
8
8
  * Middleware that logs request duration for debugging performance.
9
9
  * Export this from routes where you want to measure request timing.
10
10
  */
11
- export const requestDurationMiddleware: MiddlewareFunction = async (
12
- { request },
13
- next
14
- ) => {
11
+ export const requestDurationMiddleware: MiddlewareFunction = async ({ request }, next) => {
15
12
  const startTime = performance.now();
16
13
  const url = new URL(request.url);
17
14
  const path = url.pathname + url.search;