@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,365 @@
1
+ /**
2
+ * Shield + min-count rules + the transactional helpers that compose them.
3
+ *
4
+ * Three concerns live here:
5
+ *
6
+ * 1. **Role hierarchy** — a tiny `ROLE_RANK` map + `hasRoleAtLeast` used by the
7
+ * middleware to drive §8.2 matrix checks.
8
+ * 2. **Pure assertion helpers** — `assertCanChangeUserRole`,
9
+ * `assertCanResetPassword`, `assertCanDeleteUser`. Each throws
10
+ * `RoleRuleError` on a §8.1 shield or super-admin-min-count violation.
11
+ * Pure functions of `(caller, target, newState, totalSuperAdmins)` so they
12
+ * are trivial to unit-test against every cell.
13
+ * 3. **Transactional appliers** — `applyUserRoleChangeInTx`,
14
+ * `applyPasswordResetInTx` (signature only; Step 4 wires the password
15
+ * write), `applyUserDeletionInTx`. Each opens one SQLite transaction,
16
+ * re-reads the target row + super-admin count inside the transaction so a
17
+ * concurrent demote cannot bypass the invariant, calls the matching
18
+ * assertion helper, then applies the write. Returns a discriminated
19
+ * `{ ok: true, ... } | { ok: false, status, code, message }` so the
20
+ * Step-4 handler maps it directly into a `Response`.
21
+ *
22
+ * Step 3 ships the helpers + extensive tests; Step 4 is the first endpoint
23
+ * that consumes them.
24
+ *
25
+ * @see SPECIFICATIONS.md §8.1 — role model + shield + min-count rules
26
+ * @see SPECIFICATIONS.md §8.5 — password reset
27
+ * @see SPECIFICATIONS.md §8.6 — endpoint enforcement order
28
+ */
29
+ import { eq, sql } from "drizzle-orm";
30
+ import { user } from "../db/schema.js";
31
+ /* ────────────────────────────────────────────────────────────────────────── */
32
+ /* Role hierarchy */
33
+ /* ────────────────────────────────────────────────────────────────────────── */
34
+ /**
35
+ * Role ordering — least → most privileged. Authoritative for any
36
+ * "is this role at least as privileged as X" comparison.
37
+ *
38
+ * @see SPECIFICATIONS.md §8.1
39
+ */
40
+ export const ROLE_HIERARCHY = [
41
+ "user",
42
+ "power_user",
43
+ "admin",
44
+ "super_admin",
45
+ ];
46
+ /**
47
+ * Rank within the hierarchy — higher = more privileged. Driven by
48
+ * `ROLE_HIERARCHY` so a future role addition is one constant change.
49
+ *
50
+ * @see SPECIFICATIONS.md §8.1
51
+ */
52
+ export const ROLE_RANK = Object.freeze({
53
+ user: 0,
54
+ power_user: 1,
55
+ admin: 2,
56
+ super_admin: 3,
57
+ });
58
+ /**
59
+ * Returns `true` if `actual` is at least as privileged as `required`. Used by
60
+ * the role-enforcement middleware to gate every protected endpoint per the
61
+ * §8.2 matrix.
62
+ *
63
+ * @param actual - the role the caller's JWT carries.
64
+ * @param required - the minimum role the endpoint requires.
65
+ * @returns `true` if `actual >= required` in the hierarchy.
66
+ */
67
+ export function hasRoleAtLeast(actual, required) {
68
+ return ROLE_RANK[actual] >= ROLE_RANK[required];
69
+ }
70
+ /* ────────────────────────────────────────────────────────────────────────── */
71
+ /* Error type */
72
+ /* ────────────────────────────────────────────────────────────────────────── */
73
+ /**
74
+ * Thrown by the assertion helpers below. The transactional helpers catch
75
+ * `RoleRuleError`s and translate them into the `{ ok: false, ... }` return
76
+ * shape; raw throws are reserved for unexpected programmer errors.
77
+ */
78
+ export class RoleRuleError extends Error {
79
+ /** HTTP status the caller should return (typically 403, sometimes 404). */
80
+ status;
81
+ /** Machine-readable code matching the §7.5 `AIHubError` codes. */
82
+ code;
83
+ /**
84
+ * @param status - HTTP status code to surface.
85
+ * @param code - machine-readable code.
86
+ * @param message - human-readable description.
87
+ */
88
+ constructor(status, code, message) {
89
+ super(message);
90
+ this.name = "RoleRuleError";
91
+ this.status = status;
92
+ this.code = code;
93
+ }
94
+ }
95
+ /**
96
+ * Asserts a role-change is allowed under the §8.1 shield + min-count rules.
97
+ *
98
+ * The rules in plain English:
99
+ *
100
+ * - **Only `super_admin` can demote the last `super_admin`** — min-count rule.
101
+ * Applies regardless of who the caller is (including the super_admin's own
102
+ * self-demote when they are the only one).
103
+ * - **Only `super_admin` can promote anyone to `super_admin`**.
104
+ * - **`admin` callers can only mutate `user` / `power_user` targets** — they
105
+ * cannot change the role of another `admin` or a `super_admin` (including
106
+ * themselves — we block all admin-touches-admin, by an explicit decision
107
+ * recorded in the Step-3 prompt).
108
+ * - **`power_user` and `user` cannot change any role** — the matrix gates
109
+ * that at the endpoint level, but we double-check here so the helper is
110
+ * safe to call from anywhere.
111
+ *
112
+ * @param caller - the caller's id + current role.
113
+ * @param target - the target user row (loaded inside the same transaction).
114
+ * @param newRole - the requested new role for `target`.
115
+ * @param totalSuperAdmins - count of super-admin rows in the DB, read inside
116
+ * the same transaction so a concurrent demote can't bypass the rule.
117
+ * @throws {RoleRuleError} when the requested change violates any rule.
118
+ */
119
+ export function assertCanChangeUserRole(caller, target, newRole, totalSuperAdmins) {
120
+ // Min-count rule first — it applies regardless of caller role.
121
+ if (target.role === "super_admin" &&
122
+ newRole !== "super_admin" &&
123
+ totalSuperAdmins <= 1) {
124
+ throw new RoleRuleError(403, "forbidden", "Cannot demote the only remaining super_admin.");
125
+ }
126
+ if (caller.role === "super_admin") {
127
+ // Super-admin can do anything subject to the min-count rule above.
128
+ return;
129
+ }
130
+ if (caller.role === "admin") {
131
+ // Admin cannot promote anyone to super_admin.
132
+ if (newRole === "super_admin") {
133
+ throw new RoleRuleError(403, "forbidden", "Only super_admin can promote a user to super_admin.");
134
+ }
135
+ // Shield rule: admin cannot touch admin or super_admin targets — including
136
+ // themselves (per the Step-3 prompt decision).
137
+ if (target.role === "admin" || target.role === "super_admin") {
138
+ throw new RoleRuleError(403, "forbidden", "Only super_admin can change the role of an admin or super_admin.");
139
+ }
140
+ return;
141
+ }
142
+ // power_user and user cannot change roles at all.
143
+ throw new RoleRuleError(403, "forbidden", "Only admin and super_admin can change user roles.");
144
+ }
145
+ /**
146
+ * Asserts a password reset is allowed under the §8.1 shield + min-count rules.
147
+ *
148
+ * - `admin` cannot reset another `admin`'s or any `super_admin`'s password
149
+ * (shield rule).
150
+ * - Cannot reset the only `super_admin`'s password — would amount to
151
+ * demotion (min-count rule, §8.5).
152
+ *
153
+ * @param caller - the caller's id + current role.
154
+ * @param target - the target user row.
155
+ * @param totalSuperAdmins - count of super-admin rows read inside the same
156
+ * transaction.
157
+ * @throws {RoleRuleError} on shield or min-count violation.
158
+ */
159
+ export function assertCanResetPassword(caller, target, totalSuperAdmins) {
160
+ if (caller.role === "super_admin") {
161
+ // Min-count: cannot reset the only super_admin's password.
162
+ if (target.role === "super_admin" && totalSuperAdmins <= 1) {
163
+ throw new RoleRuleError(403, "forbidden", "Cannot reset the only remaining super_admin's password.");
164
+ }
165
+ return;
166
+ }
167
+ if (caller.role === "admin") {
168
+ if (target.role === "admin" || target.role === "super_admin") {
169
+ throw new RoleRuleError(403, "forbidden", "Only super_admin can reset the password of an admin or super_admin.");
170
+ }
171
+ return;
172
+ }
173
+ throw new RoleRuleError(403, "forbidden", "Only admin and super_admin can reset passwords.");
174
+ }
175
+ /**
176
+ * Asserts a user deletion is allowed under the §8.1 shield + min-count rules.
177
+ *
178
+ * Same shape as `assertCanResetPassword`:
179
+ *
180
+ * - `admin` cannot delete another `admin` or any `super_admin`.
181
+ * - Cannot delete the only `super_admin`.
182
+ *
183
+ * @param caller - the caller's id + current role.
184
+ * @param target - the target user row.
185
+ * @param totalSuperAdmins - count of super-admin rows read inside the same
186
+ * transaction.
187
+ * @throws {RoleRuleError} on shield or min-count violation.
188
+ */
189
+ export function assertCanDeleteUser(caller, target, totalSuperAdmins) {
190
+ if (caller.role === "super_admin") {
191
+ if (target.role === "super_admin" && totalSuperAdmins <= 1) {
192
+ throw new RoleRuleError(403, "forbidden", "Cannot delete the only remaining super_admin.");
193
+ }
194
+ return;
195
+ }
196
+ if (caller.role === "admin") {
197
+ if (target.role === "admin" || target.role === "super_admin") {
198
+ throw new RoleRuleError(403, "forbidden", "Only super_admin can delete an admin or super_admin.");
199
+ }
200
+ return;
201
+ }
202
+ throw new RoleRuleError(403, "forbidden", "Only admin and super_admin can delete users.");
203
+ }
204
+ /**
205
+ * Counts super-admin rows through `tx`. Reads inside whatever transaction
206
+ * the caller is in.
207
+ *
208
+ * @param tx - any handle that can run a Drizzle `select` against `user`.
209
+ * @returns the integer count.
210
+ */
211
+ function countSuperAdmins(tx) {
212
+ const row = tx
213
+ .select({ c: sql `count(*)`.as("c") })
214
+ .from(user)
215
+ .where(eq(user.role, "super_admin"))
216
+ .get();
217
+ return Number(row?.c ?? 0);
218
+ }
219
+ /**
220
+ * Applies a role change inside one SQLite transaction.
221
+ *
222
+ * Reads the target row + super-admin count inside the transaction so that a
223
+ * racing demote cannot win against the min-count rule. The Drizzle
224
+ * `db.transaction(fn)` API runs `fn` synchronously inside `BEGIN` / `COMMIT`;
225
+ * any thrown error inside `fn` triggers a `ROLLBACK` automatically.
226
+ *
227
+ * @param db - Drizzle client (the caller passes `ctx.db`; the helper opens
228
+ * its own transaction internally).
229
+ * @param caller - caller's id + role.
230
+ * @param targetId - the user whose role is being changed.
231
+ * @param newRole - the new role.
232
+ * @returns the updated row on success, or an error result.
233
+ */
234
+ export function applyUserRoleChangeInTx(db, caller, targetId, newRole) {
235
+ try {
236
+ return db.transaction((tx) => {
237
+ const target = tx
238
+ .select()
239
+ .from(user)
240
+ .where(eq(user.id, targetId))
241
+ .get();
242
+ if (!target) {
243
+ throw new RoleRuleError(404, "not_found", "User not found.");
244
+ }
245
+ const count = countSuperAdmins(tx);
246
+ assertCanChangeUserRole(caller, target, newRole, count);
247
+ tx
248
+ .update(user)
249
+ .set({ role: newRole, updatedAt: nowIso() })
250
+ .where(eq(user.id, targetId))
251
+ .run();
252
+ const updated = tx
253
+ .select()
254
+ .from(user)
255
+ .where(eq(user.id, targetId))
256
+ .get();
257
+ if (!updated) {
258
+ // Should be impossible — we just updated.
259
+ throw new RoleRuleError(500, "internal", "User vanished mid-transaction.");
260
+ }
261
+ return { ok: true, data: { updated } };
262
+ });
263
+ }
264
+ catch (err) {
265
+ if (err instanceof RoleRuleError) {
266
+ return {
267
+ ok: false,
268
+ status: err.status,
269
+ code: err.code,
270
+ message: err.message,
271
+ };
272
+ }
273
+ throw err;
274
+ }
275
+ }
276
+ /**
277
+ * Applies a user deletion inside one SQLite transaction. Same shape as
278
+ * `applyUserRoleChangeInTx` but DELETEs rather than UPDATEs.
279
+ *
280
+ * @param db - Drizzle client.
281
+ * @param caller - caller's id + role.
282
+ * @param targetId - the user to delete.
283
+ * @returns `{ ok: true, data: { deleted: <row before delete> } }` on success.
284
+ */
285
+ export function applyUserDeletionInTx(db, caller, targetId) {
286
+ try {
287
+ return db.transaction((tx) => {
288
+ const target = tx
289
+ .select()
290
+ .from(user)
291
+ .where(eq(user.id, targetId))
292
+ .get();
293
+ if (!target) {
294
+ throw new RoleRuleError(404, "not_found", "User not found.");
295
+ }
296
+ const count = countSuperAdmins(tx);
297
+ assertCanDeleteUser(caller, target, count);
298
+ tx.delete(user).where(eq(user.id, targetId)).run();
299
+ return { ok: true, data: { deleted: target } };
300
+ });
301
+ }
302
+ catch (err) {
303
+ if (err instanceof RoleRuleError) {
304
+ return {
305
+ ok: false,
306
+ status: err.status,
307
+ code: err.code,
308
+ message: err.message,
309
+ };
310
+ }
311
+ throw err;
312
+ }
313
+ }
314
+ /**
315
+ * Runs the password-reset assertion inside a transaction and yields the
316
+ * fresh target row. The caller (Step 4's `POST /admin/users/{id}/reset-password`
317
+ * handler) generates the new password, hashes it, and writes inside the
318
+ * same transaction — this helper exposes the read-and-assert plumbing so
319
+ * the write can happen alongside.
320
+ *
321
+ * The shape is slightly different from the role-change / delete helpers
322
+ * because the password generation + hashing is async (Argon2) and cannot
323
+ * happen inside better-sqlite3's synchronous transaction. The Step-4 handler
324
+ * will run this helper first (to validate + lock-read), then write outside
325
+ * the transaction. The risk of a concurrent role-change is small but real;
326
+ * Step 4 can re-verify after the write or accept the tiny window.
327
+ *
328
+ * @param db - Drizzle client.
329
+ * @param caller - caller's id + role.
330
+ * @param targetId - the user whose password is being reset.
331
+ * @returns the target row on success, or an error result.
332
+ */
333
+ export function assertPasswordResetInTx(db, caller, targetId) {
334
+ try {
335
+ return db.transaction((tx) => {
336
+ const target = tx
337
+ .select()
338
+ .from(user)
339
+ .where(eq(user.id, targetId))
340
+ .get();
341
+ if (!target) {
342
+ throw new RoleRuleError(404, "not_found", "User not found.");
343
+ }
344
+ const count = countSuperAdmins(tx);
345
+ assertCanResetPassword(caller, target, count);
346
+ return { ok: true, data: { target } };
347
+ });
348
+ }
349
+ catch (err) {
350
+ if (err instanceof RoleRuleError) {
351
+ return {
352
+ ok: false,
353
+ status: err.status,
354
+ code: err.code,
355
+ message: err.message,
356
+ };
357
+ }
358
+ throw err;
359
+ }
360
+ }
361
+ /** Returns the current ISO-8601 timestamp with millisecond precision. */
362
+ function nowIso() {
363
+ return new Date().toISOString();
364
+ }
365
+ //# sourceMappingURL=roleRules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleRules.js","sourceRoot":"","sources":["../../src/auth/roleRules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Builds the `{ user, deployment }` envelope returned by `/auth/me`,
3
+ * `/auth/me/preferences`, and embedded in `/auth/login`'s response.
4
+ *
5
+ * Two non-obvious concerns this module owns:
6
+ *
7
+ * 1. **Per-user vs. per-deployment split.** `user` carries strictly per-user
8
+ * fields (id, username, role, theme_preference, …); `deployment` carries
9
+ * strictly per-deployment fields (theme_default, theme_switcher_enabled,
10
+ * letta_ade_url). The split makes it impossible for a client to confuse
11
+ * "this is my preference" with "this is how the deployment is configured."
12
+ * 2. **Role-gated `letta_ade_url`.** The URL is only included for callers
13
+ * with role `power_user` or higher (§6.3.4). For `user`-role callers it's
14
+ * omitted entirely (not set to null) so the React client cannot render
15
+ * a link element it shouldn't see.
16
+ *
17
+ * The resolved theme is **not** computed here — the client does the simple
18
+ * precedence calculation client-side from `user.theme_preference +
19
+ * deployment.{theme_default, theme_switcher_enabled}` per §7.6.
20
+ *
21
+ * @see SPECIFICATIONS.md §6.3.4 — `/auth/me`
22
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
23
+ */
24
+ import type { AIHubAuthContext, AIHubDeployment, AIHubUser } from "@data-club/ai-hub";
25
+ import type { AdminSettingRow, UserRow } from "../db/schema.js";
26
+ /**
27
+ * Builds the per-user identity payload. Pure per-user state.
28
+ *
29
+ * @param row - the `user` DB row.
30
+ * @returns the wire-format `AIHubUser`.
31
+ */
32
+ export declare function buildUserView(row: UserRow): AIHubUser;
33
+ /**
34
+ * Builds the per-deployment context payload. The `letta_ade_url` field is
35
+ * gated by `callerRole >= power_user` (§6.3.4).
36
+ *
37
+ * @param settings - the `admin_setting` singleton row (pass null only when
38
+ * the row genuinely doesn't exist — first-boot before the seed has run,
39
+ * which shouldn't happen in practice).
40
+ * @param callerRole - role from the JWT's claim, used to gate
41
+ * `letta_ade_url`.
42
+ * @returns the wire-format `AIHubDeployment`.
43
+ */
44
+ export declare function buildDeploymentView(settings: AdminSettingRow | null, callerRole: UserRow["role"]): AIHubDeployment;
45
+ /**
46
+ * Convenience: builds both halves and returns them as the
47
+ * `AIHubAuthContext` envelope `/auth/me` (and `/auth/login` post-change)
48
+ * use directly.
49
+ *
50
+ * @param row - the user row.
51
+ * @param settings - the `admin_setting` singleton row.
52
+ * @returns the combined envelope.
53
+ */
54
+ export declare function buildAuthContext(row: UserRow, settings: AdminSettingRow | null): AIHubAuthContext;
55
+ //# sourceMappingURL=userView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userView.d.ts","sourceRoot":"","sources":["../../src/auth/userView.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEtF,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAQrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,GAAG,IAAI,EAChC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,GAC1B,eAAe,CAUjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,eAAe,GAAG,IAAI,GAC/B,gBAAgB,CAKlB"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Builds the `{ user, deployment }` envelope returned by `/auth/me`,
3
+ * `/auth/me/preferences`, and embedded in `/auth/login`'s response.
4
+ *
5
+ * Two non-obvious concerns this module owns:
6
+ *
7
+ * 1. **Per-user vs. per-deployment split.** `user` carries strictly per-user
8
+ * fields (id, username, role, theme_preference, …); `deployment` carries
9
+ * strictly per-deployment fields (theme_default, theme_switcher_enabled,
10
+ * letta_ade_url). The split makes it impossible for a client to confuse
11
+ * "this is my preference" with "this is how the deployment is configured."
12
+ * 2. **Role-gated `letta_ade_url`.** The URL is only included for callers
13
+ * with role `power_user` or higher (§6.3.4). For `user`-role callers it's
14
+ * omitted entirely (not set to null) so the React client cannot render
15
+ * a link element it shouldn't see.
16
+ *
17
+ * The resolved theme is **not** computed here — the client does the simple
18
+ * precedence calculation client-side from `user.theme_preference +
19
+ * deployment.{theme_default, theme_switcher_enabled}` per §7.6.
20
+ *
21
+ * @see SPECIFICATIONS.md §6.3.4 — `/auth/me`
22
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
23
+ */
24
+ /**
25
+ * Builds the per-user identity payload. Pure per-user state.
26
+ *
27
+ * @param row - the `user` DB row.
28
+ * @returns the wire-format `AIHubUser`.
29
+ */
30
+ export function buildUserView(row) {
31
+ return {
32
+ id: row.id,
33
+ username: row.username,
34
+ display_name: row.displayName ?? null,
35
+ role: row.role,
36
+ theme_preference: row.themePreference ?? null,
37
+ };
38
+ }
39
+ /**
40
+ * Builds the per-deployment context payload. The `letta_ade_url` field is
41
+ * gated by `callerRole >= power_user` (§6.3.4).
42
+ *
43
+ * @param settings - the `admin_setting` singleton row (pass null only when
44
+ * the row genuinely doesn't exist — first-boot before the seed has run,
45
+ * which shouldn't happen in practice).
46
+ * @param callerRole - role from the JWT's claim, used to gate
47
+ * `letta_ade_url`.
48
+ * @returns the wire-format `AIHubDeployment`.
49
+ */
50
+ export function buildDeploymentView(settings, callerRole) {
51
+ const out = {
52
+ theme_default: settings?.themeDefault ?? "light",
53
+ theme_switcher_enabled: settings?.themeSwitcherEnabled ?? true,
54
+ };
55
+ if (callerRole !== "user") {
56
+ const ade = settings?.lettaAdeUrl ?? null;
57
+ if (ade)
58
+ out.letta_ade_url = ade;
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * Convenience: builds both halves and returns them as the
64
+ * `AIHubAuthContext` envelope `/auth/me` (and `/auth/login` post-change)
65
+ * use directly.
66
+ *
67
+ * @param row - the user row.
68
+ * @param settings - the `admin_setting` singleton row.
69
+ * @returns the combined envelope.
70
+ */
71
+ export function buildAuthContext(row, settings) {
72
+ return {
73
+ user: buildUserView(row),
74
+ deployment: buildDeploymentView(settings, row.role),
75
+ };
76
+ }
77
+ //# sourceMappingURL=userView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userView.js","sourceRoot":"","sources":["../../src/auth/userView.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,YAAY,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACrC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,gBAAgB,EAAE,GAAG,CAAC,eAAe,IAAI,IAAI;KAC9C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgC,EAChC,UAA2B;IAE3B,MAAM,GAAG,GAAoB;QAC3B,aAAa,EAAE,QAAQ,EAAE,YAAY,IAAI,OAAO;QAChD,sBAAsB,EAAE,QAAQ,EAAE,oBAAoB,IAAI,IAAI;KAC/D,CAAC;IACF,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,QAAQ,EAAE,WAAW,IAAI,IAAI,CAAC;QAC1C,IAAI,GAAG;YAAE,GAAG,CAAC,aAAa,GAAG,GAAG,CAAC;IACnC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAY,EACZ,QAAgC;IAEhC,OAAO;QACL,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC;QACxB,UAAU,EAAE,mBAAmB,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC;KACpD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Boot orchestration: env → open DB → migrate → seed → ready.
3
+ *
4
+ * The pieces that compose this (env loader, `openDb`, `runMigrations`,
5
+ * `runFirstBootSeed`, `createLogger`) are independently importable so tests
6
+ * and embedders can wire bespoke combinations. `bootServer` is the
7
+ * convenience entry point the `dev/server.ts` runner and the per-customer
8
+ * wrapper's Next.js mount call.
9
+ *
10
+ * @see SPECIFICATIONS.md §12.1 — env vars
11
+ * @see SPECIFICATIONS.md §6.7 — signing key bootstrap
12
+ * @see SPECIFICATIONS.md §6.10 — metrics token bootstrap
13
+ */
14
+ import type Database from "better-sqlite3";
15
+ import { type DrizzleDB } from "./db/connection.js";
16
+ import { type SeedReport } from "./db/seed.js";
17
+ import { type FetchHandler } from "./handlers/fetch.js";
18
+ import type { LettaClient } from "./letta/client.js";
19
+ import { type Logger } from "./observability/logger.js";
20
+ /** Subset of `process.env` we care about — narrows for tests. */
21
+ export type EnvLike = Record<string, string | undefined>;
22
+ /**
23
+ * Parsed env config used by `bootServer`. Everything is optional except
24
+ * `encryptionKey`, which `parseEnvConfig` will raise on if missing.
25
+ */
26
+ export interface EnvConfig {
27
+ /** Filesystem dir for SQLite + state. Default `./data`. */
28
+ dataDir: string;
29
+ /** SQLite connection string (file path or `:memory:`). */
30
+ databaseUrl: string;
31
+ /** Required AES-256 key parsed from `AI_HUB_ENCRYPTION_KEY`. */
32
+ encryptionKey: Buffer;
33
+ /** Bootstrap-only signing key (env value, decoded). */
34
+ signingKeyFromEnv: Buffer | undefined;
35
+ /** Bootstrap-only metrics scrape token (env value). */
36
+ metricsScrapeTokenFromEnv: string | undefined;
37
+ /** Letta API key from env. */
38
+ lettaApiKey: string | undefined;
39
+ /** Letta base URL from env. */
40
+ lettaBaseUrl: string | undefined;
41
+ /** Letta ADE URL override from env. */
42
+ lettaAdeUrl: string | undefined;
43
+ /** Pino log level. */
44
+ logLevel: string;
45
+ /** CORS origins (comma-separated). */
46
+ corsAllowedOrigins: string;
47
+ }
48
+ /**
49
+ * Parses `process.env` (or a test-supplied stand-in) into an `EnvConfig`.
50
+ * Throws on any required-but-missing variable so the operator gets a clear
51
+ * error on first boot instead of a confusing runtime failure.
52
+ *
53
+ * @param env - the env-var record to read from.
54
+ * @returns the parsed config.
55
+ */
56
+ export declare function parseEnvConfig(env: EnvLike): EnvConfig;
57
+ /** Returned by `bootServer` — everything the dev/Next.js mount needs. */
58
+ export interface BootedServer {
59
+ /** The Fetch handler — `(Request) => Promise<Response>`. */
60
+ fetchHandler: FetchHandler;
61
+ /** Drizzle client (in case the embedder wants to read directly). */
62
+ db: DrizzleDB;
63
+ /** Raw better-sqlite3 handle. */
64
+ sqlite: Database.Database;
65
+ /** Logger configured per env. */
66
+ logger: Logger;
67
+ /** Parsed env config. */
68
+ config: EnvConfig;
69
+ /** What the seed actually wrote on this boot. */
70
+ seedReport: SeedReport;
71
+ /** Closes the DB handle. Call this on graceful shutdown. */
72
+ close: () => void;
73
+ }
74
+ /** Optional override: pass a pre-built logger or destination to capture. */
75
+ export interface BootServerOptions {
76
+ /**
77
+ * Custom env source. Defaults to `process.env`. Tests pass a synthesized
78
+ * record so they don't have to mutate the actual environment.
79
+ */
80
+ env?: EnvLike;
81
+ /** Override the logger (e.g. test capture). */
82
+ logger?: Logger;
83
+ /** Override the connection — tests use `:memory:` here. */
84
+ databaseUrl?: string;
85
+ /**
86
+ * Override the Letta client (§6.4). Tests pass a mock so the server
87
+ * never reaches a real Letta. Production omits this — the default reads
88
+ * `admin_setting.letta_base_url` + decrypted API key on each use.
89
+ */
90
+ lettaClient?: LettaClient;
91
+ }
92
+ /**
93
+ * One-shot boot: parses env, opens the DB, runs migrations, runs the
94
+ * first-boot seed (idempotent), and returns the Fetch handler.
95
+ *
96
+ * Called by `dev/server.ts` and by per-customer wrappers' Next.js mount
97
+ * (typically wrapped in a module-level singleton so it runs once per
98
+ * Next.js process).
99
+ *
100
+ * @param opts - optional overrides for testing.
101
+ * @returns the booted server bundle.
102
+ */
103
+ export declare function bootServer(opts?: BootServerOptions): Promise<BootedServer>;
104
+ //# sourceMappingURL=bootstrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAMtE,iEAAiE;AACjE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,uDAAuD;IACvD,yBAAyB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,8BAA8B;IAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,uCAAuC;IACvC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAiCtD;AAyDD,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,YAAY,EAAE,YAAY,CAAC;IAC3B,oEAAoE;IACpE,EAAE,EAAE,SAAS,CAAC;IACd,iCAAiC;IACjC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC1B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,MAAM,EAAE,SAAS,CAAC;IAClB,iDAAiD;IACjD,UAAU,EAAE,UAAU,CAAC;IACvB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6CvB"}