@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
@@ -1,23 +1,24 @@
1
1
  import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
2
- import { sql, type SQL } from "bun";
2
+ import { type SQL, sql } from "bun";
3
+ import { env } from "../config/env";
4
+ import { type AccountCategory, accountCategory } from "../contracts/account-categories";
3
5
  import {
4
- APP_APPROVAL_LIMITS as limits,
5
6
  APP_APPROVAL_PATH,
6
7
  APP_APPROVAL_PROTOCOL,
7
- AppDevicePublicKeySchema,
8
- appDeviceProofMessage,
9
- appPairingProofMessage,
10
8
  type AppDevicePublicKey,
9
+ AppDevicePublicKeySchema,
11
10
  type AppDeviceRequest,
12
11
  type AppDeviceView,
13
12
  type AppPendingLogin,
13
+ appDeviceProofMessage,
14
+ appPairingProofMessage,
15
+ APP_APPROVAL_LIMITS as limits,
14
16
  } from "../contracts/app-approval";
15
- import { accountCategory, type AccountCategory } from "../contracts/account-categories";
17
+ import { publicCloudOrigin } from "../shared/app-url";
16
18
  import { isAccountCategoryAllowed } from "./account-category-policy";
17
19
  import { audit } from "./audit";
18
- import { decryptValue } from "./settings/crypto";
19
- import { publicCloudOrigin } from "../shared/app-url";
20
20
  import { CORE_SETTINGS } from "./settings/core-settings";
21
+ import { decryptValue } from "./settings/crypto";
21
22
 
22
23
  export class AppApprovalError extends Error {
23
24
  constructor(
@@ -47,13 +48,13 @@ export const readAppApprovalConfig = async (db: SQL = sql, requireAppOrigin = tr
47
48
  const adminPairing = values.get("user.app_approval.admin_pairing") ?? false;
48
49
  if (typeof enabled !== "boolean" || typeof adminPairing !== "boolean") return reject("UNAVAILABLE", 503);
49
50
  // The issuer is the operator's canonical URL, never the request Host header.
50
- const rawIssuer = values.get("app.url") ?? process.env.APP_URL ?? "localhost:3000";
51
+ const rawIssuer = values.get("app.url") ?? env.APP_URL ?? "localhost:3000";
51
52
  const rawOrigin = values.get("user.app_approval.origin") ?? CORE_SETTINGS["user.app_approval.origin"].default;
52
53
  const origin = (value: unknown) => {
53
54
  if (typeof value !== "string") return reject("UNAVAILABLE", 503);
54
55
  try {
55
56
  const url = new URL(value);
56
- const development = process.env.NODE_ENV !== "production" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
57
+ const development = env.NODE_ENV !== "production" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
57
58
  if (
58
59
  (url.protocol !== "https:" && !(url.protocol === "http:" && development)) ||
59
60
  url.username ||
@@ -236,7 +236,7 @@ const record = async (params: AuditRecordParams, db: AuditDb = sql): Promise<voi
236
236
  ${asString(params.error?.code ?? null)},
237
237
  ${sanitizeAuditText(params.error?.message ?? null)},
238
238
  ${asString(params.requestId)},
239
- ${JSON.stringify(metadata)}::jsonb
239
+ ${JSON.stringify(metadata)}::text::jsonb
240
240
  )
241
241
  `;
242
242
  } catch (error) {
@@ -0,0 +1,35 @@
1
+ import { requestCacheRedis } from "./request-cache-redis";
2
+
3
+ // Non-JSON markers remain compatible with older readers: they discard them and
4
+ // use Postgres. A marker can never be confused with a stored JSON setting value.
5
+ export const MISSING_SETTING = "!cloud:missing-setting:v1";
6
+ const FILL_PREFIX = "!cloud:cache-fill:v1:";
7
+ const COMPLETE = `if redis.call('GET', KEYS[1]) == ARGV[1] then
8
+ return redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
9
+ end return nil`;
10
+ const DISCARD = `if redis.call('GET', KEYS[1]) == ARGV[1] then
11
+ return redis.call('DEL', KEYS[1])
12
+ end return 0`;
13
+
14
+ /** A concurrent invalidation deletes the lease, so an old read cannot refill it. */
15
+ export const claimCacheFill = async (key: string, observed: string | null): Promise<string | null> => {
16
+ try {
17
+ if (observed?.startsWith(FILL_PREFIX)) return null;
18
+ const redis = await requestCacheRedis();
19
+ if (observed !== null) await redis.send("EVAL", [DISCARD, "1", key, observed]);
20
+ const token = FILL_PREFIX + crypto.randomUUID();
21
+ // A slow loader may still return its value, but loses permission to cache it.
22
+ return (await redis.send("SET", [key, token, "NX", "EX", "5"])) === "OK" ? token : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+
28
+ export const completeCacheFill = async (key: string, token: string | null, value: string, ttlSeconds: number): Promise<void> => {
29
+ if (!token) return;
30
+ try {
31
+ await (await requestCacheRedis()).send("EVAL", [COMPLETE, "1", key, token, value, String(ttlSeconds)]);
32
+ } catch {
33
+ // A cache failure does not change a successful authoritative read.
34
+ }
35
+ };
@@ -0,0 +1,109 @@
1
+ import { type SQL, sql } from "bun";
2
+ import { getApp, listApps } from "../../_internal/registry";
3
+ import type { AppRegistryEntry } from "../../contracts/registry";
4
+ import { resolveAppPresentations } from "../../shared/app-presentation";
5
+ import { helpLocaleChain } from "../../shared/help";
6
+ import { logger } from "../logging";
7
+ import { HELP_RESOURCE_PAGE_SIZE, type HelpSelection, hasHelpBm25, metadata, queryHelp } from "./store";
8
+ import type { HelpReader } from "./types";
9
+
10
+ export { cleanupHelp, migrateHelp, registerHelp } from "./store";
11
+ export type { HelpArticle, HelpMatch, HelpMetadata, HelpReader, HelpReaderFactory } from "./types";
12
+
13
+ const log = logger("help");
14
+
15
+ export const createHelpReader = (
16
+ locale: string,
17
+ dependencies: {
18
+ db?: SQL;
19
+ listApps?: () => Promise<AppRegistryEntry[]>;
20
+ getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
21
+ } = {},
22
+ ): HelpReader => {
23
+ const db = dependencies.db ?? sql;
24
+ const select = async (appId?: string): Promise<HelpSelection[]> => {
25
+ const apps = appId
26
+ ? [await (dependencies.getApp ?? getApp)(appId)].filter((app): app is AppRegistryEntry => app !== null)
27
+ : await (dependencies.listApps ?? listApps)();
28
+ return resolveAppPresentations(apps, locale).flatMap((app) =>
29
+ app.help
30
+ ? [
31
+ {
32
+ app_id: app.id,
33
+ app_name: app.name,
34
+ manifest_hash: app.help.manifestHash,
35
+ locales: helpLocaleChain(locale, app.help.baseLocale),
36
+ },
37
+ ]
38
+ : [],
39
+ );
40
+ };
41
+ return {
42
+ async manifest(appId) {
43
+ const app = await (dependencies.getApp ?? getApp)(appId);
44
+ if (!app?.help) return null;
45
+ const [corpus] = await db`SELECT 1 FROM help.corpora WHERE app_id=${app.id} AND manifest_hash=${app.help.manifestHash}`;
46
+ if (!corpus) return null;
47
+ const rows = await queryHelp(
48
+ [
49
+ {
50
+ app_id: app.id,
51
+ app_name: app.name,
52
+ manifest_hash: app.help.manifestHash,
53
+ locales: helpLocaleChain(locale, app.help.baseLocale),
54
+ },
55
+ ],
56
+ {},
57
+ db,
58
+ );
59
+ return {
60
+ ...app.help,
61
+ locale,
62
+ documents: rows
63
+ .sort((a, b) => a.sort_order - b.sort_order || a.title.localeCompare(b.title))
64
+ .map((row) => ({
65
+ id: row.document_id,
66
+ title: row.title,
67
+ icon: row.icon ?? undefined,
68
+ description: row.description ?? undefined,
69
+ order: row.sort_order,
70
+ locale: row.locale,
71
+ searchUrl: `/api/help/v1/${encodeURIComponent(appId)}/search`,
72
+ url: `/api/help/v1/${encodeURIComponent(appId)}/documents/${encodeURIComponent(row.document_id)}`,
73
+ })),
74
+ };
75
+ },
76
+ async list(cursor = "") {
77
+ return (await queryHelp(await select(), { cursor, limit: HELP_RESOURCE_PAGE_SIZE + 1 }, db)).map(metadata);
78
+ },
79
+ async read(input) {
80
+ const [row] = await queryHelp(await select(input.appId), { documentId: input.documentId, body: true, limit: 1 }, db);
81
+ return row?.markdown === undefined ? null : { ...metadata(row), markdown: row.markdown };
82
+ },
83
+ async search(input) {
84
+ const query = input.query.trim().slice(0, 200);
85
+ if (!query) return [];
86
+ const selection = await select(input.appId);
87
+ if (!selection.length) return [];
88
+ const options = { query, limit: Math.max(1, Math.min(Math.floor(Number.isFinite(input.limit) ? input.limit! : 10), 25)) };
89
+ const bm25 = await hasHelpBm25(db);
90
+ const rows = await queryHelp(selection, { ...options, bm25 }, db).catch((error: unknown) => {
91
+ // Only missing optional SQL objects qualify for fallback, not connection/query errors.
92
+ const code = error && typeof error === "object" ? (Reflect.get(error, "errno") ?? Reflect.get(error, "code")) : undefined;
93
+ if (!bm25 || !["42883", "42704", "42P01"].includes(String(code))) throw error;
94
+ log.warn("BM25 unavailable; using native Help search");
95
+ return queryHelp(selection, options, db);
96
+ });
97
+ return rows.map((row) => ({
98
+ kind: "help",
99
+ appId: row.app_id,
100
+ appName: row.app_name,
101
+ locale: row.locale,
102
+ documentId: row.document_id,
103
+ title: row.title,
104
+ description: row.description ?? undefined,
105
+ }));
106
+ },
107
+ };
108
+ };
109
+ export { startHelpMaintenance } from "./maintenance";
@@ -0,0 +1,25 @@
1
+ import { logger } from "../logging";
2
+ import { cleanupHelp } from "./store";
3
+
4
+ const log = logger("help:maintenance");
5
+ /** Core owns the timer; stop waits for the current bounded deletion. */
6
+ export const startHelpMaintenance = (): (() => Promise<void>) => {
7
+ let stopped = false;
8
+ let inFlight: Promise<void> | undefined;
9
+ let timer: ReturnType<typeof setTimeout>;
10
+ const run = () => {
11
+ inFlight = cleanupHelp()
12
+ .catch((error) => {
13
+ log.error("Expired Help cleanup failed", { error: error instanceof Error ? error.message : String(error) });
14
+ })
15
+ .finally(() => {
16
+ if (!stopped) timer = setTimeout(run, 60_000);
17
+ });
18
+ };
19
+ timer = setTimeout(run, 60_000);
20
+ return async () => {
21
+ stopped = true;
22
+ clearTimeout(timer);
23
+ await inFlight;
24
+ };
25
+ };
@@ -0,0 +1,180 @@
1
+ import { type SQL, sql } from "bun";
2
+ import { APP_REGISTRY_TTL_MS } from "../../_internal/registry";
3
+ import type { HelpCorpus, HelpMetadata } from "./types";
4
+
5
+ // One expired app lease of grace after an app can no longer be discovered.
6
+ export const HELP_RETENTION_MS = APP_REGISTRY_TTL_MS * 2;
7
+ export const HELP_RESOURCE_PAGE_SIZE = 100;
8
+ const searchConfigs = ["english", "german", "simple"] as const;
9
+
10
+ export const migrateHelp = async (db: SQL = sql): Promise<void> => {
11
+ await db.begin(async (tx) => {
12
+ await tx`SELECT pg_advisory_xact_lock(hashtext('cloud:help:migrate'))`;
13
+ await tx`CREATE SCHEMA IF NOT EXISTS help`.simple();
14
+ await tx`CREATE TABLE IF NOT EXISTS help.corpora (
15
+ app_id text NOT NULL, manifest_hash text NOT NULL, base_locale text NOT NULL,
16
+ last_seen_at timestamptz NOT NULL DEFAULT now(),
17
+ PRIMARY KEY (app_id, manifest_hash)
18
+ )`.simple();
19
+ await tx`CREATE INDEX IF NOT EXISTS help_expiry_idx ON help.corpora(last_seen_at)`.simple();
20
+ await tx`CREATE TABLE IF NOT EXISTS help.documents (
21
+ app_id text NOT NULL, manifest_hash text NOT NULL, locale text NOT NULL, document_id text NOT NULL,
22
+ title text NOT NULL, description text, icon text, sort_order integer NOT NULL,
23
+ markdown text NOT NULL, search_text text NOT NULL, search_config text NOT NULL,
24
+ search_document tsvector NOT NULL,
25
+ PRIMARY KEY (app_id, manifest_hash, locale, document_id),
26
+ FOREIGN KEY (app_id, manifest_hash) REFERENCES help.corpora ON DELETE CASCADE
27
+ )`.simple();
28
+ await tx`CREATE INDEX IF NOT EXISTS help_search_idx ON help.documents USING gin(search_document)`.simple();
29
+ });
30
+ // The operator enables the extension; Help never installs server extensions.
31
+ const [extension] = await db<{ installed: boolean }[]>`SELECT EXISTS (
32
+ SELECT 1 FROM pg_extension WHERE extname = 'pg_textsearch') AS installed`;
33
+ if (extension?.installed) {
34
+ try {
35
+ for (const config of searchConfigs) {
36
+ await db
37
+ .unsafe(`CREATE INDEX IF NOT EXISTS help_bm25_${config}_idx ON help.documents
38
+ USING bm25(search_text) WITH (text_config='${config}') WHERE search_config='${config}'`)
39
+ .simple();
40
+ }
41
+ } catch (error) {
42
+ console.warn("Optional Help BM25 indexes unavailable; native search remains active", error);
43
+ }
44
+ }
45
+ };
46
+
47
+ /** Commit the complete source once; subsequent app heartbeats only renew its lease. */
48
+ export const registerHelp = async (corpus: HelpCorpus, db: SQL = sql): Promise<void> => {
49
+ await db.begin(async (tx) => {
50
+ await tx`SET LOCAL statement_timeout = '10s'`.simple();
51
+ const renewed = await tx`UPDATE help.corpora SET last_seen_at = clock_timestamp()
52
+ WHERE app_id = ${corpus.appId} AND manifest_hash = ${corpus.manifestHash} RETURNING app_id`;
53
+ if (renewed.length) return;
54
+ const inserted = await tx`INSERT INTO help.corpora(app_id, manifest_hash, base_locale)
55
+ VALUES (${corpus.appId}, ${corpus.manifestHash}, ${corpus.baseLocale})
56
+ ON CONFLICT (app_id, manifest_hash) DO UPDATE SET last_seen_at = clock_timestamp()
57
+ RETURNING (xmax = 0) AS inserted`;
58
+ if (!inserted[0]?.inserted) return;
59
+ for (const [locale, documents] of [[corpus.baseLocale, corpus.documents] as const, ...Object.entries(corpus.documentsByLocale)]) {
60
+ const config = locale.split("-")[0] === "de" ? "german" : locale.split("-")[0] === "en" ? "english" : "simple";
61
+ // One bounded article per statement; no duplicate whole-corpus JSON transport.
62
+ for (const document of documents) {
63
+ const text = `${document.title}\n${document.description ?? ""}\n${document.searchText}`;
64
+ await tx`INSERT INTO help.documents(app_id, manifest_hash, locale, document_id, title, description, icon,
65
+ sort_order, markdown, search_text, search_config, search_document)
66
+ VALUES (${corpus.appId}, ${corpus.manifestHash}, ${locale}, ${document.id}, ${document.title},
67
+ ${document.description ?? null}, ${document.icon ?? null}, ${document.order}, ${document.markdown}, ${text}, ${config},
68
+ setweight(to_tsvector(${config}::regconfig, ${document.id + " " + document.title}), 'A') ||
69
+ setweight(to_tsvector(${config}::regconfig, ${document.description ?? ""}), 'B') ||
70
+ setweight(to_tsvector(${config}::regconfig, ${document.searchText}), 'D'))`;
71
+ }
72
+ }
73
+ });
74
+ };
75
+
76
+ /** Row locks serialize cleanup with publication/renewal. Delete at most one corpus per tick. */
77
+ export const cleanupHelp = async (db: SQL = sql): Promise<void> => {
78
+ await db.begin(async (tx) => {
79
+ await tx`SET LOCAL statement_timeout = '10s'`.simple();
80
+ await tx`DELETE FROM help.corpora WHERE (app_id, manifest_hash) IN (
81
+ SELECT app_id, manifest_hash FROM help.corpora
82
+ WHERE last_seen_at < now() - ${HELP_RETENTION_MS} * interval '1 millisecond'
83
+ ORDER BY last_seen_at LIMIT 1 FOR UPDATE SKIP LOCKED
84
+ )`;
85
+ });
86
+ };
87
+
88
+ export type HelpSelection = { app_id: string; app_name: string; manifest_hash: string; locales: string[] };
89
+ type Row = {
90
+ app_id: string;
91
+ app_name: string;
92
+ manifest_hash: string;
93
+ locale: string;
94
+ document_id: string;
95
+ title: string;
96
+ description: string | null;
97
+ icon: string | null;
98
+ sort_order: number;
99
+ markdown?: string;
100
+ };
101
+ export const metadata = (row: Row): HelpMetadata => ({
102
+ appId: row.app_id,
103
+ appName: row.app_name,
104
+ manifestHash: row.manifest_hash,
105
+ locale: row.locale,
106
+ documentId: row.document_id,
107
+ title: row.title,
108
+ description: row.description ?? undefined,
109
+ icon: row.icon ?? undefined,
110
+ order: row.sort_order,
111
+ });
112
+
113
+ export const hasHelpBm25 = async (db: SQL = sql): Promise<boolean> => {
114
+ const [row] = await db<{ ready: boolean }[]>`SELECT
115
+ EXISTS (SELECT 1 FROM pg_extension WHERE extname='pg_textsearch') AND
116
+ (SELECT count(*) = 3 FROM pg_index i JOIN pg_class c ON c.oid=i.indexrelid JOIN pg_am a ON a.oid=c.relam
117
+ WHERE c.oid IN (to_regclass('help.help_bm25_english_idx'), to_regclass('help.help_bm25_german_idx'),
118
+ to_regclass('help.help_bm25_simple_idx')) AND i.indisvalid AND a.amname='bm25') AS ready`;
119
+ return row?.ready === true;
120
+ };
121
+
122
+ /** Locale precedence is applied before matching, ranking and limiting, including partial translations. */
123
+ export const queryHelp = async (
124
+ selection: HelpSelection[],
125
+ input: {
126
+ query?: string;
127
+ documentId?: string;
128
+ body?: boolean;
129
+ limit?: number;
130
+ cursor?: string;
131
+ bm25?: boolean;
132
+ },
133
+ db: SQL = sql,
134
+ ): Promise<Row[]> => {
135
+ if (!selection.length) return [];
136
+ const ranked = input.query !== undefined;
137
+ const nativeRank = `ts_rank_cd(d.search_document, websearch_to_tsquery(d.search_config::regconfig, $2))`;
138
+ const bm25Rank = `CASE d.search_config ${searchConfigs
139
+ .map((config) => `WHEN '${config}' THEN d.search_text <@> to_bm25query($2, 'help.help_bm25_${config}_idx')`)
140
+ .join(" ")} END`;
141
+ const matching = searchConfigs
142
+ .map((config) => `(d.search_config='${config}' AND d.search_document @@ websearch_to_tsquery('${config}', $2))`)
143
+ .join(" OR ");
144
+ const pending = db.unsafe<Row[]>(
145
+ `WITH active AS (
146
+ SELECT * FROM jsonb_to_recordset(($1::text)::jsonb) AS a(app_id text, app_name text, manifest_hash text, locales text[])
147
+ ) SELECT d.app_id, a.app_name, d.manifest_hash, d.locale, d.document_id, d.title, d.description, d.icon, d.sort_order
148
+ ${input.body ? ", d.markdown" : ""}
149
+ FROM help.documents d JOIN active a ON a.app_id=d.app_id AND a.manifest_hash=d.manifest_hash
150
+ WHERE length($2::text) >= 0 AND d.locale=ANY(a.locales)
151
+ AND NOT EXISTS (SELECT 1 FROM help.documents preferred
152
+ WHERE preferred.app_id=d.app_id AND preferred.manifest_hash=d.manifest_hash AND preferred.document_id=d.document_id
153
+ AND preferred.locale=ANY(a.locales) AND array_position(a.locales, preferred.locale)<array_position(a.locales, d.locale))
154
+ AND ($3::text IS NULL OR d.document_id=$3)
155
+ AND ($4::text IS NULL OR ('cloud://help/' || d.app_id || '/' || d.document_id) COLLATE "C" > $4)
156
+ ${ranked ? `AND (lower(d.document_id)=lower($2) OR lower(d.title)=lower($2) OR ${matching})` : ""}
157
+ ORDER BY ${
158
+ ranked
159
+ ? `(lower(d.document_id)=lower($2) OR lower(d.title)=lower($2)) DESC,
160
+ ${input.bm25 ? bm25Rank + " ASC," : ""} ${nativeRank} DESC,`
161
+ : ""
162
+ }
163
+ d.app_id COLLATE "C", ${input.cursor !== undefined ? "" : "d.sort_order,"} d.document_id COLLATE "C"
164
+ ${input.limit !== undefined ? "LIMIT $5" : ""}`,
165
+ [
166
+ JSON.stringify(selection),
167
+ input.query ?? "",
168
+ input.documentId ?? null,
169
+ input.cursor ?? null,
170
+ ...(input.limit !== undefined ? [input.limit] : []),
171
+ ],
172
+ );
173
+ // Match the app publication write deadline and bound abandoned reads at the driver.
174
+ const timeout = setTimeout(() => pending.cancel(), 10_000);
175
+ try {
176
+ return await pending;
177
+ } finally {
178
+ clearTimeout(timeout);
179
+ }
180
+ };
@@ -0,0 +1,46 @@
1
+ import type { AppRegistryHelpSummary } from "../../contracts/registry";
2
+ import type { HelpDocumentManifest } from "../../shared/help";
3
+
4
+ export type HelpDocument = {
5
+ id: string;
6
+ title: string;
7
+ icon?: string;
8
+ description?: string;
9
+ order: number;
10
+ markdown: string;
11
+ searchText: string;
12
+ };
13
+
14
+ export type HelpCorpus = {
15
+ appId: string;
16
+ manifestHash: string;
17
+ baseLocale: string;
18
+ documents: readonly HelpDocument[];
19
+ documentsByLocale: Readonly<Record<string, readonly HelpDocument[]>>;
20
+ };
21
+
22
+ export type HelpMetadata = {
23
+ appId: string;
24
+ appName: string;
25
+ manifestHash: string;
26
+ locale: string;
27
+ documentId: string;
28
+ title: string;
29
+ description?: string;
30
+ icon?: string;
31
+ order: number;
32
+ };
33
+ export type HelpArticle = HelpMetadata & { markdown: string };
34
+ export type HelpMatch = Pick<HelpMetadata, "appId" | "appName" | "locale" | "documentId" | "title" | "description"> & { kind: "help" };
35
+ export type HelpSearchInput = { query: string; appId?: string; limit?: number };
36
+ export type HelpReadInput = { appId: string; documentId: string };
37
+ export type HelpManifestResult = AppRegistryHelpSummary & { locale: string; documents: readonly HelpDocumentManifest[] };
38
+
39
+ /** One request's language; operations resolve the current published app versions. */
40
+ export type HelpReader = {
41
+ manifest(appId: string): Promise<HelpManifestResult | null>;
42
+ list(cursor?: string): Promise<HelpMetadata[]>;
43
+ search(input: HelpSearchInput): Promise<HelpMatch[]>;
44
+ read(input: HelpReadInput): Promise<HelpArticle | null>;
45
+ };
46
+ export type HelpReaderFactory = (locale: string) => HelpReader;
@@ -4,7 +4,7 @@ import type { AccessSubject } from "../../server/services/access";
4
4
  import { isAccountCategoryAllowed } from "../account-category-policy";
5
5
  import { isAccountExpired } from "../account-model";
6
6
  import type { ServiceAccount } from "../service-accounts";
7
- import { buildProjectedUser, loadCurrentUser, userProjectionSql } from "../session/user";
7
+ import { buildProjectedUser, loadCurrentUser, userProjectionJoin, userProjectionSql } from "../session/user";
8
8
  import type { CloudInvocationClaims } from "./invocation-token";
9
9
  import { getIdentityRuntimeConfig } from "./runtime-config";
10
10
 
@@ -92,7 +92,7 @@ export const resolveInvocationAuthority = async (
92
92
  ${userProjectionSql(groupsAdmin)}
93
93
  FROM auth.service_accounts sa
94
94
  LEFT JOIN auth.users u ON u.id = sa.delegated_user_id
95
- LEFT JOIN auth.user_ipa_data ui ON ui.user_id = u.id
95
+ ${userProjectionJoin()}
96
96
  WHERE sa.id = ${claims.sub}::uuid
97
97
  AND sa.status = 'active'
98
98
  `;
@@ -1,4 +1,6 @@
1
1
  import { crypto } from "@k2b/stdlib";
2
+ import { getProcessApplicationId } from "../../_internal/process-identity";
3
+ import { env } from "../../config/env";
2
4
 
3
5
  const KEY_PATTERN = /^[0-9a-f]{64}$/i;
4
6
 
@@ -23,12 +25,12 @@ const identify = async (key: string): Promise<{ id: string; key: string }> => ({
23
25
  });
24
26
 
25
27
  export const readIdentityKeyEncryptionConfig = async (): Promise<IdentityKeyEncryptionConfig> => {
26
- if ((process.env.APP_ID ?? "").trim() !== "core") {
28
+ if (getProcessApplicationId() !== "core") {
27
29
  throw new Error("Private Cloud identity keys may only be loaded by the Core application");
28
30
  }
29
- const current = parseKey("CLOUD_IDENTITY_KEY_ENCRYPTION_KEY", process.env.CLOUD_IDENTITY_KEY_ENCRYPTION_KEY, true)!;
30
- const previous = parseKey("CLOUD_IDENTITY_PREVIOUS_KEY", process.env.CLOUD_IDENTITY_PREVIOUS_KEY, false);
31
- const next = parseKey("CLOUD_IDENTITY_NEXT_KEY", process.env.CLOUD_IDENTITY_NEXT_KEY, false);
31
+ const current = parseKey("CLOUD_IDENTITY_KEY_ENCRYPTION_KEY", env.CLOUD_IDENTITY_KEY_ENCRYPTION_KEY, true)!;
32
+ const previous = parseKey("CLOUD_IDENTITY_PREVIOUS_KEY", env.CLOUD_IDENTITY_PREVIOUS_KEY, false);
33
+ const next = parseKey("CLOUD_IDENTITY_NEXT_KEY", env.CLOUD_IDENTITY_NEXT_KEY, false);
32
34
  const currentIdentified = await identify(current);
33
35
  const previousIdentified = previous ? await identify(previous) : null;
34
36
  const nextIdentified = next ? await identify(next) : null;
@@ -36,6 +38,7 @@ export const readIdentityKeyEncryptionConfig = async (): Promise<IdentityKeyEncr
36
38
  if (new Set(ids).size !== ids.length) {
37
39
  throw new Error("Configured Cloud identity KEKs must be distinct");
38
40
  }
39
- if (previousIdentified && nextIdentified) throw new Error("Configure either CLOUD_IDENTITY_PREVIOUS_KEY or CLOUD_IDENTITY_NEXT_KEY, not both");
41
+ if (previousIdentified && nextIdentified)
42
+ throw new Error("Configure either CLOUD_IDENTITY_PREVIOUS_KEY or CLOUD_IDENTITY_NEXT_KEY, not both");
40
43
  return { current: currentIdentified, previous: previousIdentified, next: nextIdentified };
41
44
  };
@@ -181,7 +181,7 @@ const insertKey = async (
181
181
  purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
182
182
  created_at, activate_at, activated_at, sign_until, verify_until
183
183
  ) VALUES (
184
- ${purpose}, ${state}, ${generated.kid}, ${CLOUD_IDENTITY_ALGORITHM}, ${JSON.stringify(generated.publicJwk)}::jsonb,
184
+ ${purpose}, ${state}, ${generated.kid}, ${CLOUD_IDENTITY_ALGORITHM}, ${JSON.stringify(generated.publicJwk)}::text::jsonb,
185
185
  ${generated.encryptedPrivateJwk}, ${generated.encryptionKeyId}, ${now}, ${activateAt},
186
186
  ${state === "active" ? now : null}, ${signUntil}, ${verifyUntil}
187
187
  )
@@ -1,3 +1,4 @@
1
+ import { env } from "../../config/env";
1
2
  import { publicCloudOrigin } from "../../shared/app-url";
2
3
  import { get as getSetting } from "../settings";
3
4
  import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_OAUTH_JWKS_PATH, CLOUD_SESSION_JWKS_PATH } from "./constants";
@@ -28,7 +29,7 @@ const normalizeGroups = (value: unknown): string[] => {
28
29
  return groups.length > 0 ? groups : ["admins"];
29
30
  };
30
31
 
31
- const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL => {
32
+ const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL => {
32
33
  if (!transportOrigin) return new URL(path, issuer);
33
34
  const origin = new URL(transportOrigin);
34
35
  if (origin.protocol !== "http:" && origin.protocol !== "https:") {
@@ -37,13 +38,13 @@ const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = proc
37
38
  return new URL(path, origin);
38
39
  };
39
40
 
40
- export const resolveSessionJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
41
+ export const resolveSessionJwksUrl = (issuer: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL =>
41
42
  resolveCoreJwksUrl(issuer, CLOUD_SESSION_JWKS_PATH, transportOrigin);
42
43
 
43
- export const resolveInvocationJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
44
+ export const resolveInvocationJwksUrl = (issuer: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL =>
44
45
  resolveCoreJwksUrl(issuer, CLOUD_INVOCATION_JWKS_PATH, transportOrigin);
45
46
 
46
- export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_OAUTH_JWKS_ORIGIN?.trim()): URL => {
47
+ export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = env.CLOUD_OAUTH_JWKS_ORIGIN): URL => {
47
48
  if (!transportOrigin) return new URL(CLOUD_OAUTH_JWKS_PATH, issuer);
48
49
 
49
50
  const url = new URL(CLOUD_OAUTH_JWKS_PATH, transportOrigin);
@@ -56,7 +57,7 @@ export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = process.en
56
57
  const load = async (): Promise<IdentityRuntimeConfig> => {
57
58
  const [appUrl, rawGroupsAdmin] = await Promise.all([getSetting<string>("app.url"), getSetting<unknown>("freeipa.groups.admin")]);
58
59
  const issuer = publicCloudOrigin(appUrl);
59
- if (process.env.NODE_ENV === "production" && new URL(issuer).protocol !== "https:") {
60
+ if (env.NODE_ENV === "production" && new URL(issuer).protocol !== "https:") {
60
61
  throw new Error("Cloud identity issuer must use HTTPS in production");
61
62
  }
62
63
  return {
@@ -4,7 +4,11 @@ export { accounts } from "./accounts";
4
4
  export { accountsAppService } from "./accounts";
5
5
  export { posix as linuxIdentities, PosixError } from "./accounts/posix";
6
6
  export type { PosixCandidate } from "./accounts/posix";
7
+ export { accountIdentities, AccountIdentityError } from "./accounts/identities";
8
+ export type { AccountIdentityUser, AccountIdentityGroup, AccountIdentityPage, AccountIdentityAvailability } from "./accounts/identities";
9
+ export type { AccountIdentityReconciliation } from "./accounts/identity-reconciliation";
7
10
  export { providers } from "./providers";
11
+ export { requestPublicHttps, type PublicHttpInput } from "./public-http";
8
12
  export { authFlows } from "./auth-flows";
9
13
  export { toPgIntArray, toPgTextArray, toPgUuidArray, escapeLikePattern, isUniqueViolation } from "./postgres";
10
14
 
@@ -144,6 +148,10 @@ export {
144
148
  MarkdownPdfError,
145
149
  buildMarkdownPdfHtml,
146
150
  getGotenbergConfig,
151
+ attachPdfFiles,
152
+ attachPdfFilesWithConfig,
153
+ renderFacturXHtmlToPdf,
154
+ renderFacturXHtmlToPdfWithConfig,
147
155
  mergePdfs,
148
156
  mergePdfsWithConfig,
149
157
  renderMarkdownToPdf,
@@ -155,6 +163,8 @@ export {
155
163
  } from "./pdf";
156
164
  export type {
157
165
  GotenbergConfig,
166
+ AttachPdfFilesInput,
167
+ RenderFacturXHtmlToPdfInput,
158
168
  GotenbergRenderErrorCode,
159
169
  MarkdownPdfErrorCode,
160
170
  MarkdownPdfTemplateId,
@@ -185,3 +195,8 @@ export { latestTopicCursor } from "./topic-cursor";
185
195
  export { readAccountCategoryPolicy, isAccountCategoryAllowed } from "./account-category-policy";
186
196
  export { appApproval, type AppDeviceEnrollmentNotice } from "./app-approval";
187
197
  export { legalConsent } from "./legal-consent";
198
+
199
+ /** Core-owned app bar administration; service methods enforce administrator access. */
200
+ export { railShortcuts } from "./rail-shortcuts";
201
+
202
+ export type { WebVitalsOverview, WebVitalsQuery, WebVitalMetric, WebVitalName } from "./logging/web-vitals";
@@ -1,4 +1,4 @@
1
- import { sql } from "bun";
1
+ import type { sql } from "bun";
2
2
 
3
3
  /** Mirror directory-owned attributes, including missing values; never apply local defaults. */
4
4
  export const mirrorIpaPosix = async (
@@ -6,9 +6,9 @@ import { applyIpaAccountTransitionPolicy } from "../accounts/switching";
6
6
  import { getFreeIpaConfig } from "../freeipa-config";
7
7
  import { logger } from "../logging";
8
8
  import { session } from "../session";
9
- import { mirrorIpaPosix } from "./posix";
10
9
  import * as settings from "../settings";
11
10
  import { buildEffectiveIpaGroupsByUid } from "./effective-groups";
11
+ import { mirrorIpaPosix } from "./posix";
12
12
  import { calculateIpaProfileFromEffectiveProjection, getEffectiveUserGroups } from "./profile";
13
13
  import { createIpaRuntimeCheckpoint, type IpaSyncRuntime } from "./runtime";
14
14
  import { assessIpaDestructionGuard, formatIpaDestructionGuardFailure, readCompleteIpaList, selectStaleLocalIpaRows } from "./sync-planning";