@k2b/cloud 0.6.0 → 0.8.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.
Files changed (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -2,6 +2,7 @@ import { sql } from "bun";
2
2
  import type { MutationResult, UserProfile } from "../../../contracts/shared";
3
3
  import { isAccountCategoryAllowed } from "../../account-category-policy";
4
4
  import { writeDeletedAccountAudit } from "../../account-lifecycle/audit";
5
+ import { emailAlreadyUsed, findAccountsByEmail, lockAccountEmail, normalizeAccountEmail } from "../../accounts/email-write";
5
6
  import { resolveStoredAdminState } from "../../accounts/model";
6
7
  import { writeLocalAccount } from "../../accounts/posix";
7
8
  import type { AuditActor } from "../../audit";
@@ -31,6 +32,7 @@ export const create = async (params: {
31
32
  admin?: boolean;
32
33
  actor?: AuditActor;
33
34
  }): Promise<MutationResult<{ id: string }>> => {
35
+ const email = normalizeAccountEmail(params.data.email);
34
36
  const uid = await createLocalUid();
35
37
  const admin = resolveStoredAdminState({
36
38
  provider: "local",
@@ -42,6 +44,8 @@ export const create = async (params: {
42
44
  return await writeLocalAccount(async (tx) => {
43
45
  if (!(await isAccountCategoryAllowed({ provider: "local", profile: params.profile }, tx)))
44
46
  return { ok: false, error: "This account category is disabled. Contact an administrator.", status: 403 };
47
+ await lockAccountEmail(tx, email);
48
+ if ((await findAccountsByEmail(tx, email)).length > 0) return emailAlreadyUsed();
45
49
  const rows = await tx<{ id: string }[]>`
46
50
  INSERT INTO auth.users (
47
51
  uid,
@@ -58,7 +62,7 @@ export const create = async (params: {
58
62
  ${uid},
59
63
  'local',
60
64
  ${params.profile},
61
- ${params.data.email},
65
+ ${email},
62
66
  ${params.data.givenname ?? ""},
63
67
  ${params.data.sn ?? ""},
64
68
  ${params.data.displayName ?? ""},
@@ -118,21 +122,26 @@ export const update = async (params: {
118
122
  mail?: string;
119
123
  };
120
124
  }): Promise<MutationResult<void>> => {
121
- const existingRows = await sql<DbRow[]>`SELECT id FROM auth.users WHERE id = ${params.id}::uuid AND provider = 'local'`;
122
- if (existingRows.length === 0) {
123
- return { ok: false, error: "Local user not found", status: 404 };
124
- }
125
-
126
- await sql`
127
- UPDATE auth.users
128
- SET given_name = CASE WHEN ${params.data.givenname !== undefined} THEN ${params.data.givenname ?? ""} ELSE given_name END,
129
- sn = CASE WHEN ${params.data.sn !== undefined} THEN ${params.data.sn ?? ""} ELSE sn END,
130
- display_name = CASE WHEN ${params.data.displayName !== undefined} THEN ${params.data.displayName ?? ""} ELSE display_name END,
131
- mail = CASE WHEN ${params.data.mail !== undefined} THEN ${params.data.mail ?? null} ELSE mail END
132
- WHERE id = ${params.id}::uuid
133
- `;
134
-
135
- return { ok: true, data: undefined };
125
+ return sql.begin(async (tx) => {
126
+ if (params.data.mail !== undefined) await lockAccountEmail(tx, params.data.mail);
127
+ else await tx`LOCK TABLE auth.users IN ROW EXCLUSIVE MODE`;
128
+ const [existing] = await tx<{ mail: string | null }[]>`
129
+ SELECT mail FROM auth.users WHERE id = ${params.id}::uuid AND provider = 'local' FOR UPDATE
130
+ `;
131
+ if (!existing) return { ok: false, error: "Local user not found", status: 404 };
132
+ const email = params.data.mail === undefined ? undefined : normalizeAccountEmail(params.data.mail);
133
+ const changed = email !== undefined && email !== normalizeAccountEmail(existing.mail ?? "");
134
+ if (changed && email && (await findAccountsByEmail(tx, email)).some((row) => row.id !== params.id)) return emailAlreadyUsed();
135
+ await tx`
136
+ UPDATE auth.users
137
+ SET given_name = CASE WHEN ${params.data.givenname !== undefined} THEN ${params.data.givenname ?? ""} ELSE given_name END,
138
+ sn = CASE WHEN ${params.data.sn !== undefined} THEN ${params.data.sn ?? ""} ELSE sn END,
139
+ display_name = CASE WHEN ${params.data.displayName !== undefined} THEN ${params.data.displayName ?? ""} ELSE display_name END,
140
+ mail = CASE WHEN ${changed} THEN ${email || null} ELSE mail END
141
+ WHERE id = ${params.id}::uuid
142
+ `;
143
+ return { ok: true, data: undefined };
144
+ });
136
145
  };
137
146
 
138
147
  export const setProfile = async (params: {
@@ -0,0 +1,101 @@
1
+ import type { ClientRequest, IncomingMessage } from "node:http";
2
+ import { type RequestOptions, request } from "node:https";
3
+ import { resolvePublicNetworkAddresses } from "./network-security";
4
+
5
+ export type PublicHttpInput = {
6
+ url: string;
7
+ method: string;
8
+ headers: Record<string, string>;
9
+ body?: Uint8Array;
10
+ maxBytes: number;
11
+ signal: AbortSignal;
12
+ };
13
+
14
+ /** One public HTTPS request, pinned to a validated address. Never redirects or retries.
15
+ * Callers own credential authorization, request budgets and response disclosure. */
16
+ export async function requestPublicHttps(
17
+ input: PublicHttpInput,
18
+ dependencies: {
19
+ resolve: typeof resolvePublicNetworkAddresses;
20
+ request: (options: RequestOptions, callback: (response: IncomingMessage) => void) => ClientRequest;
21
+ } = { resolve: resolvePublicNetworkAddresses, request },
22
+ ) {
23
+ if (!Number.isSafeInteger(input.maxBytes) || input.maxBytes < 0) throw new Error("INVALID_BYTE_LIMIT");
24
+ const url = new URL(input.url);
25
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("HTTPS_REQUIRED");
26
+ input.signal.throwIfAborted();
27
+ let cancelLookup = () => {};
28
+ const addresses = await Promise.race([
29
+ dependencies.resolve(url.hostname),
30
+ new Promise<never>((_, reject) => {
31
+ cancelLookup = () => reject(new Error("HTTP_CANCELLED"));
32
+ input.signal.addEventListener("abort", cancelLookup, { once: true });
33
+ if (input.signal.aborted) cancelLookup();
34
+ }),
35
+ ]).finally(() => input.signal.removeEventListener("abort", cancelLookup));
36
+ input.signal.throwIfAborted();
37
+ const target = addresses[0]!;
38
+ return new Promise<{ status: number; headers: Record<string, string>; body: Uint8Array }>((resolve, reject) => {
39
+ let settled = false;
40
+ const fail = () => finish(undefined);
41
+ const finish = (value?: { status: number; headers: Record<string, string>; body: Uint8Array }) => {
42
+ if (settled) return;
43
+ settled = true;
44
+ input.signal.removeEventListener("abort", abort);
45
+ if (value) resolve(value);
46
+ else reject(new Error("HTTP_FAILED"));
47
+ };
48
+ const outgoing = dependencies.request(
49
+ {
50
+ hostname: target.address,
51
+ family: target.family,
52
+ port: url.port || 443,
53
+ servername: url.hostname.replace(/^\[|\]$/g, ""),
54
+ rejectUnauthorized: true,
55
+ path: url.pathname + url.search,
56
+ method: input.method,
57
+ headers: { ...input.headers, host: url.host, "accept-encoding": "identity" },
58
+ },
59
+ (incoming) => {
60
+ const status = incoming.statusCode ?? 0;
61
+ const headers: Record<string, string> = {};
62
+ for (const name of ["content-type", "retry-after", "etag", "last-modified"]) {
63
+ const value = incoming.headers[name];
64
+ if (typeof value === "string") headers[name] = value;
65
+ }
66
+ // A redirect never receives a second request or exposes a Location token.
67
+ if (status >= 300 && status < 400 && status !== 304) {
68
+ incoming.destroy();
69
+ finish({ status, headers, body: new Uint8Array() });
70
+ return;
71
+ }
72
+ if (incoming.headers["content-encoding"] && incoming.headers["content-encoding"] !== "identity") {
73
+ incoming.destroy();
74
+ fail();
75
+ return;
76
+ }
77
+ const chunks: Uint8Array[] = [];
78
+ let bytes = 0;
79
+ incoming.on("data", (chunk: Buffer) => {
80
+ if (settled) return;
81
+ bytes += chunk.byteLength;
82
+ if (bytes > input.maxBytes) {
83
+ incoming.destroy();
84
+ fail();
85
+ } else chunks.push(chunk);
86
+ });
87
+ incoming.on("end", () => finish({ status, headers, body: Buffer.concat(chunks, bytes) }));
88
+ incoming.on("error", fail);
89
+ incoming.on("aborted", fail);
90
+ },
91
+ );
92
+ const abort = () => {
93
+ outgoing.destroy();
94
+ fail();
95
+ };
96
+ outgoing.on("error", fail); // Never expose errors containing injected headers.
97
+ input.signal.addEventListener("abort", abort, { once: true });
98
+ if (input.signal.aborted) abort();
99
+ else outgoing.end(input.body);
100
+ });
101
+ }
@@ -0,0 +1,95 @@
1
+ import { type SQL, sql } from "bun";
2
+ import { type RailAdminInput, RailAdminInputSchema, RailAdminSchema, type RailAdminState } from "../contracts/rail-admin";
3
+ import { RailShortcutSchema } from "../contracts/rail-preferences";
4
+ import { hasRole, type User } from "../contracts/shared";
5
+ import { buildAccessPrincipalCondition, createAccess } from "../server/services/access";
6
+
7
+ export class RailAdminError extends Error {
8
+ constructor(
9
+ public status: 400 | 403 | 409,
10
+ message: string,
11
+ ) {
12
+ super(message);
13
+ }
14
+ }
15
+ const requireAdmin = (actor: User | undefined) => {
16
+ if (!actor || !hasRole(actor, "admin")) throw new RailAdminError(403, "Administrator access required");
17
+ };
18
+
19
+ export const createRailShortcutsService = (db: SQL = sql) => {
20
+ const list = async (query: SQL = db): Promise<RailAdminState> => {
21
+ const [state] = await query<{ revision: number }[]>`SELECT revision FROM auth.rail_state WHERE singleton`;
22
+ const rows = await query<{ shortcut: unknown; access: unknown }[]>`
23
+ SELECT r.shortcut, COALESCE((
24
+ SELECT jsonb_agg(jsonb_build_object(
25
+ 'id', a.id, 'permission', a.permission, 'createdAt', a.created_at,
26
+ 'principal', CASE WHEN a.user_id IS NOT NULL THEN jsonb_build_object('type', 'user', 'userId', a.user_id)
27
+ WHEN a.group_id IS NOT NULL THEN jsonb_build_object('type', 'group', 'groupId', a.group_id)
28
+ ELSE jsonb_build_object('type', 'authenticated') END,
29
+ 'displayName', COALESCE(u.display_name, g.name, '')
30
+ ) ORDER BY a.created_at, a.id)
31
+ FROM auth.rail_shortcut_access ra JOIN auth.access a ON a.id = ra.access_id
32
+ LEFT JOIN auth.users u ON u.id = a.user_id LEFT JOIN auth.groups g ON g.id = a.group_id
33
+ WHERE ra.shortcut_id = r.id
34
+ ), '[]'::jsonb) AS access
35
+ FROM auth.rail_shortcuts r ORDER BY r.position, r.id
36
+ `;
37
+ return RailAdminSchema.parse({ revision: state?.revision ?? 0, entries: rows });
38
+ };
39
+ return {
40
+ async invalidateCache(actor: User | undefined) {
41
+ requireAdmin(actor);
42
+ // One generation switch invalidates every user, including concurrent old refills.
43
+ // Keep the configuration revision unchanged so open editors remain valid.
44
+ await db`UPDATE auth.rail_state SET cache_version = gen_random_uuid() WHERE singleton`;
45
+ },
46
+ async list(actor: User | undefined) {
47
+ requireAdmin(actor);
48
+ return list();
49
+ },
50
+ async save(actor: User | undefined, input: RailAdminInput) {
51
+ requireAdmin(actor);
52
+ const value = RailAdminInputSchema.parse(input);
53
+ return db.begin(async (tx) => {
54
+ const [state] = await tx<{ revision: number }[]>`SELECT revision FROM auth.rail_state WHERE singleton FOR UPDATE`;
55
+ if (state?.revision !== value.revision) throw new RailAdminError(409, "App bar configuration changed. Reload before saving.");
56
+ await tx`DELETE FROM auth.access WHERE id IN (SELECT access_id FROM auth.rail_shortcut_access)`;
57
+ await tx`DELETE FROM auth.rail_shortcuts`;
58
+ for (const [position, entry] of value.entries.entries()) {
59
+ await tx`INSERT INTO auth.rail_shortcuts(id, position, shortcut)
60
+ VALUES (${entry.shortcut.id}, ${position}, (${JSON.stringify(entry.shortcut)}::text)::jsonb)`;
61
+ const seen = new Set<string>();
62
+ for (const access of entry.access) {
63
+ const key = JSON.stringify(access.principal);
64
+ if (seen.has(key)) continue;
65
+ seen.add(key);
66
+ const created = await createAccess({ principal: access.principal, permission: "read" }, tx);
67
+ if (!created.ok) throw new RailAdminError(400, "A selected user or group no longer exists.");
68
+ await tx`INSERT INTO auth.rail_shortcut_access(shortcut_id, access_id) VALUES (${entry.shortcut.id}, ${created.data.id}::uuid)`;
69
+ }
70
+ }
71
+ await tx`UPDATE auth.rail_state SET revision = revision + 1 WHERE singleton`;
72
+ return list(tx);
73
+ });
74
+ },
75
+ async forUser(userId: string) {
76
+ const match = buildAccessPrincipalCondition({
77
+ subject: { type: "user", userId },
78
+ columns: {
79
+ userId: db`a.user_id`,
80
+ groupId: db`a.group_id`,
81
+ serviceAccountId: db`a.service_account_id`,
82
+ authenticatedOnly: db`a.authenticated_only`,
83
+ },
84
+ });
85
+ const rows = await db<{ shortcut: unknown }[]>`
86
+ SELECT r.shortcut FROM auth.rail_shortcuts r WHERE EXISTS (
87
+ SELECT 1 FROM auth.rail_shortcut_access ra JOIN auth.access a ON a.id = ra.access_id
88
+ WHERE ra.shortcut_id = r.id AND a.permission = 'read' AND ${match}
89
+ ) ORDER BY r.position, r.id
90
+ `;
91
+ return rows.map((row) => RailShortcutSchema.parse(row.shortcut));
92
+ },
93
+ };
94
+ };
95
+ export const railShortcuts = createRailShortcutsService();
@@ -0,0 +1,53 @@
1
+ import { type RailSnapshot, RailSnapshotSchema } from "../contracts/rail-preferences";
2
+ import type { User } from "../contracts/shared";
3
+ import { railPreferences } from "./rail-preferences";
4
+ import { railShortcuts } from "./rail-shortcuts";
5
+ import { requestCacheRedis } from "./request-cache-redis";
6
+
7
+ // Private request metadata: never serialized into the public User contract.
8
+ const versions = new WeakMap<User, string>();
9
+ export const setRailCacheVersion = (user: User, version: unknown) => {
10
+ if (typeof version === "string") versions.set(user, version);
11
+ };
12
+
13
+ /** TTL only bounds unused generations; freshness comes from the current identity query. */
14
+ const CACHE_TTL_SECONDS = 300;
15
+ export const createRailSnapshotReader =
16
+ (
17
+ load: (userId: string) => Promise<RailSnapshot>,
18
+ cache: { get(key: string): Promise<string | null>; set(key: string, value: string): Promise<unknown> },
19
+ ) =>
20
+ async (user: User): Promise<RailSnapshot> => {
21
+ const version = versions.get(user);
22
+ if (!version) return load(user.id);
23
+ const key = `appglobalcache:user:${user.id}:rail:v1:${version}`;
24
+ try {
25
+ const cached = await cache.get(key);
26
+ if (cached !== null) {
27
+ const parsed = RailSnapshotSchema.safeParse(JSON.parse(cached));
28
+ if (parsed.success) return parsed.data;
29
+ }
30
+ } catch {
31
+ /* Valkey is an optimization; authoritative reads still work. */
32
+ }
33
+ const value = await load(user.id);
34
+ // A concurrent mutation changes the generation. A late refill remains under
35
+ // this request's old key and cannot poison a subsequent request's snapshot.
36
+ try {
37
+ await cache.set(key, JSON.stringify(value));
38
+ } catch {
39
+ /* Retry on a later read. */
40
+ }
41
+ return value;
42
+ };
43
+
44
+ export const readRailSnapshot = createRailSnapshotReader(
45
+ async (userId) => {
46
+ const [personal, managedShortcuts] = await Promise.all([railPreferences.get(userId), railShortcuts.forUser(userId)]);
47
+ return { ...personal, managedShortcuts };
48
+ },
49
+ {
50
+ get: async (key) => (await requestCacheRedis()).get(key),
51
+ set: async (key, value) => (await requestCacheRedis()).set(key, value, "EX", CACHE_TTL_SECONDS),
52
+ },
53
+ );
@@ -0,0 +1,29 @@
1
+ import { RedisClient } from "bun";
2
+ import { env } from "../config/env";
3
+
4
+ // Cache commands must not queue across a disconnect: delayed fills/invalidations
5
+ // are not useful, and reads must be able to fall back to Postgres. Keep this
6
+ // connection separate from sessions, rate limits and other Redis consumers.
7
+ let client: RedisClient | undefined;
8
+ let initialConnection: Promise<void> | undefined;
9
+ let reconnecting: Promise<void> | undefined;
10
+
11
+ export const requestCacheRedis = async (): Promise<RedisClient> => {
12
+ if (!client) {
13
+ client = new RedisClient(env.REDIS_URL, { enableOfflineQueue: false });
14
+ // The first operation waits for normal connection establishment (Bun's
15
+ // connection timeout applies). Later disconnected operations fail promptly.
16
+ initialConnection = client.connect().catch(() => {});
17
+ }
18
+ await initialConnection;
19
+ if (!client.connected && !reconnecting) {
20
+ // Bun reconnects automatically; also allow recovery after its retry limit.
21
+ reconnecting = client
22
+ .connect()
23
+ .catch(() => {})
24
+ .finally(() => {
25
+ reconnecting = undefined;
26
+ });
27
+ }
28
+ return client;
29
+ };
@@ -11,7 +11,7 @@ import { invalidateIdentitySignerCache, prepareIdentitySigner } from "../identit
11
11
  import { getIdentityRuntimeConfig } from "../identity/runtime-config";
12
12
  import { type CloudSessionClaims, isSessionJwtCandidate, signSessionToken, verifySessionToken } from "../identity/session-token";
13
13
  import * as settings from "../settings";
14
- import { loadJwtSessionUser } from "./user";
14
+ import { loadJwtSessionIdentity, loadJwtSessionUser } from "./user";
15
15
 
16
16
  export type SessionData = {
17
17
  userId: string;
@@ -68,7 +68,6 @@ const authenticateVerified = async (credential: CloudSessionClaims): Promise<Aut
68
68
  groupsAdmin,
69
69
  });
70
70
  if (!user) return null;
71
- if (!(await isAccountCategoryAllowed(user))) return null;
72
71
  if (isAccountExpired(user.accountExpires)) {
73
72
  await session.revokeAllForUser(user.id);
74
73
  return null;
@@ -89,6 +88,18 @@ const authenticateToken = async (token: string): Promise<AuthenticatedSession |
89
88
  return credential ? authenticateVerified(credential) : null;
90
89
  };
91
90
 
91
+ const authenticateUserId = async (token: string): Promise<string | null> => {
92
+ const credential = await verifyCredential(token);
93
+ if (!credential) return null;
94
+ const identity = await loadJwtSessionIdentity({ userId: credential.sub, sid: credential.sid, authEpoch: credential.auth_epoch });
95
+ if (!identity) return null;
96
+ if (isAccountExpired(identity.accountExpires)) {
97
+ await session.revokeAllForUser(identity.userId);
98
+ return null;
99
+ }
100
+ return identity.userId;
101
+ };
102
+
92
103
  const revokeToken = async (token: string, requestId?: string | null): Promise<void> => {
93
104
  if (isSessionJwtCandidate(token)) {
94
105
  const claims = await verifySessionToken(token);
@@ -222,6 +233,8 @@ export const session = {
222
233
  },
223
234
 
224
235
  authenticate: authenticateToken,
236
+ /** Fresh session validity for user-scoped streams; does not grant resource access. */
237
+ authenticateUserId,
225
238
 
226
239
  authenticateRequest: (c: Context, token: string): Promise<AuthenticatedSession | null> =>
227
240
  requestCached(authenticationByRequest, c, token, async () => {
@@ -1,11 +1,13 @@
1
- import { sql } from "bun";
1
+ import { type SQLQuery, sql } from "bun";
2
2
  import type { User } from "../../contracts/shared";
3
+ import { decodeAccountCategoryEnabled } from "../account-category-policy";
3
4
  import { buildRoles } from "../accounts/authz";
4
5
  import { resolveProviderProfile } from "../accounts/base-user";
5
- import { managedGroupIdsSubquery, managedGroupsNamesSubquery } from "../accounts/group-sql";
6
+ import { managedGroupIdsSubquery } from "../accounts/group-sql";
6
7
  import { buildIpaUserData, emptyIpaUserData, userIpaDataColumns, userIpaDataJoin } from "../accounts/ipa-data";
7
8
  import { resolveAccountExpires } from "../accounts/model";
8
9
  import { toPgTextArray } from "../postgres";
10
+ import { setRailCacheVersion } from "../rail-snapshot";
9
11
 
10
12
  type DbRow = Record<string, unknown>;
11
13
 
@@ -69,14 +71,19 @@ export const buildProjectedUser = (row: DbRow): User => {
69
71
  manages,
70
72
  managesGroupIds,
71
73
  };
72
- return provider === "ipa"
73
- ? { ...common, provider: "ipa", ipa: buildIpaUserData(row) ?? emptyIpaUserData() }
74
- : { ...common, provider: "local", ipa: null };
74
+ const user: User =
75
+ provider === "ipa"
76
+ ? { ...common, provider: "ipa", ipa: buildIpaUserData(row) ?? emptyIpaUserData() }
77
+ : { ...common, provider: "local", ipa: null };
78
+ setRailCacheVersion(user, row.rail_cache_version);
79
+ return user;
75
80
  };
76
81
 
77
82
  export const userProjectionSql = (groupsAdmin: string[]) => sql`
78
83
  u.*,
79
- ${userIpaDataColumns},
84
+ (SELECT cache_version::text FROM auth.rail_state WHERE singleton) || ':' ||
85
+ COALESCE((SELECT cache_version::text FROM auth.rail_preferences WHERE user_id = u.id), 'default') AS rail_cache_version,
86
+ ${userIpaDataColumns()},
80
87
  CASE
81
88
  WHEN u.provider = 'local' THEN u.admin
82
89
  ELSE EXISTS(
@@ -85,41 +92,45 @@ export const userProjectionSql = (groupsAdmin: string[]) => sql`
85
92
  AND eg.group_name = ANY(${toPgTextArray(groupsAdmin)}::text[])
86
93
  )
87
94
  END AS effective_admin,
88
- COALESCE(ARRAY(
89
- SELECT g.name
90
- FROM auth.user_groups_v2 ug
91
- JOIN auth.groups g ON g.id = ug.group_id
92
- WHERE ug.user_id = u.id
93
- ORDER BY g.name
94
- ), '{}') AS member_groups,
95
- COALESCE(ARRAY(
96
- SELECT ug.group_id
97
- FROM auth.user_groups_v2 ug
98
- JOIN auth.groups g ON g.id = ug.group_id
95
+ membership.member_groups, membership.member_group_ids,
96
+ management.manages, management.manages_group_ids
97
+ `;
98
+
99
+ /** One scan for membership names/IDs and one recursive traversal for management. */
100
+ export const userProjectionJoin = () => sql`
101
+ ${userIpaDataJoin()}
102
+ LEFT JOIN LATERAL (
103
+ SELECT COALESCE(array_agg(g.name ORDER BY g.name), '{}') AS member_groups,
104
+ COALESCE(array_agg(g.id ORDER BY g.name), '{}') AS member_group_ids
105
+ FROM auth.user_groups_v2 ug JOIN auth.groups g ON g.id = ug.group_id
99
106
  WHERE ug.user_id = u.id
100
- ORDER BY g.name
101
- ), '{}') AS member_group_ids,
102
- COALESCE(ARRAY(${managedGroupsNamesSubquery(sql`u.id`)}), '{}') AS manages,
103
- COALESCE(ARRAY(${managedGroupIdsSubquery(sql`u.id`)}), '{}') AS manages_group_ids
107
+ ) membership ON true
108
+ LEFT JOIN LATERAL (
109
+ SELECT COALESCE(array_agg(DISTINCT g.name ORDER BY g.name), '{}') AS manages,
110
+ COALESCE(array_agg(g.id ORDER BY g.name), '{}') AS manages_group_ids
111
+ FROM auth.groups g WHERE g.id IN (${managedGroupIdsSubquery(sql`u.id`)})
112
+ ) management ON true
104
113
  `;
105
114
 
106
- export const loadJwtSessionUser = async (
107
- params: {
108
- userId: string;
109
- sid: string;
110
- authEpoch: number;
111
- groupsAdmin: string[];
112
- /** Core's consent screen only: still validates family, epoch and expiry. */
113
- allowPendingLegalConsent?: boolean;
114
- },
115
- query: typeof sql = sql,
116
- ): Promise<User | null> => {
117
- const rows = await query<DbRow[]>`
118
- SELECT ${userProjectionSql(params.groupsAdmin)}
115
+ type SessionUserParams = {
116
+ userId: string;
117
+ sid: string;
118
+ authEpoch: number;
119
+ /** Core's consent screen only: still validates family, epoch and expiry. */
120
+ allowPendingLegalConsent?: boolean;
121
+ };
122
+
123
+ // Both projections use the same live validity checks. Neither caches authorization.
124
+ const loadSessionRow = async (params: SessionUserParams, projection: SQLQuery, join: SQLQuery, query: typeof sql) => {
125
+ const [row] = await query<DbRow[]>`
126
+ SELECT ${projection}, (
127
+ SELECT value FROM settings.entries WHERE key = 'user.category.' ||
128
+ CASE WHEN u.provider = 'ipa' THEN 'freeipa' WHEN u.profile = 'guest' THEN 'guest' ELSE 'login' END || '.enabled'
129
+ ) AS category_enabled
119
130
  FROM auth.session_families sf
120
131
  JOIN auth.users u ON u.id = sf.user_id
121
132
  JOIN auth.signing_keys sk ON sk.kid = sf.signing_kid AND sk.state <> 'revoked'
122
- ${userIpaDataJoin}
133
+ ${join}
123
134
  WHERE sf.sid = ${params.sid}::uuid
124
135
  AND sf.user_id = ${params.userId}::uuid
125
136
  AND sf.auth_epoch = ${params.authEpoch}
@@ -130,14 +141,28 @@ export const loadJwtSessionUser = async (
130
141
  SELECT 1 FROM auth.legal_acceptances la WHERE la.user_id = u.id
131
142
  ))
132
143
  `;
133
- return rows[0] ? buildProjectedUser(rows[0]) : null;
144
+ return row && (await decodeAccountCategoryEnabled(row.category_enabled)) ? row : null;
145
+ };
146
+
147
+ export const loadJwtSessionUser = async (
148
+ params: SessionUserParams & { groupsAdmin: string[] },
149
+ query: typeof sql = sql,
150
+ ): Promise<User | null> => {
151
+ const row = await loadSessionRow(params, userProjectionSql(params.groupsAdmin), userProjectionJoin(), query);
152
+ return row ? buildProjectedUser(row) : null;
153
+ };
154
+
155
+ /** Identity-only consumers must not pay for group, IPA, role and rail projections. */
156
+ export const loadJwtSessionIdentity = async (params: SessionUserParams, query: typeof sql = sql) => {
157
+ const row = await loadSessionRow(params, sql`u.id, u.account_expires`, sql``, query);
158
+ return row ? { userId: String(row.id), accountExpires: resolveAccountExpires(row)?.toISOString() ?? null } : null;
134
159
  };
135
160
 
136
161
  export const loadCurrentUser = async (params: { userId: string; groupsAdmin: string[] }, query: typeof sql = sql): Promise<User | null> => {
137
162
  const rows = await query<DbRow[]>`
138
163
  SELECT ${userProjectionSql(params.groupsAdmin)}
139
164
  FROM auth.users u
140
- ${userIpaDataJoin}
165
+ ${userProjectionJoin()}
141
166
  WHERE u.id = ${params.userId}::uuid
142
167
  `;
143
168
  return rows[0] ? buildProjectedUser(rows[0]) : null;
@@ -30,7 +30,11 @@ const redactSecretValue = (entry: SettingEntry): SettingEntry => {
30
30
 
31
31
  export const settingsService = {
32
32
  entry: {
33
- list: async (config?: { pagination?: PageParams; filter?: { query?: string; group?: string }; locale?: string }): Promise<Paginated<SettingEntry>> => {
33
+ list: async (config?: {
34
+ pagination?: PageParams;
35
+ filter?: { query?: string; group?: string };
36
+ locale?: string;
37
+ }): Promise<Paginated<SettingEntry>> => {
34
38
  const entries = await settingsPrimitives.getAll(config?.locale);
35
39
  const query = config?.filter?.query?.trim().toLowerCase();
36
40
  const group = config?.filter?.group?.trim().toLowerCase();