@data-club/ai-hub-server 0.0.1

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 (248) hide show
  1. package/dist/auth/argon2.d.ts +72 -0
  2. package/dist/auth/argon2.d.ts.map +1 -0
  3. package/dist/auth/argon2.js +145 -0
  4. package/dist/auth/argon2.js.map +1 -0
  5. package/dist/auth/encryption.d.ts +52 -0
  6. package/dist/auth/encryption.d.ts.map +1 -0
  7. package/dist/auth/encryption.js +98 -0
  8. package/dist/auth/encryption.js.map +1 -0
  9. package/dist/auth/jwt.d.ts +133 -0
  10. package/dist/auth/jwt.d.ts.map +1 -0
  11. package/dist/auth/jwt.js +147 -0
  12. package/dist/auth/jwt.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +101 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +161 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/passwordPolicy.d.ts +72 -0
  18. package/dist/auth/passwordPolicy.d.ts.map +1 -0
  19. package/dist/auth/passwordPolicy.js +88 -0
  20. package/dist/auth/passwordPolicy.js.map +1 -0
  21. package/dist/auth/roleRules.d.ts +200 -0
  22. package/dist/auth/roleRules.d.ts.map +1 -0
  23. package/dist/auth/roleRules.js +365 -0
  24. package/dist/auth/roleRules.js.map +1 -0
  25. package/dist/auth/userView.d.ts +55 -0
  26. package/dist/auth/userView.d.ts.map +1 -0
  27. package/dist/auth/userView.js +77 -0
  28. package/dist/auth/userView.js.map +1 -0
  29. package/dist/bootstrap.d.ts +104 -0
  30. package/dist/bootstrap.d.ts.map +1 -0
  31. package/dist/bootstrap.js +160 -0
  32. package/dist/bootstrap.js.map +1 -0
  33. package/dist/db/connection.d.ts +39 -0
  34. package/dist/db/connection.d.ts.map +1 -0
  35. package/dist/db/connection.js +62 -0
  36. package/dist/db/connection.js.map +1 -0
  37. package/dist/db/migrate.d.ts +31 -0
  38. package/dist/db/migrate.d.ts.map +1 -0
  39. package/dist/db/migrate.js +197 -0
  40. package/dist/db/migrate.js.map +1 -0
  41. package/dist/db/schema.d.ts +1902 -0
  42. package/dist/db/schema.d.ts.map +1 -0
  43. package/dist/db/schema.js +662 -0
  44. package/dist/db/schema.js.map +1 -0
  45. package/dist/db/seed.d.ts +92 -0
  46. package/dist/db/seed.d.ts.map +1 -0
  47. package/dist/db/seed.js +160 -0
  48. package/dist/db/seed.js.map +1 -0
  49. package/dist/handlers/fetch.d.ts +89 -0
  50. package/dist/handlers/fetch.d.ts.map +1 -0
  51. package/dist/handlers/fetch.js +173 -0
  52. package/dist/handlers/fetch.js.map +1 -0
  53. package/dist/handlers/nextjs.d.ts +40 -0
  54. package/dist/handlers/nextjs.d.ts.map +1 -0
  55. package/dist/handlers/nextjs.js +42 -0
  56. package/dist/handlers/nextjs.js.map +1 -0
  57. package/dist/handlers/responses.d.ts +46 -0
  58. package/dist/handlers/responses.d.ts.map +1 -0
  59. package/dist/handlers/responses.js +74 -0
  60. package/dist/handlers/responses.js.map +1 -0
  61. package/dist/handlers/routeTable.d.ts +21 -0
  62. package/dist/handlers/routeTable.d.ts.map +1 -0
  63. package/dist/handlers/routeTable.js +133 -0
  64. package/dist/handlers/routeTable.js.map +1 -0
  65. package/dist/handlers/router.d.ts +37 -0
  66. package/dist/handlers/router.d.ts.map +1 -0
  67. package/dist/handlers/router.js +82 -0
  68. package/dist/handlers/router.js.map +1 -0
  69. package/dist/handlers/sse.d.ts +38 -0
  70. package/dist/handlers/sse.d.ts.map +1 -0
  71. package/dist/handlers/sse.js +52 -0
  72. package/dist/handlers/sse.js.map +1 -0
  73. package/dist/handlers/types.d.ts +96 -0
  74. package/dist/handlers/types.d.ts.map +1 -0
  75. package/dist/handlers/types.js +14 -0
  76. package/dist/handlers/types.js.map +1 -0
  77. package/dist/index.d.ts +25 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +24 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/letta/client.d.ts +355 -0
  82. package/dist/letta/client.d.ts.map +1 -0
  83. package/dist/letta/client.js +529 -0
  84. package/dist/letta/client.js.map +1 -0
  85. package/dist/letta/eventTranslator.d.ts +166 -0
  86. package/dist/letta/eventTranslator.d.ts.map +1 -0
  87. package/dist/letta/eventTranslator.js +532 -0
  88. package/dist/letta/eventTranslator.js.map +1 -0
  89. package/dist/letta/index.d.ts +13 -0
  90. package/dist/letta/index.d.ts.map +1 -0
  91. package/dist/letta/index.js +12 -0
  92. package/dist/letta/index.js.map +1 -0
  93. package/dist/letta/modelSettings.d.ts +176 -0
  94. package/dist/letta/modelSettings.d.ts.map +1 -0
  95. package/dist/letta/modelSettings.js +314 -0
  96. package/dist/letta/modelSettings.js.map +1 -0
  97. package/dist/letta/promptWrapping.d.ts +141 -0
  98. package/dist/letta/promptWrapping.d.ts.map +1 -0
  99. package/dist/letta/promptWrapping.js +213 -0
  100. package/dist/letta/promptWrapping.js.map +1 -0
  101. package/dist/letta/sandbox.d.ts +100 -0
  102. package/dist/letta/sandbox.d.ts.map +1 -0
  103. package/dist/letta/sandbox.js +151 -0
  104. package/dist/letta/sandbox.js.map +1 -0
  105. package/dist/letta/settingsFileSeed.d.ts +146 -0
  106. package/dist/letta/settingsFileSeed.d.ts.map +1 -0
  107. package/dist/letta/settingsFileSeed.js +177 -0
  108. package/dist/letta/settingsFileSeed.js.map +1 -0
  109. package/dist/letta/subprocessThrottle.d.ts +151 -0
  110. package/dist/letta/subprocessThrottle.d.ts.map +1 -0
  111. package/dist/letta/subprocessThrottle.js +195 -0
  112. package/dist/letta/subprocessThrottle.js.map +1 -0
  113. package/dist/letta/tpmRecovery.d.ts +202 -0
  114. package/dist/letta/tpmRecovery.d.ts.map +1 -0
  115. package/dist/letta/tpmRecovery.js +314 -0
  116. package/dist/letta/tpmRecovery.js.map +1 -0
  117. package/dist/letta/types.d.ts +54 -0
  118. package/dist/letta/types.d.ts.map +1 -0
  119. package/dist/letta/types.js +16 -0
  120. package/dist/letta/types.js.map +1 -0
  121. package/dist/observability/logger.d.ts +48 -0
  122. package/dist/observability/logger.d.ts.map +1 -0
  123. package/dist/observability/logger.js +81 -0
  124. package/dist/observability/logger.js.map +1 -0
  125. package/dist/observability/metrics.d.ts +53 -0
  126. package/dist/observability/metrics.d.ts.map +1 -0
  127. package/dist/observability/metrics.js +92 -0
  128. package/dist/observability/metrics.js.map +1 -0
  129. package/dist/routes/admin/agents.d.ts +63 -0
  130. package/dist/routes/admin/agents.d.ts.map +1 -0
  131. package/dist/routes/admin/agents.js +461 -0
  132. package/dist/routes/admin/agents.js.map +1 -0
  133. package/dist/routes/admin/rotatingMessages.d.ts +51 -0
  134. package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
  135. package/dist/routes/admin/rotatingMessages.js +292 -0
  136. package/dist/routes/admin/rotatingMessages.js.map +1 -0
  137. package/dist/routes/admin/sessions.d.ts +61 -0
  138. package/dist/routes/admin/sessions.d.ts.map +1 -0
  139. package/dist/routes/admin/sessions.js +232 -0
  140. package/dist/routes/admin/sessions.js.map +1 -0
  141. package/dist/routes/admin/settings.d.ts +110 -0
  142. package/dist/routes/admin/settings.d.ts.map +1 -0
  143. package/dist/routes/admin/settings.js +401 -0
  144. package/dist/routes/admin/settings.js.map +1 -0
  145. package/dist/routes/admin/users.d.ts +115 -0
  146. package/dist/routes/admin/users.d.ts.map +1 -0
  147. package/dist/routes/admin/users.js +374 -0
  148. package/dist/routes/admin/users.js.map +1 -0
  149. package/dist/routes/agents.d.ts +30 -0
  150. package/dist/routes/agents.d.ts.map +1 -0
  151. package/dist/routes/agents.js +55 -0
  152. package/dist/routes/agents.js.map +1 -0
  153. package/dist/routes/auth.d.ts +121 -0
  154. package/dist/routes/auth.d.ts.map +1 -0
  155. package/dist/routes/auth.js +422 -0
  156. package/dist/routes/auth.js.map +1 -0
  157. package/dist/routes/chat.d.ts +97 -0
  158. package/dist/routes/chat.d.ts.map +1 -0
  159. package/dist/routes/chat.js +484 -0
  160. package/dist/routes/chat.js.map +1 -0
  161. package/dist/routes/featureToggles.d.ts +34 -0
  162. package/dist/routes/featureToggles.d.ts.map +1 -0
  163. package/dist/routes/featureToggles.js +47 -0
  164. package/dist/routes/featureToggles.js.map +1 -0
  165. package/dist/routes/health.d.ts +24 -0
  166. package/dist/routes/health.d.ts.map +1 -0
  167. package/dist/routes/health.js +37 -0
  168. package/dist/routes/health.js.map +1 -0
  169. package/dist/routes/metrics.d.ts +35 -0
  170. package/dist/routes/metrics.d.ts.map +1 -0
  171. package/dist/routes/metrics.js +106 -0
  172. package/dist/routes/metrics.js.map +1 -0
  173. package/dist/routes/sessions.d.ts +161 -0
  174. package/dist/routes/sessions.d.ts.map +1 -0
  175. package/dist/routes/sessions.js +1501 -0
  176. package/dist/routes/sessions.js.map +1 -0
  177. package/dist/routes/stubs.d.ts +25 -0
  178. package/dist/routes/stubs.d.ts.map +1 -0
  179. package/dist/routes/stubs.js +25 -0
  180. package/dist/routes/stubs.js.map +1 -0
  181. package/dist/state/cron.d.ts +58 -0
  182. package/dist/state/cron.d.ts.map +1 -0
  183. package/dist/state/cron.js +160 -0
  184. package/dist/state/cron.js.map +1 -0
  185. package/dist/state/rotationRunner.d.ts +96 -0
  186. package/dist/state/rotationRunner.d.ts.map +1 -0
  187. package/dist/state/rotationRunner.js +246 -0
  188. package/dist/state/rotationRunner.js.map +1 -0
  189. package/dist/state/rotationScheduler.d.ts +76 -0
  190. package/dist/state/rotationScheduler.d.ts.map +1 -0
  191. package/dist/state/rotationScheduler.js +124 -0
  192. package/dist/state/rotationScheduler.js.map +1 -0
  193. package/dist/state/turnRegistry.d.ts +95 -0
  194. package/dist/state/turnRegistry.d.ts.map +1 -0
  195. package/dist/state/turnRegistry.js +149 -0
  196. package/dist/state/turnRegistry.js.map +1 -0
  197. package/dist/state/turnRunner.d.ts +134 -0
  198. package/dist/state/turnRunner.d.ts.map +1 -0
  199. package/dist/state/turnRunner.js +476 -0
  200. package/dist/state/turnRunner.js.map +1 -0
  201. package/package.json +47 -0
  202. package/src/auth/argon2.ts +152 -0
  203. package/src/auth/encryption.ts +110 -0
  204. package/src/auth/jwt.ts +217 -0
  205. package/src/auth/middleware.ts +250 -0
  206. package/src/auth/passwordPolicy.ts +99 -0
  207. package/src/auth/roleRules.ts +497 -0
  208. package/src/auth/userView.ts +88 -0
  209. package/src/bootstrap.ts +254 -0
  210. package/src/db/connection.ts +75 -0
  211. package/src/db/migrate.ts +212 -0
  212. package/src/db/schema.ts +750 -0
  213. package/src/db/seed.ts +236 -0
  214. package/src/handlers/fetch.ts +251 -0
  215. package/src/handlers/nextjs.ts +49 -0
  216. package/src/handlers/responses.ts +95 -0
  217. package/src/handlers/routeTable.ts +188 -0
  218. package/src/handlers/router.ts +96 -0
  219. package/src/handlers/sse.ts +55 -0
  220. package/src/handlers/types.ts +104 -0
  221. package/src/index.ts +60 -0
  222. package/src/letta/client.ts +860 -0
  223. package/src/letta/eventTranslator.ts +667 -0
  224. package/src/letta/index.ts +46 -0
  225. package/src/letta/lettaCliShim.mjs +40 -0
  226. package/src/letta/sandbox.ts +167 -0
  227. package/src/letta/settingsFileSeed.ts +254 -0
  228. package/src/letta/subprocessThrottle.ts +251 -0
  229. package/src/letta/types.ts +65 -0
  230. package/src/observability/logger.ts +103 -0
  231. package/src/observability/metrics.ts +114 -0
  232. package/src/routes/admin/agents.ts +574 -0
  233. package/src/routes/admin/rotatingMessages.ts +351 -0
  234. package/src/routes/admin/sessions.ts +283 -0
  235. package/src/routes/admin/settings.ts +463 -0
  236. package/src/routes/admin/users.ts +455 -0
  237. package/src/routes/agents.ts +66 -0
  238. package/src/routes/auth.ts +513 -0
  239. package/src/routes/chat.ts +534 -0
  240. package/src/routes/featureToggles.ts +52 -0
  241. package/src/routes/health.ts +44 -0
  242. package/src/routes/metrics.ts +111 -0
  243. package/src/routes/sessions.ts +1878 -0
  244. package/src/state/cron.ts +179 -0
  245. package/src/state/rotationRunner.ts +326 -0
  246. package/src/state/rotationScheduler.ts +164 -0
  247. package/src/state/turnRegistry.ts +210 -0
  248. package/src/state/turnRunner.ts +588 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `/admin/settings/*` — super_admin-only deployment-wide configuration.
3
+ *
4
+ * Five endpoints:
5
+ *
6
+ * - `GET /admin/settings` — Letta connection (api key **masked**), theme,
7
+ * metrics + signing-key rotation timestamps. Encrypted Letta API key
8
+ * surfaces only as `letta_api_key_masked` (`••••••••<last4>`); plaintext
9
+ * never leaves the server outside the `Reveal once` flow (Step 12).
10
+ * - `PATCH /admin/settings` — body `{ letta_base_url?, letta_ade_url?, letta_api_key?, theme?: { default?, switcher_enabled? } }`.
11
+ * API key, when supplied, is encrypted under `AI_HUB_ENCRYPTION_KEY`. Theme
12
+ * fields live as **separate columns**, not in `feature_toggles`.
13
+ * - `GET /admin/settings/feature-toggles` — flat JSON blob from §8.4.
14
+ * - `PATCH /admin/settings/feature-toggles` — merge-patch the same blob.
15
+ * Unknown keys are accepted (forward-compat for future toggles).
16
+ * - `POST /admin/settings/rotate-signing-key` — replace
17
+ * `service_token_signing_key` with random 64 bytes; bump
18
+ * `signing_key_rotated_at`. Returns nothing — the key never leaves the
19
+ * server. After rotation every outstanding JWT fails signature
20
+ * verification (no grace period in v1; the calling super-admin is logged
21
+ * out alongside everyone else).
22
+ * - `POST /admin/settings/rotate-metrics-token` — generate a new 32-byte
23
+ * hex token, bump `metrics_scrape_token_rotated_at`, return
24
+ * `{ token: "<new>" }` exactly once in the response. Same one-time-reveal
25
+ * pattern as user password reset; subsequent `GET /admin/settings` calls
26
+ * do not echo it back.
27
+ *
28
+ * @see SPECIFICATIONS.md §6.3.4 — settings endpoints
29
+ * @see SPECIFICATIONS.md §6.7 — signing key rotation
30
+ * @see SPECIFICATIONS.md §6.10 — metrics token rotation
31
+ * @see ui-sketches/08-admin-settings.md
32
+ */
33
+ import type { RouteHandler } from "../../handlers/types.js";
34
+ /**
35
+ * `GET /admin/settings` — super_admin-only deployment configuration.
36
+ *
37
+ * Encrypted Letta API key surfaces only as the masked placeholder
38
+ * (`letta_api_key_masked: "••••••••"`) + the boolean `letta_api_key_set`;
39
+ * plaintext never leaves the server through this endpoint. Step 12 wires the
40
+ * `Reveal once` flow as a separate UI affordance.
41
+ *
42
+ * @see SPECIFICATIONS.md §6.3.4
43
+ * @see ui-sketches/08-admin-settings.md
44
+ */
45
+ export declare const handleGetSettings: RouteHandler;
46
+ /**
47
+ * `PATCH /admin/settings` — partial update of deployment configuration.
48
+ *
49
+ * The Letta API key, when supplied, is encrypted under
50
+ * `AI_HUB_ENCRYPTION_KEY` (env-only) before write. Theme fields live as
51
+ * separate columns (`theme_default`, `theme_switcher_enabled`) per §8.4.
52
+ *
53
+ * @see SPECIFICATIONS.md §6.3.4
54
+ * @see SPECIFICATIONS.md §8.4 — theme settings are explicit columns
55
+ */
56
+ export declare const handlePatchSettings: RouteHandler;
57
+ /**
58
+ * `GET /admin/settings/feature-toggles` — the §8.4 flat JSON blob.
59
+ *
60
+ * Missing keys fall back to the React side's built-in defaults at render
61
+ * time; the server doesn't merge defaults in here (so the admin UI can show
62
+ * "default" vs. "explicitly set").
63
+ *
64
+ * @see SPECIFICATIONS.md §8.4
65
+ */
66
+ export declare const handleGetFeatureToggles: RouteHandler;
67
+ /**
68
+ * `PATCH /admin/settings/feature-toggles` — merge-patch the §8.4 blob.
69
+ *
70
+ * Unknown keys → 400. Each successful PATCH replaces the entire stored blob
71
+ * with the merge of (old state) + (incoming patch). Setting a key to `false`
72
+ * makes it explicit-false; omitting a key leaves the current value.
73
+ *
74
+ * @see SPECIFICATIONS.md §8.4
75
+ */
76
+ export declare const handlePatchFeatureToggles: RouteHandler;
77
+ /**
78
+ * `POST /admin/settings/rotate-signing-key` — replace the JWT signing key.
79
+ *
80
+ * Generates 64 new random bytes (matches seed bytes), writes to
81
+ * `admin_setting.service_token_signing_key`, bumps `signing_key_rotated_at`.
82
+ * Returns 204 No Content — the key never leaves the server.
83
+ *
84
+ * **Every outstanding JWT fails signature verification immediately after this
85
+ * call** (no grace period in v1). The calling super-admin's own token is
86
+ * invalidated alongside everyone else's; the React UI handles the 401 on the
87
+ * next request by routing back to login.
88
+ *
89
+ * @see SPECIFICATIONS.md §6.7 — signing key
90
+ * @see ui-sketches/08-admin-settings.md
91
+ */
92
+ export declare const handleRotateSigningKey: RouteHandler;
93
+ /**
94
+ * `POST /admin/settings/rotate-metrics-token` — replace the `/metrics` bearer
95
+ * token.
96
+ *
97
+ * Generates a fresh 32-byte hex token, writes it, bumps
98
+ * `metrics_scrape_token_rotated_at`, and returns `{ token: "<new>" }` exactly
99
+ * once. The plaintext never appears in subsequent `GET /admin/settings`
100
+ * responses (same one-time-reveal pattern as user password resets).
101
+ *
102
+ * Rotating does **not** invalidate the calling super-admin's JWT — only the
103
+ * metrics-scraper bearer changes. Operators paste the new value into their
104
+ * Prometheus / Alloy config; old scrapers start failing immediately.
105
+ *
106
+ * @see SPECIFICATIONS.md §6.10
107
+ * @see ui-sketches/08-admin-settings.md
108
+ */
109
+ export declare const handleRotateMetricsToken: RouteHandler;
110
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/routes/admin/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAgBH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AA2H5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAO/B,CAAC;AA6BF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAgDjC,CAAC;AAMF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,YAOrC,CAAC;AA8BF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,EAAE,YAuCvC,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,EAAE,YAsBpC,CAAC;AAMF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,EAAE,YAsBtC,CAAC"}
@@ -0,0 +1,401 @@
1
+ /**
2
+ * `/admin/settings/*` — super_admin-only deployment-wide configuration.
3
+ *
4
+ * Five endpoints:
5
+ *
6
+ * - `GET /admin/settings` — Letta connection (api key **masked**), theme,
7
+ * metrics + signing-key rotation timestamps. Encrypted Letta API key
8
+ * surfaces only as `letta_api_key_masked` (`••••••••<last4>`); plaintext
9
+ * never leaves the server outside the `Reveal once` flow (Step 12).
10
+ * - `PATCH /admin/settings` — body `{ letta_base_url?, letta_ade_url?, letta_api_key?, theme?: { default?, switcher_enabled? } }`.
11
+ * API key, when supplied, is encrypted under `AI_HUB_ENCRYPTION_KEY`. Theme
12
+ * fields live as **separate columns**, not in `feature_toggles`.
13
+ * - `GET /admin/settings/feature-toggles` — flat JSON blob from §8.4.
14
+ * - `PATCH /admin/settings/feature-toggles` — merge-patch the same blob.
15
+ * Unknown keys are accepted (forward-compat for future toggles).
16
+ * - `POST /admin/settings/rotate-signing-key` — replace
17
+ * `service_token_signing_key` with random 64 bytes; bump
18
+ * `signing_key_rotated_at`. Returns nothing — the key never leaves the
19
+ * server. After rotation every outstanding JWT fails signature
20
+ * verification (no grace period in v1; the calling super-admin is logged
21
+ * out alongside everyone else).
22
+ * - `POST /admin/settings/rotate-metrics-token` — generate a new 32-byte
23
+ * hex token, bump `metrics_scrape_token_rotated_at`, return
24
+ * `{ token: "<new>" }` exactly once in the response. Same one-time-reveal
25
+ * pattern as user password reset; subsequent `GET /admin/settings` calls
26
+ * do not echo it back.
27
+ *
28
+ * @see SPECIFICATIONS.md §6.3.4 — settings endpoints
29
+ * @see SPECIFICATIONS.md §6.7 — signing key rotation
30
+ * @see SPECIFICATIONS.md §6.10 — metrics token rotation
31
+ * @see ui-sketches/08-admin-settings.md
32
+ */
33
+ import { randomBytes } from "node:crypto";
34
+ import { eq } from "drizzle-orm";
35
+ import { z } from "zod";
36
+ import { encryptString } from "../../auth/encryption.js";
37
+ import { protectedRoute } from "../../auth/middleware.js";
38
+ import { adminSetting } from "../../db/schema.js";
39
+ import { internalError, jsonResponse, validationError, } from "../../handlers/responses.js";
40
+ /* ────────────────────────────────────────────────────────────────────────── */
41
+ /* Helpers */
42
+ /* ────────────────────────────────────────────────────────────────────────── */
43
+ /** ISO-8601 timestamp with millisecond precision. */
44
+ function nowIso() {
45
+ return new Date().toISOString();
46
+ }
47
+ /**
48
+ * Computes a privacy-preserving display of the encrypted Letta API key.
49
+ *
50
+ * Per [08-admin-settings.md], the UI shows `••••••••` while the value is in
51
+ * the field — operators reveal-once if they need to copy. The server's
52
+ * `GET /admin/settings` never returns plaintext.
53
+ *
54
+ * @param row - the admin_setting row.
55
+ * @returns a masked string, or `null` when no key is stored.
56
+ */
57
+ function maskLettaApiKey(row) {
58
+ if (!row.lettaApiKeyEncrypted)
59
+ return null;
60
+ // We can't decrypt without the key, and decrypting just to show "<last 4>"
61
+ // would defeat the purpose of encrypting at rest. Use a constant placeholder
62
+ // so the UI knows a key is configured without revealing length / contents.
63
+ return "••••••••";
64
+ }
65
+ /**
66
+ * Determines the *effective* source of the Letta API key at this moment.
67
+ * Mirrors `letta/client.ts:resolveApiKey` (DB → env → throw) so the admin
68
+ * UI can show the truth about which source the runtime will use, not just
69
+ * what's stored in the DB.
70
+ *
71
+ * Returning the SOURCE — not the value — keeps plaintext off the wire.
72
+ *
73
+ * @param row - the admin_setting row (may be missing).
74
+ * @param envSource - env-var record from the server context. Lets tests
75
+ * exercise the source-resolution without touching the real `process.env`.
76
+ * @returns `"db"` when DB column has a non-empty value (the runtime will
77
+ * use it); `"env"` when DB is empty AND `LETTA_API_KEY` in `envSource`
78
+ * is set and non-empty; `"none"` otherwise.
79
+ */
80
+ function resolveApiKeySource(row, envSource) {
81
+ // We deliberately can't tell here whether the DB-encrypted bytes decrypt
82
+ // to a non-whitespace string without doing the decrypt — but the rule is
83
+ // "non-empty bytes ⇒ candidate." The runtime resolver does the actual
84
+ // whitespace check; the UI source is just "is there a DB value worth
85
+ // showing?" The mismatch (DB has whitespace-only after decrypt) is rare
86
+ // enough that we don't pay the cost of decrypting here just to detect
87
+ // it; `letta_api_key_set` aligned with `lettaApiKeyEncrypted IS NOT NULL`
88
+ // matches the existing PATCH semantics (we never write whitespace).
89
+ if (row?.lettaApiKeyEncrypted)
90
+ return "db";
91
+ const envKey = envSource.LETTA_API_KEY;
92
+ if (envKey && envKey.trim().length > 0)
93
+ return "env";
94
+ return "none";
95
+ }
96
+ /**
97
+ * Reads the singleton admin-settings row. Throws if the row hasn't been
98
+ * seeded — should not happen after first-boot.
99
+ *
100
+ * @param ctx - minimal server-context slice carrying the Drizzle client.
101
+ * @param ctx.db - the Drizzle client used for the read.
102
+ * @returns the row, or `null` if absent.
103
+ */
104
+ function readSettings(ctx) {
105
+ return ctx.db.select().from(adminSetting).where(eq(adminSetting.id, 1)).get() ?? null;
106
+ }
107
+ /**
108
+ * Wire-format view of the settings row for `GET /admin/settings`.
109
+ *
110
+ * @param row - the admin_setting row.
111
+ * @returns the JSON-safe response shape.
112
+ */
113
+ function settingsView(row, envSource) {
114
+ const source = resolveApiKeySource(row, envSource);
115
+ return {
116
+ letta_base_url: row.lettaBaseUrl ?? null,
117
+ letta_ade_url: row.lettaAdeUrl ?? null,
118
+ letta_api_key_masked: maskLettaApiKey(row),
119
+ // True when EITHER the DB column is populated OR env.LETTA_API_KEY is
120
+ // set — matches the runtime resolver's "is the chat path going to
121
+ // work?" answer. The admin UI uses this to decide whether to render a
122
+ // "configured" badge vs. a "missing" warning, regardless of which
123
+ // source supplies the value.
124
+ letta_api_key_set: source !== "none",
125
+ // Source the runtime is *currently* using (per resolveApiKey precedence
126
+ // in letta/client.ts). Drives the admin UI's source-indicator text —
127
+ // "(from env)" vs "(from admin panel)" vs "(not configured)" — so
128
+ // operators don't get confused when the DB row is empty but chat
129
+ // still works (env fallback) or vice versa.
130
+ letta_api_key_source: source,
131
+ theme: {
132
+ default: row.themeDefault,
133
+ switcher_enabled: row.themeSwitcherEnabled,
134
+ },
135
+ feature_toggles: row.featureToggles,
136
+ signing_key_rotated_at: row.signingKeyRotatedAt ?? null,
137
+ metrics_scrape_token_rotated_at: row.metricsScrapeTokenRotatedAt ?? null,
138
+ updated_at: row.updatedAt,
139
+ // `metrics_scrape_token` and `service_token_signing_key` are deliberately
140
+ // omitted — see file-level comment.
141
+ };
142
+ }
143
+ /* ────────────────────────────────────────────────────────────────────────── */
144
+ /* GET /admin/settings */
145
+ /* ────────────────────────────────────────────────────────────────────────── */
146
+ /**
147
+ * `GET /admin/settings` — super_admin-only deployment configuration.
148
+ *
149
+ * Encrypted Letta API key surfaces only as the masked placeholder
150
+ * (`letta_api_key_masked: "••••••••"`) + the boolean `letta_api_key_set`;
151
+ * plaintext never leaves the server through this endpoint. Step 12 wires the
152
+ * `Reveal once` flow as a separate UI affordance.
153
+ *
154
+ * @see SPECIFICATIONS.md §6.3.4
155
+ * @see ui-sketches/08-admin-settings.md
156
+ */
157
+ export const handleGetSettings = protectedRoute({ requiredRole: "super_admin" }, async (_req, ctx) => {
158
+ const row = readSettings(ctx);
159
+ if (!row)
160
+ return internalError("Admin settings row is missing — server misconfigured.");
161
+ return jsonResponse(200, settingsView(row, ctx.envSource));
162
+ });
163
+ /* ────────────────────────────────────────────────────────────────────────── */
164
+ /* PATCH /admin/settings */
165
+ /* ────────────────────────────────────────────────────────────────────────── */
166
+ /** Body for `PATCH /admin/settings`. Every field optional; empty body is a no-op. */
167
+ const patchSettingsSchema = z
168
+ .object({
169
+ letta_base_url: z.string().url().nullable().optional(),
170
+ letta_ade_url: z.string().url().nullable().optional(),
171
+ // The API key accepts:
172
+ // - a non-empty string → encrypt + store; runtime will use this DB
173
+ // value over env on the next request.
174
+ // - an empty/whitespace string → clear the DB column to NULL; the
175
+ // runtime then falls back to env.LETTA_API_KEY (the operator-spec'd
176
+ // "if admin panel is empty, use env" precedence).
177
+ // - omitted → no change.
178
+ letta_api_key: z.string().max(2048).optional(),
179
+ theme: z
180
+ .object({
181
+ default: z.enum(["light", "dark"]).optional(),
182
+ switcher_enabled: z.boolean().optional(),
183
+ })
184
+ .strict()
185
+ .optional(),
186
+ })
187
+ .strict();
188
+ /**
189
+ * `PATCH /admin/settings` — partial update of deployment configuration.
190
+ *
191
+ * The Letta API key, when supplied, is encrypted under
192
+ * `AI_HUB_ENCRYPTION_KEY` (env-only) before write. Theme fields live as
193
+ * separate columns (`theme_default`, `theme_switcher_enabled`) per §8.4.
194
+ *
195
+ * @see SPECIFICATIONS.md §6.3.4
196
+ * @see SPECIFICATIONS.md §8.4 — theme settings are explicit columns
197
+ */
198
+ export const handlePatchSettings = protectedRoute({ requiredRole: "super_admin" }, async (req, ctx, auth) => {
199
+ let raw;
200
+ try {
201
+ raw = await req.json();
202
+ }
203
+ catch {
204
+ return validationError("Request body must be valid JSON.");
205
+ }
206
+ const parsed = patchSettingsSchema.safeParse(raw);
207
+ if (!parsed.success) {
208
+ return validationError(parsed.error.issues[0]?.message ?? "Invalid request body.");
209
+ }
210
+ const body = parsed.data;
211
+ const update = { updatedAt: nowIso() };
212
+ if (body.letta_base_url !== undefined)
213
+ update.lettaBaseUrl = body.letta_base_url;
214
+ if (body.letta_ade_url !== undefined)
215
+ update.lettaAdeUrl = body.letta_ade_url;
216
+ if (body.letta_api_key !== undefined) {
217
+ // Empty / whitespace-only string clears the DB column to NULL. The
218
+ // runtime then falls back to env.LETTA_API_KEY (see
219
+ // letta/client.ts:resolveApiKey). Non-empty values are encrypted +
220
+ // stored as the operator's override of env.
221
+ const trimmed = body.letta_api_key.trim();
222
+ update.lettaApiKeyEncrypted =
223
+ trimmed.length === 0 ? null : encryptString(ctx.encryptionKey, body.letta_api_key);
224
+ }
225
+ if (body.theme?.default !== undefined)
226
+ update.themeDefault = body.theme.default;
227
+ if (body.theme?.switcher_enabled !== undefined)
228
+ update.themeSwitcherEnabled = body.theme.switcher_enabled;
229
+ if (Object.keys(update).length > 1) {
230
+ // More than just `updatedAt` — write.
231
+ ctx.db.update(adminSetting).set(update).where(eq(adminSetting.id, 1)).run();
232
+ ctx.logger.info({
233
+ actor_id: auth.user.id,
234
+ patch_keys: Object.keys(body),
235
+ event: "admin_settings_patched",
236
+ }, "Admin updated deployment settings.");
237
+ }
238
+ const fresh = readSettings(ctx);
239
+ if (!fresh)
240
+ return internalError();
241
+ return jsonResponse(200, settingsView(fresh, ctx.envSource));
242
+ });
243
+ /* ────────────────────────────────────────────────────────────────────────── */
244
+ /* GET /admin/settings/feature-toggles */
245
+ /* ────────────────────────────────────────────────────────────────────────── */
246
+ /**
247
+ * `GET /admin/settings/feature-toggles` — the §8.4 flat JSON blob.
248
+ *
249
+ * Missing keys fall back to the React side's built-in defaults at render
250
+ * time; the server doesn't merge defaults in here (so the admin UI can show
251
+ * "default" vs. "explicitly set").
252
+ *
253
+ * @see SPECIFICATIONS.md §8.4
254
+ */
255
+ export const handleGetFeatureToggles = protectedRoute({ requiredRole: "super_admin" }, async (_req, ctx) => {
256
+ const row = readSettings(ctx);
257
+ if (!row)
258
+ return internalError();
259
+ return jsonResponse(200, { feature_toggles: row.featureToggles ?? {} });
260
+ });
261
+ /* ────────────────────────────────────────────────────────────────────────── */
262
+ /* PATCH /admin/settings/feature-toggles */
263
+ /* ────────────────────────────────────────────────────────────────────────── */
264
+ /**
265
+ * `PATCH /admin/settings/feature-toggles` body. Strict — only known toggles
266
+ * are accepted. **Adding a new toggle requires bumping this schema** so
267
+ * accidental typos in the UI surface as 400s instead of silently being saved
268
+ * into the blob.
269
+ */
270
+ const featureTogglesSchema = z
271
+ .object({
272
+ sessions_panel_enabled: z.boolean().optional(),
273
+ sessions_panel_resizable: z.boolean().optional(),
274
+ sessions_panel_collapsible: z.boolean().optional(),
275
+ create_session_button_enabled: z.boolean().optional(),
276
+ user_input_enabled: z.boolean().optional(),
277
+ stop_button_enabled: z.boolean().optional(),
278
+ tool_calls_visible: z.boolean().optional(),
279
+ reasoning_visible: z.boolean().optional(),
280
+ system_messages_visible: z.boolean().optional(),
281
+ tpm_notice_visible: z.boolean().optional(),
282
+ effort_control_visible: z.boolean().optional(),
283
+ scroll_to_present_visible: z.boolean().optional(),
284
+ turn_status_visible: z.boolean().optional(),
285
+ })
286
+ .strict();
287
+ /**
288
+ * `PATCH /admin/settings/feature-toggles` — merge-patch the §8.4 blob.
289
+ *
290
+ * Unknown keys → 400. Each successful PATCH replaces the entire stored blob
291
+ * with the merge of (old state) + (incoming patch). Setting a key to `false`
292
+ * makes it explicit-false; omitting a key leaves the current value.
293
+ *
294
+ * @see SPECIFICATIONS.md §8.4
295
+ */
296
+ export const handlePatchFeatureToggles = protectedRoute({ requiredRole: "super_admin" }, async (req, ctx, auth) => {
297
+ let raw;
298
+ try {
299
+ raw = await req.json();
300
+ }
301
+ catch {
302
+ return validationError("Request body must be valid JSON.");
303
+ }
304
+ const parsed = featureTogglesSchema.safeParse(raw);
305
+ if (!parsed.success) {
306
+ return validationError(parsed.error.issues[0]?.message ?? "Invalid request body.");
307
+ }
308
+ const body = parsed.data;
309
+ const row = readSettings(ctx);
310
+ if (!row)
311
+ return internalError();
312
+ const current = (row.featureToggles ?? {});
313
+ const next = { ...current };
314
+ for (const [k, v] of Object.entries(body)) {
315
+ if (v !== undefined)
316
+ next[k] = v;
317
+ }
318
+ ctx.db
319
+ .update(adminSetting)
320
+ .set({ featureToggles: next, updatedAt: nowIso() })
321
+ .where(eq(adminSetting.id, 1))
322
+ .run();
323
+ ctx.logger.info({
324
+ actor_id: auth.user.id,
325
+ patch_keys: Object.keys(body),
326
+ event: "admin_feature_toggles_patched",
327
+ }, "Admin updated feature toggles.");
328
+ return jsonResponse(200, { feature_toggles: next });
329
+ });
330
+ /* ────────────────────────────────────────────────────────────────────────── */
331
+ /* POST /admin/settings/rotate-signing-key */
332
+ /* ────────────────────────────────────────────────────────────────────────── */
333
+ /**
334
+ * `POST /admin/settings/rotate-signing-key` — replace the JWT signing key.
335
+ *
336
+ * Generates 64 new random bytes (matches seed bytes), writes to
337
+ * `admin_setting.service_token_signing_key`, bumps `signing_key_rotated_at`.
338
+ * Returns 204 No Content — the key never leaves the server.
339
+ *
340
+ * **Every outstanding JWT fails signature verification immediately after this
341
+ * call** (no grace period in v1). The calling super-admin's own token is
342
+ * invalidated alongside everyone else's; the React UI handles the 401 on the
343
+ * next request by routing back to login.
344
+ *
345
+ * @see SPECIFICATIONS.md §6.7 — signing key
346
+ * @see ui-sketches/08-admin-settings.md
347
+ */
348
+ export const handleRotateSigningKey = protectedRoute({ requiredRole: "super_admin" }, async (_req, ctx, auth) => {
349
+ const newKey = randomBytes(64);
350
+ ctx.db
351
+ .update(adminSetting)
352
+ .set({
353
+ serviceTokenSigningKey: newKey,
354
+ signingKeyRotatedAt: nowIso(),
355
+ updatedAt: nowIso(),
356
+ })
357
+ .where(eq(adminSetting.id, 1))
358
+ .run();
359
+ ctx.logger.info({
360
+ actor_id: auth.user.id,
361
+ event: "admin_signing_key_rotated",
362
+ }, "Admin rotated the JWT signing key. All outstanding tokens are now invalid.");
363
+ return new Response(null, { status: 204 });
364
+ });
365
+ /* ────────────────────────────────────────────────────────────────────────── */
366
+ /* POST /admin/settings/rotate-metrics-token */
367
+ /* ────────────────────────────────────────────────────────────────────────── */
368
+ /**
369
+ * `POST /admin/settings/rotate-metrics-token` — replace the `/metrics` bearer
370
+ * token.
371
+ *
372
+ * Generates a fresh 32-byte hex token, writes it, bumps
373
+ * `metrics_scrape_token_rotated_at`, and returns `{ token: "<new>" }` exactly
374
+ * once. The plaintext never appears in subsequent `GET /admin/settings`
375
+ * responses (same one-time-reveal pattern as user password resets).
376
+ *
377
+ * Rotating does **not** invalidate the calling super-admin's JWT — only the
378
+ * metrics-scraper bearer changes. Operators paste the new value into their
379
+ * Prometheus / Alloy config; old scrapers start failing immediately.
380
+ *
381
+ * @see SPECIFICATIONS.md §6.10
382
+ * @see ui-sketches/08-admin-settings.md
383
+ */
384
+ export const handleRotateMetricsToken = protectedRoute({ requiredRole: "super_admin" }, async (_req, ctx, auth) => {
385
+ const newToken = randomBytes(32).toString("hex");
386
+ ctx.db
387
+ .update(adminSetting)
388
+ .set({
389
+ metricsScrapeToken: newToken,
390
+ metricsScrapeTokenRotatedAt: nowIso(),
391
+ updatedAt: nowIso(),
392
+ })
393
+ .where(eq(adminSetting.id, 1))
394
+ .run();
395
+ ctx.logger.info({
396
+ actor_id: auth.user.id,
397
+ event: "admin_metrics_token_rotated",
398
+ }, "Admin rotated the /metrics scrape token.");
399
+ return jsonResponse(200, { token: newToken });
400
+ });
401
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../../src/routes/admin/settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `/admin/users/*` — admin + super_admin user management.
3
+ *
4
+ * Five endpoints:
5
+ *
6
+ * - `GET /admin/users` — list every user in the deployment.
7
+ * - `POST /admin/users` — create a user; the server generates a 16-char
8
+ * alphanumeric password, hashes it, and returns the **plaintext exactly
9
+ * once** in the response. The caller delivers it to the user out-of-band.
10
+ * Sets `is_initial_password = true` so the user is forced to change on
11
+ * first login (§8.5).
12
+ * - `PATCH /admin/users/:id/role` — change a user's role. Uses
13
+ * `applyUserRoleChangeInTx` so the shield + min-count rules are enforced
14
+ * inside the transaction (no race possible).
15
+ * - `POST /admin/users/:id/reset-password` — regenerate a 16-char password
16
+ * for an existing user. Same return-once semantics; sets the initial-
17
+ * password flag back to true. Uses `assertPasswordResetInTx` to validate
18
+ * the shield + min-count rules, then performs the async Argon2 hash + write
19
+ * outside the transaction (better-sqlite3 transactions are synchronous so
20
+ * an Argon2 hash cannot live inside one). The narrow window between assert
21
+ * and write is documented in the helper's JSDoc and is acceptable in v1.
22
+ * - `DELETE /admin/users/:id` — hard-delete the user. Uses
23
+ * `applyUserDeletionInTx`; cascades to `session` via the FK.
24
+ *
25
+ * **Admin shield + min-count rules** are enforced by the helpers in
26
+ * `auth/roleRules.ts`; the handlers here just translate the helper's
27
+ * `TxResult` into a Response.
28
+ *
29
+ * **Admin cannot create / promote to `super_admin`.** The role enum on the
30
+ * POST body is filtered by caller role (admins see user / power_user /
31
+ * admin; super_admin sees all four). This mirrors the [05-admin-users.md]
32
+ * sketch's "Create User" modal.
33
+ *
34
+ * @see SPECIFICATIONS.md §6.3.4 — admin users endpoints
35
+ * @see SPECIFICATIONS.md §8.1 — shield + min-count rules
36
+ * @see SPECIFICATIONS.md §8.5 — password reset semantics
37
+ * @see ui-sketches/05-admin-users.md
38
+ */
39
+ import type { RouteHandler } from "../../handlers/types.js";
40
+ /**
41
+ * `GET /admin/users` — list every user in the deployment.
42
+ *
43
+ * `admin` and `super_admin` only; gated by `protectedRoute({ requiredRole: "admin" })`.
44
+ * Sorted alphabetically by username for stable rendering in the admin table.
45
+ *
46
+ * @see SPECIFICATIONS.md §6.3.4
47
+ * @see ui-sketches/05-admin-users.md
48
+ */
49
+ export declare const handleListUsers: RouteHandler;
50
+ /**
51
+ * `POST /admin/users` — create a new user with a server-generated password.
52
+ *
53
+ * The 16-char alphanumeric plaintext is returned **exactly once** in the
54
+ * response (`{ user, generated_password }`); the caller (admin) is expected
55
+ * to deliver it out-of-band per §8.5. The plaintext never appears in logs
56
+ * (pino's redaction policy plus we never log the field).
57
+ *
58
+ * Caller-role gates:
59
+ *
60
+ * - `admin` can create role `user` / `power_user` / `admin`.
61
+ * - `super_admin` can additionally create `super_admin`.
62
+ *
63
+ * On username collision returns 409 `username_taken` (not 400) — the request
64
+ * itself was well-formed; the conflict is with existing data.
65
+ *
66
+ * @see SPECIFICATIONS.md §6.3.4 / §8.5
67
+ * @see ui-sketches/05-admin-users.md
68
+ */
69
+ export declare const handleCreateUser: RouteHandler;
70
+ /**
71
+ * `PATCH /admin/users/:id/role` — change a user's role.
72
+ *
73
+ * Delegates the shield + min-count check to `applyUserRoleChangeInTx`, which
74
+ * re-reads the target row + super-admin count inside one transaction so a
75
+ * concurrent demote cannot bypass the invariants.
76
+ *
77
+ * Additional caller-role gate on top of the helper: an `admin` cannot grant
78
+ * the `super_admin` role on a target they could otherwise mutate (e.g.
79
+ * promoting a `power_user → super_admin`) — only super-admins can.
80
+ *
81
+ * @see SPECIFICATIONS.md §6.3.4 / §8.1
82
+ */
83
+ export declare const handlePatchUserRole: RouteHandler;
84
+ /**
85
+ * `POST /admin/users/:id/reset-password` — regenerate the user's password.
86
+ *
87
+ * Uses `assertPasswordResetInTx` to validate shield + min-count inside a
88
+ * transaction, then performs the async Argon2 hash + UPDATE *outside* the
89
+ * transaction (better-sqlite3 transactions are synchronous). The narrow
90
+ * window between assertion and write is acceptable in v1 — a concurrent
91
+ * role-mutation could in theory bypass the rule, but the worst-case is a
92
+ * single password reset on a user the admin shouldn't have been able to
93
+ * touch; the admin already had to authenticate and the new password is
94
+ * displayed only to them. Mitigation if this ever bites: re-verify the
95
+ * caller's role hasn't dropped between assertion and write.
96
+ *
97
+ * Returns `{ generated_password }` exactly once. Sets `is_initial_password =
98
+ * true` so the user is forced to change on their next login.
99
+ *
100
+ * @see SPECIFICATIONS.md §6.3.4 / §8.5
101
+ * @see ui-sketches/05-admin-users.md
102
+ */
103
+ export declare const handleResetUserPassword: RouteHandler;
104
+ /**
105
+ * `DELETE /admin/users/:id` — hard-delete the user.
106
+ *
107
+ * Uses `applyUserDeletionInTx`; the FK on `session.user_id` cascades, so
108
+ * deleting a user removes all their sessions atomically. The shield + min-
109
+ * count invariants apply (an admin cannot delete another admin or super
110
+ * admin; nobody can delete the only super admin).
111
+ *
112
+ * @see SPECIFICATIONS.md §6.3.4 / §8.1
113
+ */
114
+ export declare const handleDeleteUser: RouteHandler;
115
+ //# sourceMappingURL=users.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/routes/admin/users.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AA0BH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAgF5D;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAE,YAQ7B,CAAC;AAiBF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAyE9B,CAAC;AAaF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,EAAE,YA4CjC,CAAC;AAMF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,EAAE,YA6CrC,CAAC;AAMF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,EAAE,YA6B9B,CAAC"}