@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,110 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+
4
+ import type { ContentTypeDescriptor } from "@digitalpaws/starr-types-core";
5
+
6
+ export interface PluginMeta {
7
+ id: string;
8
+ version: string;
9
+ starrApiVersion: string;
10
+ }
11
+
12
+ export interface ConnectionTestResult {
13
+ ok: boolean;
14
+ message?: string;
15
+ }
16
+
17
+ export interface PluginLifecycle {
18
+ start?(): void | Promise<void>;
19
+ stop?(): void | Promise<void>;
20
+ testConnection?(
21
+ values: Record<string, unknown>,
22
+ ): ConnectionTestResult | Promise<ConnectionTestResult>;
23
+ }
24
+
25
+ export interface Logger {
26
+ debug(message: string, meta?: Record<string, unknown>): void;
27
+ info(message: string, meta?: Record<string, unknown>): void;
28
+ warn(message: string, meta?: Record<string, unknown>): void;
29
+ error(message: string, meta?: Record<string, unknown>): void;
30
+ child(tag: string): Logger;
31
+ }
32
+
33
+ export interface StarrEvent<P = unknown> {
34
+ readonly type: string;
35
+ readonly id: string;
36
+ readonly traceId: string;
37
+ readonly correlationId?: string;
38
+ readonly source: string;
39
+ readonly timestamp: string; // ISO 8601
40
+ readonly schemaVersion: number;
41
+ readonly payload: P;
42
+ /** Convenience: emit a correlated reply. */
43
+ reply<R>(type: string, payload: R): void;
44
+ }
45
+
46
+ export interface EmitOpts {
47
+ correlationId?: string;
48
+ }
49
+
50
+ export interface RequestOpts {
51
+ responseType: string;
52
+ windowMs: number;
53
+ max?: number;
54
+ }
55
+
56
+ export type Unsubscribe = () => void;
57
+
58
+ export type EventHandler<P = unknown> = (event: StarrEvent<P>) => void | Promise<void>;
59
+
60
+ export interface EventBus {
61
+ emit<P>(type: string, payload: P, opts?: EmitOpts): void;
62
+ on<P>(type: string, handler: EventHandler<P>): Unsubscribe;
63
+ request<Res>(type: string, payload: unknown, opts: RequestOpts): Promise<StarrEvent<Res>[]>;
64
+ }
65
+
66
+ export interface PluginStorage {
67
+ get<T>(key: string): Promise<T | undefined>;
68
+ set<T>(key: string, value: T): Promise<void>;
69
+ delete(key: string): Promise<void>;
70
+ /** List all keys (optionally filtered by prefix), sorted lexically. */
71
+ list<T>(prefix?: string): Promise<Array<{ key: string; value: T }>>;
72
+ /** Delete all keys for this plugin's namespace. Used during rollback. */
73
+ deleteAll(): Promise<void>;
74
+ }
75
+
76
+ export interface ContentTypeRegistry {
77
+ /**
78
+ * Register a content-type descriptor. Throws on:
79
+ * - Malformed descriptor (empty identity, list without itemType,
80
+ * enum without options)
81
+ * - Duplicate id already registered by another plugin
82
+ */
83
+ register(descriptor: ContentTypeDescriptor): void;
84
+ }
85
+
86
+ export interface ConfigField {
87
+ type: string;
88
+ label: string;
89
+ required?: boolean;
90
+ default?: unknown;
91
+ options?: string[];
92
+ description?: string;
93
+ group?: string;
94
+ }
95
+
96
+ export type ConfigSchema<T> = { [K in keyof T]: ConfigField };
97
+
98
+ export interface PluginContext {
99
+ readonly apiVersion: string;
100
+ define(meta: PluginMeta): void;
101
+ config<T>(schema: ConfigSchema<T>): Readonly<T>;
102
+ events: EventBus;
103
+ storage: PluginStorage;
104
+ contentTypes: ContentTypeRegistry;
105
+ logger: Logger;
106
+ }
107
+
108
+ export type RegisterFn = (
109
+ ctx: PluginContext
110
+ ) => void | PluginLifecycle | Promise<void | PluginLifecycle>;
@@ -0,0 +1,182 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+
4
+ import Fastify from "fastify";
5
+ import fastifyStatic from "@fastify/static";
6
+ import fastifyCookie from "@fastify/cookie";
7
+ import fastifyFormbody from "@fastify/formbody";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import type { Logger } from "../types/plugin.js";
11
+ import type { AuthService } from "../auth/auth-service.js";
12
+ import type { OidcProviderService } from "../auth/oidc/oidc-provider-service.js";
13
+ import type { OidcFlow } from "../auth/oidc/oidc-flow.js";
14
+ import type { UserService } from "../auth/oidc/user-service.js";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ // ── Deferred AuthService reference ───────────────────────
19
+ // Set by main.ts via initAuthService() after AuthService is created.
20
+ let authService: AuthService | null = null;
21
+
22
+ export function initAuthService(auth: AuthService): void {
23
+ authService = auth;
24
+ }
25
+
26
+ // ── Deferred OIDC service references ─────────────────────
27
+ let oidcProviderService: OidcProviderService | null = null;
28
+ let oidcFlow: OidcFlow | null = null;
29
+ let userService: UserService | null = null;
30
+
31
+ export function initOidcServices(
32
+ oidcProviders: OidcProviderService,
33
+ flow: OidcFlow,
34
+ users: UserService,
35
+ ): void {
36
+ oidcProviderService = oidcProviders;
37
+ oidcFlow = flow;
38
+ userService = users;
39
+ }
40
+
41
+ // ── Server factory ───────────────────────────────────────
42
+
43
+ export async function createWebServer(logger: Logger) {
44
+ // forceCloseConnections ensures SSE and other persistent connections
45
+ // are forcefully torn down on close(), preventing shutdown hangs
46
+ // that keep the port bound (the root cause of EADDRINUSE on restart).
47
+ const server = Fastify({ logger: false, forceCloseConnections: true });
48
+
49
+ // Parse cookies (needed for session)
50
+ await server.register(fastifyCookie);
51
+
52
+ // Parse form/multipart bodies (needed for auth, config)
53
+ await server.register(fastifyFormbody);
54
+
55
+ // ---- AUTH HOOK ----
56
+ // All /api/* routes require a valid session, except an explicit
57
+ // allowlist of public endpoints (login, setup, OIDC initiation, etc.).
58
+ // Non-API routes (static files) are always public.
59
+ server.addHook("onRequest", async (request, reply) => {
60
+ // Non-API routes (static files) are always public
61
+ if (!request.url.startsWith("/api/")) {
62
+ return;
63
+ }
64
+
65
+ // Public paths that don't require authentication
66
+ const publicPaths = [
67
+ "/api/auth/login",
68
+ "/api/auth/setup",
69
+ "/api/auth/needs-setup",
70
+ "/api/auth/status",
71
+ "/api/auth/logout", // safe — only clears cookie
72
+ "/api/oidc/providers", // login page needs this before auth
73
+ "/api/oidc/login/", // initiates OIDC flow
74
+ "/api/oidc/callback/", // OIDC provider redirects here
75
+ ];
76
+
77
+ for (const pub of publicPaths) {
78
+ if (request.url === pub || request.url.startsWith(pub)) {
79
+ return; // skip auth for public paths
80
+ }
81
+ }
82
+
83
+ // All other /api/* routes require a valid session
84
+ const sid = request.cookies["starr_sid"];
85
+ if (!sid) {
86
+ reply.status(401).send({ error: "Unauthorized" });
87
+ return;
88
+ }
89
+
90
+ if (!authService) {
91
+ reply.status(500).send({ error: "auth service not available" });
92
+ return;
93
+ }
94
+
95
+ const result = authService.validateToken(sid);
96
+ if (!result) {
97
+ reply.status(401).send({ error: "Invalid session" });
98
+ return;
99
+ }
100
+
101
+ // Attach user info including role and id
102
+ (request as any).user = {
103
+ id: result.userId,
104
+ username: result.userId,
105
+ role: result.role ?? "user",
106
+ };
107
+ });
108
+
109
+ // ---- REGISTER API ROUTES ----
110
+ // Each route module is registered as a Fastify plugin.
111
+ // Dynamic import with try/catch means a missing route module won't crash the server.
112
+
113
+ const routeModules = [
114
+ "./routes/auth.js",
115
+ "./routes/discovery.js",
116
+ "./routes/content-types.js",
117
+ "./routes/requests.js",
118
+ "./routes/config.js",
119
+ "./routes/activity.js",
120
+ "./routes/sse.js",
121
+ "./routes/oidc.js",
122
+ "./routes/library.js",
123
+ "./routes/library-management.js",
124
+ "./routes/library-import.js",
125
+ ];
126
+
127
+ for (const modPath of routeModules) {
128
+ try {
129
+ const mod = await import(modPath);
130
+ await server.register(mod.default);
131
+ } catch (err: any) {
132
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
133
+ logger.warn(`route module not found (skipping): ${modPath}`);
134
+ } else {
135
+ logger.error(`failed to register route module: ${modPath}`, { error: String(err) });
136
+ }
137
+ }
138
+ }
139
+
140
+ // ---- INIT OIDC ROUTES ----
141
+ // OIDC routes need the services hooked up after registration
142
+ try {
143
+ const { initOidcRoutes } = await import("./routes/oidc.js");
144
+ if (authService && oidcProviderService && oidcFlow && userService) {
145
+ initOidcRoutes(authService, oidcProviderService, oidcFlow, userService);
146
+ } else {
147
+ logger.warn("OIDC services not fully initialized — skipping OIDC route init");
148
+ }
149
+ } catch (err: any) {
150
+ logger.error("failed to init OIDC routes", { error: String(err) });
151
+ }
152
+
153
+ // ---- STATIC FILE SERVING (SPA) ----
154
+ // In production, the Svelte build output is at ../../web-ui/dist
155
+ // In development, the Vite dev server proxies to the host; static files aren't served.
156
+ const spaDist = path.resolve(__dirname, "../../../web-ui/dist");
157
+ try {
158
+ await server.register(fastifyStatic, {
159
+ root: spaDist,
160
+ prefix: "/",
161
+ wildcard: false,
162
+ });
163
+
164
+ // SPA fallback: serve index.html for any non-API, non-file route
165
+ server.setNotFoundHandler(async (request, reply) => {
166
+ if (request.url.startsWith("/api/")) {
167
+ reply.status(404).send({ error: "not found" });
168
+ return;
169
+ }
170
+ // Serve index.html for client-side routing
171
+ await reply.sendFile("index.html");
172
+ });
173
+ } catch {
174
+ logger.warn(
175
+ "SPA static files not found (web-ui/dist missing); " +
176
+ "run `pnpm --filter starr-web-ui build` first"
177
+ );
178
+ }
179
+
180
+ logger.info("web server created");
181
+ return server;
182
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+
4
+ import type { FastifyPluginAsync } from "fastify";
5
+ import type { Starr } from "../../host.js";
6
+
7
+ let starrInstance: Starr | null = null;
8
+
9
+ export function initActivityRoutes(starr: Starr): void {
10
+ starrInstance = starr;
11
+ }
12
+
13
+ const activityRoutes: FastifyPluginAsync = async (server) => {
14
+ server.get<{ Params: { id: string } }>(
15
+ "/api/requests/:id/activity",
16
+ async (request, reply) => {
17
+ if (!starrInstance) {
18
+ reply.status(500).send({ error: "host not available" });
19
+ return;
20
+ }
21
+
22
+ const req = starrInstance.getStateStore().getByRequestId(request.params.id);
23
+ if (!req) {
24
+ reply.status(404).send({ error: "request not found" });
25
+ return;
26
+ }
27
+
28
+ // Use the trace-store wrapper module (Task 020 data surface).
29
+ // Gracefully degrades to empty array if the wrapper isn't wired yet.
30
+ try {
31
+ const { getTraceEvents } = await import("../../state/trace-store.js");
32
+ const events = getTraceEvents(req.requestId);
33
+ return { requestId: req.requestId, events };
34
+ } catch {
35
+ return { requestId: req.requestId, events: [] };
36
+ }
37
+ },
38
+ );
39
+ };
40
+
41
+ export default activityRoutes;
@@ -0,0 +1,170 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+
4
+ import type { FastifyPluginAsync } from "fastify";
5
+ import type { AuthService } from "../../auth/auth-service.js";
6
+ import { rateLimit } from "../../auth/rate-limiter.js";
7
+
8
+ /**
9
+ * This module needs access to the AuthService.
10
+ * We provide it via a module-level setter called from main.ts.
11
+ */
12
+ let authService: AuthService | null = null;
13
+
14
+ export function initAuthRoutes(auth: AuthService): void {
15
+ authService = auth;
16
+ }
17
+
18
+ const SESSION_COOKIE = "starr_sid";
19
+ const COOKIE_OPTS = {
20
+ httpOnly: true,
21
+ sameSite: "strict" as const,
22
+ path: "/",
23
+ secure: false,
24
+ // TTL from env or default 7 days (matches AuthService default)
25
+ maxAge: parseInt(process.env.STARR_SESSION_TTL_HOURS ?? "168", 10) * 3600,
26
+ };
27
+
28
+ const authRoutes: FastifyPluginAsync = async (server) => {
29
+ // GET /api/auth/needs-setup — check if admin account exists
30
+ server.get("/api/auth/needs-setup", async (_request, _reply) => {
31
+ const needsSetup = authService?.needsSetup() ?? true;
32
+ return { needsSetup };
33
+ });
34
+
35
+ // GET /api/auth/status — check if logged in
36
+ server.get("/api/auth/status", async (request, reply) => {
37
+ const sid = request.cookies[SESSION_COOKIE];
38
+ if (!sid || !authService) {
39
+ reply.status(401).send({ error: "Unauthorized" });
40
+ return;
41
+ }
42
+ const result = authService.validateToken(sid);
43
+ if (!result) {
44
+ reply.status(401).send({ error: "Invalid session" });
45
+ return;
46
+ }
47
+ // Return richer user info (display name, avatar, role)
48
+ const userInfo = authService.getUserInfo(result.userId);
49
+ return {
50
+ user: result.userId,
51
+ displayName: userInfo?.displayName ?? result.userId,
52
+ avatarUrl: userInfo?.avatarUrl ?? null,
53
+ role: result.role ?? "user",
54
+ };
55
+ });
56
+
57
+ // POST /api/auth/setup — first-run admin creation
58
+ server.post<{ Body: { username?: string; password?: string } }>(
59
+ "/api/auth/setup",
60
+ async (request, reply) => {
61
+ if (!authService) {
62
+ reply.status(500).send({ error: "auth service not available" });
63
+ return;
64
+ }
65
+ const { username, password } = request.body;
66
+ if (!username || !password) {
67
+ reply.status(400).send({ error: "username and password are required" });
68
+ return;
69
+ }
70
+
71
+ try {
72
+ await authService.setup(username, password);
73
+ } catch (err: unknown) {
74
+ const msg = err instanceof Error ? err.message : "Setup failed";
75
+ if (msg.includes("already exists")) {
76
+ reply.status(403).send({ error: msg });
77
+ return;
78
+ }
79
+ reply.status(400).send({ error: msg });
80
+ return;
81
+ }
82
+
83
+ // Log in immediately after setup
84
+ const jwt = await authService.login(username, password);
85
+ if (jwt) {
86
+ reply.setCookie(SESSION_COOKIE, jwt, COOKIE_OPTS);
87
+ return { ok: true };
88
+ }
89
+ reply.status(500).send({ error: "Account created but login failed" });
90
+ },
91
+ );
92
+
93
+ // POST /api/auth/login — rate-limited (5 attempts per minute per IP)
94
+ server.post<{ Body: { username?: string; password?: string } }>(
95
+ "/api/auth/login",
96
+ { preHandler: rateLimit(5, 1) },
97
+ async (request, reply) => {
98
+ if (!authService) {
99
+ reply.status(500).send({ error: "auth service not available" });
100
+ return;
101
+ }
102
+ const { username, password } = request.body;
103
+ if (!username || !password) {
104
+ reply.status(400).send({ error: "username and password are required" });
105
+ return;
106
+ }
107
+
108
+ const jwt = await authService.login(username, password);
109
+ if (!jwt) {
110
+ reply.status(401).send({ error: "Invalid credentials" });
111
+ return;
112
+ }
113
+
114
+ reply.setCookie(SESSION_COOKIE, jwt, COOKIE_OPTS);
115
+ return { ok: true };
116
+ },
117
+ );
118
+
119
+ // POST /api/auth/logout
120
+ server.post("/api/auth/logout", async (request, reply) => {
121
+ const sid = request.cookies[SESSION_COOKIE];
122
+ if (sid && authService) {
123
+ const result = authService.validateToken(sid);
124
+ if (result) {
125
+ authService.logout(result.sessionId);
126
+ }
127
+ }
128
+ reply.clearCookie(SESSION_COOKIE, { path: "/" });
129
+ return { ok: true };
130
+ });
131
+
132
+ // POST /api/auth/change-password
133
+ server.post<{ Body: { currentPassword?: string; newPassword?: string } }>(
134
+ "/api/auth/change-password",
135
+ async (request, reply) => {
136
+ if (!authService) {
137
+ reply.status(500).send({ error: "auth service not available" });
138
+ return;
139
+ }
140
+
141
+ const { currentPassword, newPassword } = request.body;
142
+ if (!currentPassword || !newPassword) {
143
+ reply.status(400).send({ error: "currentPassword and newPassword are required" });
144
+ return;
145
+ }
146
+
147
+ const user = (request as any).user;
148
+ const username = user?.username;
149
+
150
+ if (!username) {
151
+ reply.status(401).send({ error: "Unauthorized" });
152
+ return;
153
+ }
154
+
155
+ try {
156
+ await authService.changePassword(username, currentPassword, newPassword);
157
+ // Create a new session with the new password context
158
+ const newJwt = await authService.login(username, newPassword);
159
+ if (newJwt) {
160
+ reply.setCookie(SESSION_COOKIE, newJwt, COOKIE_OPTS);
161
+ }
162
+ return { ok: true };
163
+ } catch (err: unknown) {
164
+ reply.status(400).send({ error: err instanceof Error ? err.message : "Password change failed" });
165
+ }
166
+ },
167
+ );
168
+ };
169
+
170
+ export default authRoutes;