@digitalpaws/starr 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 (290) hide show
  1. package/README.rst +75 -0
  2. package/data/.nfs.811bbb0d.43c34.4 +0 -0
  3. package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
  4. package/data/.nfs.82d127f7.fffff5704.4 +0 -0
  5. package/data/.nfs.82d127fe.fffff5704.4 +0 -0
  6. package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
  7. package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
  8. package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
  9. package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
  10. package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
  11. package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
  12. package/data/.nfs.82d18536.fffff5e24.4 +0 -0
  13. package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
  14. package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
  15. package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
  16. package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
  17. package/data/.nfs.8314122e.fffffb044.4 +0 -0
  18. package/data/.nfs.83141236.fffffb044.4 +0 -0
  19. package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
  20. package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
  21. package/data/logs/starr.log +66244 -0
  22. package/data/starr.db +0 -0
  23. package/data/starr.db-shm +0 -0
  24. package/data/starr.db-wal +0 -0
  25. package/data/starr.jwt +1 -0
  26. package/data/starr.key +1 -0
  27. package/dist/auth/auth-service.d.ts +42 -0
  28. package/dist/auth/auth-service.d.ts.map +1 -0
  29. package/dist/auth/auth-service.js +226 -0
  30. package/dist/auth/auth-service.js.map +1 -0
  31. package/dist/auth/http-server.d.ts +16 -0
  32. package/dist/auth/http-server.d.ts.map +1 -0
  33. package/dist/auth/http-server.js +207 -0
  34. package/dist/auth/http-server.js.map +1 -0
  35. package/dist/auth/jwt-secret.d.ts +6 -0
  36. package/dist/auth/jwt-secret.d.ts.map +1 -0
  37. package/dist/auth/jwt-secret.js +51 -0
  38. package/dist/auth/jwt-secret.js.map +1 -0
  39. package/dist/auth/oidc/oidc-flow.d.ts +60 -0
  40. package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
  41. package/dist/auth/oidc/oidc-flow.js +201 -0
  42. package/dist/auth/oidc/oidc-flow.js.map +1 -0
  43. package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
  44. package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
  45. package/dist/auth/oidc/oidc-provider-service.js +124 -0
  46. package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
  47. package/dist/auth/oidc/user-service.d.ts +67 -0
  48. package/dist/auth/oidc/user-service.d.ts.map +1 -0
  49. package/dist/auth/oidc/user-service.js +177 -0
  50. package/dist/auth/oidc/user-service.js.map +1 -0
  51. package/dist/auth/rate-limiter.d.ts +31 -0
  52. package/dist/auth/rate-limiter.d.ts.map +1 -0
  53. package/dist/auth/rate-limiter.js +70 -0
  54. package/dist/auth/rate-limiter.js.map +1 -0
  55. package/dist/config/config-service.d.ts +83 -0
  56. package/dist/config/config-service.d.ts.map +1 -0
  57. package/dist/config/config-service.js +309 -0
  58. package/dist/config/config-service.js.map +1 -0
  59. package/dist/config/validate.d.ts +12 -0
  60. package/dist/config/validate.d.ts.map +1 -0
  61. package/dist/config/validate.js +140 -0
  62. package/dist/config/validate.js.map +1 -0
  63. package/dist/content-types/registry.d.ts +75 -0
  64. package/dist/content-types/registry.d.ts.map +1 -0
  65. package/dist/content-types/registry.js +171 -0
  66. package/dist/content-types/registry.js.map +1 -0
  67. package/dist/datastore/db.d.ts +9 -0
  68. package/dist/datastore/db.d.ts.map +1 -0
  69. package/dist/datastore/db.js +27 -0
  70. package/dist/datastore/db.js.map +1 -0
  71. package/dist/datastore/encrypt.d.ts +9 -0
  72. package/dist/datastore/encrypt.d.ts.map +1 -0
  73. package/dist/datastore/encrypt.js +84 -0
  74. package/dist/datastore/encrypt.js.map +1 -0
  75. package/dist/datastore/migrations.d.ts +10 -0
  76. package/dist/datastore/migrations.d.ts.map +1 -0
  77. package/dist/datastore/migrations.js +340 -0
  78. package/dist/datastore/migrations.js.map +1 -0
  79. package/dist/datastore/storage.d.ts +4 -0
  80. package/dist/datastore/storage.d.ts.map +1 -0
  81. package/dist/datastore/storage.js +60 -0
  82. package/dist/datastore/storage.js.map +1 -0
  83. package/dist/events/event-bus.d.ts +15 -0
  84. package/dist/events/event-bus.d.ts.map +1 -0
  85. package/dist/events/event-bus.js +140 -0
  86. package/dist/events/event-bus.js.map +1 -0
  87. package/dist/events/spine.d.ts +31 -0
  88. package/dist/events/spine.d.ts.map +1 -0
  89. package/dist/events/spine.js +68 -0
  90. package/dist/events/spine.js.map +1 -0
  91. package/dist/events/trace.d.ts +24 -0
  92. package/dist/events/trace.d.ts.map +1 -0
  93. package/dist/events/trace.js +33 -0
  94. package/dist/events/trace.js.map +1 -0
  95. package/dist/host.d.ts +65 -0
  96. package/dist/host.d.ts.map +1 -0
  97. package/dist/host.js +94 -0
  98. package/dist/host.js.map +1 -0
  99. package/dist/library/importer.d.ts +29 -0
  100. package/dist/library/importer.d.ts.map +1 -0
  101. package/dist/library/importer.js +145 -0
  102. package/dist/library/importer.js.map +1 -0
  103. package/dist/library/library-cache.d.ts +46 -0
  104. package/dist/library/library-cache.d.ts.map +1 -0
  105. package/dist/library/library-cache.js +152 -0
  106. package/dist/library/library-cache.js.map +1 -0
  107. package/dist/library/library-config.d.ts +12 -0
  108. package/dist/library/library-config.d.ts.map +1 -0
  109. package/dist/library/library-config.js +10 -0
  110. package/dist/library/library-config.js.map +1 -0
  111. package/dist/library/library-scanner.d.ts +62 -0
  112. package/dist/library/library-scanner.d.ts.map +1 -0
  113. package/dist/library/library-scanner.js +336 -0
  114. package/dist/library/library-scanner.js.map +1 -0
  115. package/dist/library/library-service.d.ts +65 -0
  116. package/dist/library/library-service.d.ts.map +1 -0
  117. package/dist/library/library-service.js +197 -0
  118. package/dist/library/library-service.js.map +1 -0
  119. package/dist/library/metadata-matcher.d.ts +56 -0
  120. package/dist/library/metadata-matcher.d.ts.map +1 -0
  121. package/dist/library/metadata-matcher.js +207 -0
  122. package/dist/library/metadata-matcher.js.map +1 -0
  123. package/dist/library/naming.d.ts +8 -0
  124. package/dist/library/naming.d.ts.map +1 -0
  125. package/dist/library/naming.js +47 -0
  126. package/dist/library/naming.js.map +1 -0
  127. package/dist/library/scan-controller.d.ts +95 -0
  128. package/dist/library/scan-controller.d.ts.map +1 -0
  129. package/dist/library/scan-controller.js +379 -0
  130. package/dist/library/scan-controller.js.map +1 -0
  131. package/dist/library/source-resolver.d.ts +17 -0
  132. package/dist/library/source-resolver.d.ts.map +1 -0
  133. package/dist/library/source-resolver.js +75 -0
  134. package/dist/library/source-resolver.js.map +1 -0
  135. package/dist/logging/logger.d.ts +3 -0
  136. package/dist/logging/logger.d.ts.map +1 -0
  137. package/dist/logging/logger.js +107 -0
  138. package/dist/logging/logger.js.map +1 -0
  139. package/dist/main.d.ts +36 -0
  140. package/dist/main.d.ts.map +1 -0
  141. package/dist/main.js +229 -0
  142. package/dist/main.js.map +1 -0
  143. package/dist/plugins/context.d.ts +10 -0
  144. package/dist/plugins/context.d.ts.map +1 -0
  145. package/dist/plugins/context.js +110 -0
  146. package/dist/plugins/context.js.map +1 -0
  147. package/dist/plugins/discovery.d.ts +2 -0
  148. package/dist/plugins/discovery.d.ts.map +1 -0
  149. package/dist/plugins/discovery.js +32 -0
  150. package/dist/plugins/discovery.js.map +1 -0
  151. package/dist/plugins/loader.d.ts +9 -0
  152. package/dist/plugins/loader.d.ts.map +1 -0
  153. package/dist/plugins/loader.js +245 -0
  154. package/dist/plugins/loader.js.map +1 -0
  155. package/dist/state/request-intake.d.ts +26 -0
  156. package/dist/state/request-intake.d.ts.map +1 -0
  157. package/dist/state/request-intake.js +74 -0
  158. package/dist/state/request-intake.js.map +1 -0
  159. package/dist/state/state-store.d.ts +146 -0
  160. package/dist/state/state-store.d.ts.map +1 -0
  161. package/dist/state/state-store.js +465 -0
  162. package/dist/state/state-store.js.map +1 -0
  163. package/dist/state/trace-store.d.ts +12 -0
  164. package/dist/state/trace-store.d.ts.map +1 -0
  165. package/dist/state/trace-store.js +24 -0
  166. package/dist/state/trace-store.js.map +1 -0
  167. package/dist/types/plugin.d.ts +93 -0
  168. package/dist/types/plugin.d.ts.map +1 -0
  169. package/dist/types/plugin.js +4 -0
  170. package/dist/types/plugin.js.map +1 -0
  171. package/dist/web/index.d.ts +10 -0
  172. package/dist/web/index.d.ts.map +1 -0
  173. package/dist/web/index.js +153 -0
  174. package/dist/web/index.js.map +1 -0
  175. package/dist/web/routes/activity.d.ts +6 -0
  176. package/dist/web/routes/activity.d.ts.map +1 -0
  177. package/dist/web/routes/activity.js +31 -0
  178. package/dist/web/routes/activity.js.map +1 -0
  179. package/dist/web/routes/auth.d.ts +6 -0
  180. package/dist/web/routes/auth.d.ts.map +1 -0
  181. package/dist/web/routes/auth.js +142 -0
  182. package/dist/web/routes/auth.js.map +1 -0
  183. package/dist/web/routes/config.d.ts +7 -0
  184. package/dist/web/routes/config.d.ts.map +1 -0
  185. package/dist/web/routes/config.js +204 -0
  186. package/dist/web/routes/config.js.map +1 -0
  187. package/dist/web/routes/content-types.d.ts +6 -0
  188. package/dist/web/routes/content-types.d.ts.map +1 -0
  189. package/dist/web/routes/content-types.js +29 -0
  190. package/dist/web/routes/content-types.js.map +1 -0
  191. package/dist/web/routes/discovery.d.ts +6 -0
  192. package/dist/web/routes/discovery.d.ts.map +1 -0
  193. package/dist/web/routes/discovery.js +111 -0
  194. package/dist/web/routes/discovery.js.map +1 -0
  195. package/dist/web/routes/library-import.d.ts +6 -0
  196. package/dist/web/routes/library-import.d.ts.map +1 -0
  197. package/dist/web/routes/library-import.js +120 -0
  198. package/dist/web/routes/library-import.js.map +1 -0
  199. package/dist/web/routes/library-management.d.ts +6 -0
  200. package/dist/web/routes/library-management.d.ts.map +1 -0
  201. package/dist/web/routes/library-management.js +120 -0
  202. package/dist/web/routes/library-management.js.map +1 -0
  203. package/dist/web/routes/library.d.ts +6 -0
  204. package/dist/web/routes/library.d.ts.map +1 -0
  205. package/dist/web/routes/library.js +28 -0
  206. package/dist/web/routes/library.js.map +1 -0
  207. package/dist/web/routes/oidc.d.ts +9 -0
  208. package/dist/web/routes/oidc.d.ts.map +1 -0
  209. package/dist/web/routes/oidc.js +203 -0
  210. package/dist/web/routes/oidc.js.map +1 -0
  211. package/dist/web/routes/requests.d.ts +6 -0
  212. package/dist/web/routes/requests.d.ts.map +1 -0
  213. package/dist/web/routes/requests.js +183 -0
  214. package/dist/web/routes/requests.js.map +1 -0
  215. package/dist/web/routes/sse.d.ts +6 -0
  216. package/dist/web/routes/sse.d.ts.map +1 -0
  217. package/dist/web/routes/sse.js +54 -0
  218. package/dist/web/routes/sse.js.map +1 -0
  219. package/package.json +33 -0
  220. package/package.json.license +2 -0
  221. package/src/auth/auth-service.ts +333 -0
  222. package/src/auth/http-server.ts +249 -0
  223. package/src/auth/jwt-secret.ts +65 -0
  224. package/src/auth/oidc/oidc-flow.ts +266 -0
  225. package/src/auth/oidc/oidc-provider-service.ts +174 -0
  226. package/src/auth/oidc/user-service.ts +227 -0
  227. package/src/auth/rate-limiter.ts +85 -0
  228. package/src/config/config-service.ts +434 -0
  229. package/src/config/validate.ts +153 -0
  230. package/src/content-types/registry.ts +194 -0
  231. package/src/datastore/db.ts +38 -0
  232. package/src/datastore/encrypt.ts +107 -0
  233. package/src/datastore/migrations.ts +367 -0
  234. package/src/datastore/storage.ts +83 -0
  235. package/src/events/event-bus.ts +180 -0
  236. package/src/events/spine.ts +87 -0
  237. package/src/events/trace.ts +38 -0
  238. package/src/host.ts +130 -0
  239. package/src/library/importer.ts +174 -0
  240. package/src/library/library-cache.ts +212 -0
  241. package/src/library/library-config.ts +22 -0
  242. package/src/library/library-scanner.ts +404 -0
  243. package/src/library/library-service.ts +282 -0
  244. package/src/library/metadata-matcher.ts +285 -0
  245. package/src/library/naming.ts +59 -0
  246. package/src/library/scan-controller.ts +490 -0
  247. package/src/library/source-resolver.ts +92 -0
  248. package/src/logging/logger.ts +115 -0
  249. package/src/main.ts +364 -0
  250. package/src/plugins/context.ts +138 -0
  251. package/src/plugins/discovery.ts +29 -0
  252. package/src/plugins/loader.ts +309 -0
  253. package/src/state/request-intake.ts +101 -0
  254. package/src/state/state-store.ts +621 -0
  255. package/src/state/trace-store.ts +27 -0
  256. package/src/types/plugin.ts +110 -0
  257. package/src/web/index.ts +182 -0
  258. package/src/web/routes/activity.ts +41 -0
  259. package/src/web/routes/auth.ts +170 -0
  260. package/src/web/routes/config.ts +282 -0
  261. package/src/web/routes/content-types.ts +42 -0
  262. package/src/web/routes/discovery.ts +156 -0
  263. package/src/web/routes/library-import.ts +196 -0
  264. package/src/web/routes/library-management.ts +140 -0
  265. package/src/web/routes/library.ts +46 -0
  266. package/src/web/routes/oidc.ts +260 -0
  267. package/src/web/routes/requests.ts +250 -0
  268. package/src/web/routes/sse.ts +92 -0
  269. package/tests/auth.test.ts +510 -0
  270. package/tests/config-route.test.ts +345 -0
  271. package/tests/config.test.ts +612 -0
  272. package/tests/content-types.test.ts +376 -0
  273. package/tests/datastore.test.ts +345 -0
  274. package/tests/e2e.test.ts +430 -0
  275. package/tests/event-bus.test.ts +925 -0
  276. package/tests/host.test.ts +449 -0
  277. package/tests/library-cache.test.ts +171 -0
  278. package/tests/library-scanner.test.ts +177 -0
  279. package/tests/library.test.ts +210 -0
  280. package/tests/loader-context.test.ts +644 -0
  281. package/tests/logger-jsonl.test.ts +154 -0
  282. package/tests/metadata-matcher.test.ts +198 -0
  283. package/tests/oidc.test.ts +218 -0
  284. package/tests/scan-controller.test.ts +328 -0
  285. package/tests/source-resolver.test.ts +100 -0
  286. package/tests/starr-video-integration.test.ts +153 -0
  287. package/tests/state-store.test.ts +700 -0
  288. package/tests/trace-history.test.ts +356 -0
  289. package/tsconfig.json +8 -0
  290. package/tsconfig.json.license +2 -0
@@ -0,0 +1,60 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { Logger } from "../../types/plugin.js";
3
+ import type { OidcProviderService } from "./oidc-provider-service.js";
4
+ import type { User, UserService } from "./user-service.js";
5
+ /** Result of a successful OIDC callback. */
6
+ export interface OidcCallbackResult {
7
+ user: User;
8
+ providerId: string;
9
+ created: boolean;
10
+ redirectTo: string;
11
+ }
12
+ export declare class OidcFlow {
13
+ private db;
14
+ private providerService;
15
+ private userService;
16
+ private baseUrl;
17
+ private logger;
18
+ constructor(db: Database.Database, providerService: OidcProviderService, userService: UserService, baseUrl: string, // e.g. "http://localhost:3457"
19
+ logger: Logger);
20
+ /**
21
+ * Stage 1 — Initiate the OIDC login flow.
22
+ *
23
+ * 1. Look up the provider configuration.
24
+ * 2. Discover the OIDC issuer's endpoints (via .well-known).
25
+ * 3. Generate PKCE code verifier and challenge, state, nonce.
26
+ * 4. Store the state in oidc_auth_states.
27
+ * 5. Return the redirect URL to send the user's browser to.
28
+ *
29
+ * @param providerId The OIDC provider's UUID.
30
+ * @param redirectTo Where to send the user after successful login (e.g. "/browse").
31
+ * @returns The authorization URL to redirect the browser to.
32
+ */
33
+ initiateLogin(providerId: string, redirectTo?: string): Promise<string>;
34
+ /**
35
+ * Stage 2 — Handle the OIDC callback.
36
+ *
37
+ * 1. Validate the state (CSRF protection).
38
+ * 2. Look up stored auth state from DB.
39
+ * 3. Exchange authorization code for tokens.
40
+ * 4. Validate the ID token (handled by openid-client).
41
+ * 5. Find or create the user.
42
+ * 6. Clean up the used auth state.
43
+ *
44
+ * @returns The authenticated user and whether they were just created.
45
+ */
46
+ handleCallback(providerId: string, code: string, state: string): Promise<OidcCallbackResult>;
47
+ /**
48
+ * Resolve a Starr role from an OIDC claim using a JSONPath-like expression.
49
+ *
50
+ * Simple dot-notation path (e.g. "$.starr_roles" or "starr_roles"):
51
+ * - If the resolved value is an array, it's checked for "admin".
52
+ * - If it's a string, it's checked as-is.
53
+ * - If it's a boolean, true=admin, false=defaultRole.
54
+ * - If not found, returns null (caller uses defaultRole).
55
+ */
56
+ private resolveRoleFromClaim;
57
+ /** Delete auth states older than 10 minutes (capped at 100 rows). */
58
+ private cleanupExpiredStates;
59
+ }
60
+ //# sourceMappingURL=oidc-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-flow.d.ts","sourceRoot":"","sources":["../../../src/auth/oidc/oidc-flow.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI3D,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,IAAI,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,QAAQ;IAEjB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;gBAJN,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,eAAe,EAAE,mBAAmB,EACpC,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,MAAM,EAAI,+BAA+B;IAClD,MAAM,EAAE,MAAM;IAGxB;;;;;;;;;;;;OAYG;IACG,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAkB,GAC7B,OAAO,CAAC,MAAM,CAAC;IAqDlB;;;;;;;;;;;OAWG;IACG,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,CAAC;IA2F9B;;;;;;;;OAQG;IACH,OAAO,CAAC,oBAAoB;IAkC5B,qEAAqE;IACrE,OAAO,CAAC,oBAAoB;CAS7B"}
@@ -0,0 +1,201 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+ import { discovery, buildAuthorizationUrl, authorizationCodeGrant, randomPKCECodeVerifier, calculatePKCECodeChallenge, randomState, randomNonce, } from "openid-client";
4
+ const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
5
+ export class OidcFlow {
6
+ db;
7
+ providerService;
8
+ userService;
9
+ baseUrl;
10
+ logger;
11
+ constructor(db, providerService, userService, baseUrl, // e.g. "http://localhost:3457"
12
+ logger) {
13
+ this.db = db;
14
+ this.providerService = providerService;
15
+ this.userService = userService;
16
+ this.baseUrl = baseUrl;
17
+ this.logger = logger;
18
+ }
19
+ /**
20
+ * Stage 1 — Initiate the OIDC login flow.
21
+ *
22
+ * 1. Look up the provider configuration.
23
+ * 2. Discover the OIDC issuer's endpoints (via .well-known).
24
+ * 3. Generate PKCE code verifier and challenge, state, nonce.
25
+ * 4. Store the state in oidc_auth_states.
26
+ * 5. Return the redirect URL to send the user's browser to.
27
+ *
28
+ * @param providerId The OIDC provider's UUID.
29
+ * @param redirectTo Where to send the user after successful login (e.g. "/browse").
30
+ * @returns The authorization URL to redirect the browser to.
31
+ */
32
+ async initiateLogin(providerId, redirectTo = "/browse") {
33
+ const provider = this.providerService.get(providerId);
34
+ if (!provider) {
35
+ throw new Error(`OIDC provider not found: ${providerId}`);
36
+ }
37
+ if (!provider.enabled) {
38
+ throw new Error(`OIDC provider is disabled: ${providerId}`);
39
+ }
40
+ // Discover the OIDC issuer's metadata
41
+ const config = await discovery(new URL(provider.issuerUrl), provider.clientId, { client_secret: provider.clientSecret });
42
+ // Generate PKCE and state values
43
+ const codeVerifier = randomPKCECodeVerifier();
44
+ const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
45
+ const state = randomState();
46
+ const nonce = randomNonce();
47
+ // Store the auth state
48
+ this.db.prepare(`
49
+ INSERT INTO oidc_auth_states (state, nonce, code_verifier, provider_id, redirect_to, created_at)
50
+ VALUES (@state, @nonce, @codeVerifier, @providerId, @redirectTo, datetime('now'))
51
+ `).run({
52
+ state,
53
+ nonce,
54
+ codeVerifier,
55
+ providerId: provider.id,
56
+ redirectTo,
57
+ });
58
+ // Build the authorization URL
59
+ const authUrl = buildAuthorizationUrl(config, {
60
+ redirect_uri: `${this.baseUrl}/api/oidc/callback/${provider.id}`,
61
+ scope: provider.scopes,
62
+ state,
63
+ nonce,
64
+ code_challenge: codeChallenge,
65
+ code_challenge_method: "S256",
66
+ response_type: "code",
67
+ });
68
+ this.logger.info("OIDC login initiated", {
69
+ providerId: provider.id,
70
+ providerName: provider.name,
71
+ });
72
+ return authUrl.href;
73
+ }
74
+ /**
75
+ * Stage 2 — Handle the OIDC callback.
76
+ *
77
+ * 1. Validate the state (CSRF protection).
78
+ * 2. Look up stored auth state from DB.
79
+ * 3. Exchange authorization code for tokens.
80
+ * 4. Validate the ID token (handled by openid-client).
81
+ * 5. Find or create the user.
82
+ * 6. Clean up the used auth state.
83
+ *
84
+ * @returns The authenticated user and whether they were just created.
85
+ */
86
+ async handleCallback(providerId, code, state) {
87
+ // 1. Validate the state (CSRF)
88
+ if (!state || !code) {
89
+ throw new Error("Missing state or code parameter");
90
+ }
91
+ // 2. Look up the stored auth state
92
+ const authState = this.db.prepare(`
93
+ SELECT * FROM oidc_auth_states WHERE state = @state AND provider_id = @providerId
94
+ `).get({ state, providerId });
95
+ if (!authState) {
96
+ throw new Error("Invalid or expired OIDC state");
97
+ }
98
+ // Delete the used state now (before any further processing)
99
+ this.db.prepare("DELETE FROM oidc_auth_states WHERE state = @state").run({ state });
100
+ // 3. Clean up any expired auth states (older than 10 minutes)
101
+ this.cleanupExpiredStates();
102
+ // 4. Get the provider config and exchange the code
103
+ const provider = this.providerService.get(providerId);
104
+ if (!provider) {
105
+ throw new Error(`OIDC provider not found: ${providerId}`);
106
+ }
107
+ const config = await discovery(new URL(provider.issuerUrl), provider.clientId, { client_secret: provider.clientSecret });
108
+ // Build the callback URL with code + state params (as the provider would redirect to)
109
+ const callbackUrl = new URL(`${this.baseUrl}/api/oidc/callback/${provider.id}`);
110
+ callbackUrl.searchParams.set("code", code);
111
+ callbackUrl.searchParams.set("state", state);
112
+ // Exchange the authorization code for tokens.
113
+ // openid-client automatically:
114
+ // - Validates the state parameter
115
+ // - Validates the ID token (signature, iss, aud, exp, expectedNonce)
116
+ // - Fetches the JWKS from the provider
117
+ const tokenSet = await authorizationCodeGrant(config, callbackUrl, {
118
+ expectedNonce: authState.nonce,
119
+ pkceCodeVerifier: authState.code_verifier,
120
+ expectedState: state,
121
+ });
122
+ // 5. Extract claims from the ID token
123
+ const idTokenClaims = tokenSet.claims();
124
+ if (!idTokenClaims) {
125
+ throw new Error("OIDC provider did not return an ID token");
126
+ }
127
+ // Cast claims to access OIDC standard claims (preferred_username, name, etc.)
128
+ const claims = idTokenClaims;
129
+ this.logger.info("OIDC callback successful", {
130
+ providerId,
131
+ sub: String(claims.sub ?? ""),
132
+ email: claims.email,
133
+ });
134
+ // 6. Resolve the role from the provider's role_attribute, if configured
135
+ let role = provider.defaultRole;
136
+ if (provider.roleAttribute) {
137
+ const resolvedRole = this.resolveRoleFromClaim(claims, provider.roleAttribute, provider.defaultRole);
138
+ if (resolvedRole)
139
+ role = resolvedRole;
140
+ }
141
+ // 7. Find or create the user
142
+ const { user, created } = this.userService.findOrCreateFromOidc({
143
+ sub: String(claims.sub ?? ""),
144
+ preferred_username: claims.preferred_username,
145
+ name: claims.name,
146
+ picture: claims.picture,
147
+ email: claims.email,
148
+ }, providerId, role);
149
+ return { user, providerId, created, redirectTo: authState.redirect_to ?? "/browse" };
150
+ }
151
+ /**
152
+ * Resolve a Starr role from an OIDC claim using a JSONPath-like expression.
153
+ *
154
+ * Simple dot-notation path (e.g. "$.starr_roles" or "starr_roles"):
155
+ * - If the resolved value is an array, it's checked for "admin".
156
+ * - If it's a string, it's checked as-is.
157
+ * - If it's a boolean, true=admin, false=defaultRole.
158
+ * - If not found, returns null (caller uses defaultRole).
159
+ */
160
+ resolveRoleFromClaim(claims, roleAttribute, defaultRole) {
161
+ try {
162
+ // Simple dot-notation path resolution
163
+ const path = roleAttribute.replace(/^\$\.?/, ""); // strip leading "$." or "$"
164
+ const keys = path.split(".");
165
+ let value = claims;
166
+ for (const key of keys) {
167
+ if (value && typeof value === "object" && key in value) {
168
+ value = value[key];
169
+ }
170
+ else {
171
+ return null; // path not found
172
+ }
173
+ }
174
+ // Interpret the value
175
+ if (Array.isArray(value)) {
176
+ return value.includes("admin") ? "admin" : defaultRole;
177
+ }
178
+ if (typeof value === "string") {
179
+ return value === "admin" ? "admin" : (value === "user" ? "user" : null);
180
+ }
181
+ if (typeof value === "boolean") {
182
+ return value ? "admin" : defaultRole;
183
+ }
184
+ }
185
+ catch {
186
+ // If anything goes wrong resolving the role, fall back to default
187
+ }
188
+ return null;
189
+ }
190
+ /** Delete auth states older than 10 minutes (capped at 100 rows). */
191
+ cleanupExpiredStates() {
192
+ // Use SQLite's datetime arithmetic for consistent comparison with
193
+ // the stored datetime('now') format (no T, no Z).
194
+ this.db.prepare(`
195
+ DELETE FROM oidc_auth_states
196
+ WHERE created_at < datetime('now', @offset)
197
+ LIMIT 100
198
+ `).run({ offset: `-${STATE_TTL_MS / 1000} seconds` });
199
+ }
200
+ }
201
+ //# sourceMappingURL=oidc-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-flow.js","sourceRoot":"","sources":["../../../src/auth/oidc/oidc-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { Logger } from "../../types/plugin.js";
3
+ /**
4
+ * Shape of an OIDC provider as stored.
5
+ * client_secret is encrypted at rest.
6
+ */
7
+ export interface OidcProvider {
8
+ id: string;
9
+ name: string;
10
+ issuerUrl: string;
11
+ clientId: string;
12
+ clientSecret: string;
13
+ scopes: string;
14
+ roleAttribute: string | null;
15
+ defaultRole: "admin" | "user";
16
+ enabled: boolean;
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ }
20
+ /**
21
+ * Public-facing shape (no secret, for the login page).
22
+ */
23
+ export interface OidcProviderPublic {
24
+ id: string;
25
+ name: string;
26
+ enabled: boolean;
27
+ }
28
+ /** Shape used when creating or updating a provider. */
29
+ export interface OidcProviderInput {
30
+ name: string;
31
+ issuerUrl: string;
32
+ clientId: string;
33
+ clientSecret: string;
34
+ scopes?: string;
35
+ roleAttribute?: string | null;
36
+ defaultRole?: "admin" | "user";
37
+ enabled?: boolean;
38
+ }
39
+ export declare class OidcProviderService {
40
+ private db;
41
+ private encrypt;
42
+ private decrypt;
43
+ private logger;
44
+ constructor(db: Database.Database, encrypt: (plaintext: string) => string, decrypt: (ciphertext: string) => string, logger: Logger);
45
+ /** List all providers (with encrypted secrets, for admin API). */
46
+ list(): OidcProvider[];
47
+ /** List only enabled providers (public info, no secrets). */
48
+ listEnabled(): OidcProviderPublic[];
49
+ /** Get a single provider by ID. */
50
+ get(id: string): OidcProvider | null;
51
+ /** Create a new provider. Returns the created provider. */
52
+ create(input: OidcProviderInput): OidcProvider;
53
+ /** Update an existing provider. Fields that are undefined are not changed. */
54
+ update(id: string, input: Partial<OidcProviderInput>): OidcProvider | null;
55
+ /** Delete a provider. Also cleans up auth states referencing it (CASCADE). */
56
+ delete(id: string): boolean;
57
+ private rowToProvider;
58
+ }
59
+ //# sourceMappingURL=oidc-provider-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-provider-service.d.ts","sourceRoot":"","sources":["../../../src/auth/oidc/oidc-provider-service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,uDAAuD;AACvD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;gBAHN,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EACtC,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,EACvC,MAAM,EAAE,MAAM;IAGxB,kEAAkE;IAClE,IAAI,IAAI,YAAY,EAAE;IAOtB,6DAA6D;IAC7D,WAAW,IAAI,kBAAkB,EAAE;IAWnC,mCAAmC;IACnC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAOpC,2DAA2D;IAC3D,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY;IA2B9C,8EAA8E;IAC9E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,YAAY,GAAG,IAAI;IAmC1E,8EAA8E;IAC9E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAW3B,OAAO,CAAC,aAAa;CAetB"}
@@ -0,0 +1,124 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+ import { randomUUID } from "node:crypto";
4
+ export class OidcProviderService {
5
+ db;
6
+ encrypt;
7
+ decrypt;
8
+ logger;
9
+ constructor(db, encrypt, decrypt, logger) {
10
+ this.db = db;
11
+ this.encrypt = encrypt;
12
+ this.decrypt = decrypt;
13
+ this.logger = logger;
14
+ }
15
+ /** List all providers (with encrypted secrets, for admin API). */
16
+ list() {
17
+ const rows = this.db
18
+ .prepare("SELECT * FROM oidc_providers ORDER BY name ASC")
19
+ .all();
20
+ return rows.map(this.rowToProvider);
21
+ }
22
+ /** List only enabled providers (public info, no secrets). */
23
+ listEnabled() {
24
+ const rows = this.db
25
+ .prepare("SELECT id, name, enabled FROM oidc_providers WHERE enabled = 1 ORDER BY name ASC")
26
+ .all();
27
+ return rows.map((r) => ({
28
+ id: r.id,
29
+ name: r.name,
30
+ enabled: !!r.enabled,
31
+ }));
32
+ }
33
+ /** Get a single provider by ID. */
34
+ get(id) {
35
+ const row = this.db
36
+ .prepare("SELECT * FROM oidc_providers WHERE id = @id")
37
+ .get({ id });
38
+ return row ? this.rowToProvider(row) : null;
39
+ }
40
+ /** Create a new provider. Returns the created provider. */
41
+ create(input) {
42
+ const id = randomUUID();
43
+ const encryptedSecret = this.encrypt(input.clientSecret);
44
+ const scopes = input.scopes ?? "openid profile email";
45
+ const defaultRole = input.defaultRole ?? "user";
46
+ this.db.prepare(`
47
+ INSERT INTO oidc_providers (id, name, issuer_url, client_id, client_secret,
48
+ scopes, role_attribute, default_role, enabled)
49
+ VALUES (@id, @name, @issuerUrl, @clientId, @clientSecret,
50
+ @scopes, @roleAttribute, @defaultRole, @enabled)
51
+ `).run({
52
+ id,
53
+ name: input.name,
54
+ issuerUrl: input.issuerUrl,
55
+ clientId: input.clientId,
56
+ clientSecret: encryptedSecret,
57
+ scopes,
58
+ roleAttribute: input.roleAttribute ?? null,
59
+ defaultRole,
60
+ enabled: input.enabled !== false ? 1 : 0,
61
+ });
62
+ this.logger.info("OIDC provider created", { id, name: input.name });
63
+ return this.get(id);
64
+ }
65
+ /** Update an existing provider. Fields that are undefined are not changed. */
66
+ update(id, input) {
67
+ const existing = this.get(id);
68
+ if (!existing)
69
+ return null;
70
+ const name = input.name ?? existing.name;
71
+ const issuerUrl = input.issuerUrl ?? existing.issuerUrl;
72
+ const clientId = input.clientId ?? existing.clientId;
73
+ const clientSecret = input.clientSecret
74
+ ? this.encrypt(input.clientSecret)
75
+ : existing.clientSecret; // Keep existing encrypted value if no new secret provided
76
+ const scopes = input.scopes ?? existing.scopes;
77
+ const roleAttribute = input.roleAttribute !== undefined
78
+ ? (input.roleAttribute ?? null)
79
+ : existing.roleAttribute;
80
+ const defaultRole = input.defaultRole ?? existing.defaultRole;
81
+ const enabled = input.enabled !== undefined ? (input.enabled ? 1 : 0) : (existing.enabled ? 1 : 0);
82
+ this.db.prepare(`
83
+ UPDATE oidc_providers SET
84
+ name = @name,
85
+ issuer_url = @issuerUrl,
86
+ client_id = @clientId,
87
+ client_secret = @clientSecret,
88
+ scopes = @scopes,
89
+ role_attribute = @roleAttribute,
90
+ default_role = @defaultRole,
91
+ enabled = @enabled,
92
+ updated_at = datetime('now')
93
+ WHERE id = @id
94
+ `).run({ id, name, issuerUrl, clientId, clientSecret, scopes, roleAttribute, defaultRole, enabled });
95
+ this.logger.info("OIDC provider updated", { id, name });
96
+ return this.get(id);
97
+ }
98
+ /** Delete a provider. Also cleans up auth states referencing it (CASCADE). */
99
+ delete(id) {
100
+ const existing = this.get(id);
101
+ if (!existing)
102
+ return false;
103
+ this.db.prepare("DELETE FROM oidc_providers WHERE id = @id").run({ id });
104
+ this.logger.info("OIDC provider deleted", { id, name: existing.name });
105
+ return true;
106
+ }
107
+ // ── Private helpers ──
108
+ rowToProvider(row) {
109
+ return {
110
+ id: row.id,
111
+ name: row.name,
112
+ issuerUrl: row.issuer_url,
113
+ clientId: row.client_id,
114
+ clientSecret: this.decrypt(row.client_secret),
115
+ scopes: row.scopes,
116
+ roleAttribute: row.role_attribute,
117
+ defaultRole: row.default_role,
118
+ enabled: !!row.enabled,
119
+ createdAt: row.created_at,
120
+ updatedAt: row.updated_at,
121
+ };
122
+ }
123
+ }
124
+ //# sourceMappingURL=oidc-provider-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-provider-service.js","sourceRoot":"","sources":["../../../src/auth/oidc/oidc-provider-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { Logger } from "../../types/plugin.js";
3
+ export interface User {
4
+ id: string;
5
+ username: string;
6
+ displayName: string | null;
7
+ avatarUrl: string | null;
8
+ role: "admin" | "user";
9
+ provider: "local" | "oidc";
10
+ oidcSub: string | null;
11
+ oidcProviderId: string | null;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ }
15
+ /** Shape for updating user role (admin-only). */
16
+ export interface UserRoleUpdate {
17
+ role: "admin" | "user";
18
+ }
19
+ export declare class UserService {
20
+ private db;
21
+ private logger;
22
+ constructor(db: Database.Database, logger: Logger);
23
+ /** Get a user by UUID or the fixed admin id. */
24
+ get(id: string): User | null;
25
+ /** Get a user by username. */
26
+ getByUsername(username: string): User | null;
27
+ /**
28
+ * Find a user by OIDC subject and provider.
29
+ * This is the primary lookup during OIDC callback.
30
+ */
31
+ getByOidcSub(oidcSub: string, oidcProviderId: string): User | null;
32
+ /**
33
+ * Find or create a user from OIDC claims during callback.
34
+ *
35
+ * Steps:
36
+ * 1. Look up by (oidc_sub, oidc_provider_id).
37
+ * 2. If found, update display_name, avatar_url, username from claims.
38
+ * 3. If not found, create a new user with provider='oidc'.
39
+ *
40
+ * Returns the user and a boolean indicating whether it was newly created.
41
+ */
42
+ findOrCreateFromOidc(claims: {
43
+ sub: string;
44
+ preferred_username?: string;
45
+ name?: string;
46
+ picture?: string;
47
+ email?: string;
48
+ }, oidcProviderId: string, defaultRole: "admin" | "user"): {
49
+ user: User;
50
+ created: boolean;
51
+ };
52
+ /** List all users (admin only). */
53
+ list(): User[];
54
+ /** Count how many admin users exist. */
55
+ countAdmins(): number;
56
+ /** Change a user's role. Returns the updated user or null if not found. */
57
+ setRole(id: string, role: "admin" | "user"): User | null;
58
+ /** Check if there is at least one admin user. */
59
+ hasAdmin(): boolean;
60
+ /**
61
+ * Resolve a user identifier (UUID or "admin") to a user.
62
+ * Used when validating sessions that may have either format in user_id.
63
+ */
64
+ resolveUser(userId: string): User | null;
65
+ private rowToUser;
66
+ }
67
+ //# sourceMappingURL=user-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-service.d.ts","sourceRoot":"","sources":["../../../src/auth/oidc/user-service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;CACxB;AAED,qBAAa,WAAW;IAEpB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,MAAM;gBADN,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM;IAGxB,gDAAgD;IAChD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAO5B,8BAA8B;IAC9B,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAO5C;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IASlE;;;;;;;;;OASG;IACH,oBAAoB,CAClB,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;QACZ,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,EACD,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,OAAO,GAAG,MAAM,GAC5B;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE;IAiEnC,mCAAmC;IACnC,IAAI,IAAI,IAAI,EAAE;IAOd,wCAAwC;IACxC,WAAW,IAAI,MAAM;IAOrB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI;IA4BxD,iDAAiD;IACjD,QAAQ,IAAI,OAAO;IAOnB;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAUxC,OAAO,CAAC,SAAS;CAclB"}