@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,340 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+ const CREATE_MIGRATIONS_TABLE = `
4
+ CREATE TABLE IF NOT EXISTS _migrations (
5
+ id INTEGER PRIMARY KEY,
6
+ name TEXT NOT NULL UNIQUE,
7
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
8
+ );
9
+ `;
10
+ export function runMigrations(db, migrations, logger) {
11
+ db.exec(CREATE_MIGRATIONS_TABLE);
12
+ const applied = new Set(db
13
+ .prepare("SELECT id FROM _migrations")
14
+ .all()
15
+ .map((row) => row.id));
16
+ const pending = migrations
17
+ .filter((m) => !applied.has(m.id))
18
+ .sort((a, b) => a.id - b.id);
19
+ if (pending.length === 0) {
20
+ logger.debug("no pending migrations");
21
+ return;
22
+ }
23
+ logger.info("running migrations", { count: pending.length });
24
+ const insert = db.prepare("INSERT INTO _migrations (id, name) VALUES (@id, @name)");
25
+ const runOne = db.transaction((migrations) => {
26
+ for (const m of migrations) {
27
+ logger.debug("applying migration", { id: m.id, name: m.name });
28
+ db.exec(m.up);
29
+ insert.run({ id: m.id, name: m.name });
30
+ }
31
+ });
32
+ runOne(pending);
33
+ logger.info("migrations complete");
34
+ }
35
+ // ── Core migrations ──────────────────────────────────────────
36
+ export const CORE_MIGRATIONS = [
37
+ {
38
+ id: 1,
39
+ name: "create_plugin_storage",
40
+ up: `
41
+ CREATE TABLE IF NOT EXISTS plugin_storage (
42
+ plugin_id TEXT NOT NULL,
43
+ key TEXT NOT NULL,
44
+ value TEXT NOT NULL,
45
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
46
+ PRIMARY KEY (plugin_id, key)
47
+ );
48
+ `,
49
+ },
50
+ {
51
+ id: 2,
52
+ name: "create_plugin_config",
53
+ up: `
54
+ CREATE TABLE IF NOT EXISTS plugin_config (
55
+ plugin_id TEXT NOT NULL PRIMARY KEY,
56
+ schema TEXT NOT NULL DEFAULT '{}',
57
+ "values" TEXT NOT NULL DEFAULT '{}',
58
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
59
+ );
60
+ `,
61
+ },
62
+ {
63
+ id: 3,
64
+ name: "create_tracked_requests",
65
+ up: `
66
+ CREATE TABLE IF NOT EXISTS tracked_requests (
67
+ request_id TEXT NOT NULL PRIMARY KEY,
68
+ content_type_id TEXT NOT NULL,
69
+ content_identity TEXT NOT NULL,
70
+ item TEXT NOT NULL,
71
+ owner_user_id TEXT NOT NULL,
72
+ watchers TEXT NOT NULL DEFAULT '[]',
73
+ status TEXT NOT NULL,
74
+ status_detail TEXT,
75
+ chosen_release_id TEXT,
76
+ attempts INTEGER NOT NULL DEFAULT 0,
77
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
78
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
79
+ );
80
+ CREATE INDEX IF NOT EXISTS idx_tracked_requests_identity
81
+ ON tracked_requests(content_identity);
82
+ `,
83
+ },
84
+ {
85
+ id: 4,
86
+ name: "create_trace_log",
87
+ up: `
88
+ CREATE TABLE IF NOT EXISTS trace_log (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ trace_id TEXT NOT NULL,
91
+ request_id TEXT NOT NULL,
92
+ event_type TEXT NOT NULL,
93
+ event_data TEXT NOT NULL,
94
+ timestamp TEXT NOT NULL
95
+ );
96
+ CREATE INDEX IF NOT EXISTS idx_trace_log_request
97
+ ON trace_log(request_id);
98
+ CREATE INDEX IF NOT EXISTS idx_trace_log_trace
99
+ ON trace_log(trace_id, id);
100
+ `,
101
+ },
102
+ {
103
+ id: 5,
104
+ name: "create_accounts_and_sessions",
105
+ up: `
106
+ CREATE TABLE IF NOT EXISTS admin_accounts (
107
+ username TEXT NOT NULL PRIMARY KEY,
108
+ password_hash TEXT NOT NULL,
109
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
110
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
111
+ );
112
+
113
+ CREATE TABLE IF NOT EXISTS sessions (
114
+ session_id TEXT NOT NULL PRIMARY KEY,
115
+ user_id TEXT NOT NULL REFERENCES admin_accounts(username) ON DELETE CASCADE,
116
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
117
+ expires_at TEXT NOT NULL
118
+ );
119
+
120
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id
121
+ ON sessions(user_id);
122
+ `,
123
+ },
124
+ {
125
+ id: 6,
126
+ name: "create_users_and_oidc_providers",
127
+ up: `
128
+ -- Users table: unified account for local admin and OIDC users
129
+ CREATE TABLE IF NOT EXISTS users (
130
+ id TEXT NOT NULL PRIMARY KEY, -- UUID
131
+ username TEXT NOT NULL UNIQUE,
132
+ display_name TEXT,
133
+ avatar_url TEXT,
134
+ role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin', 'user')),
135
+ provider TEXT NOT NULL DEFAULT 'local' CHECK(provider IN ('local', 'oidc')),
136
+ oidc_sub TEXT, -- OIDC 'sub' claim (unique per provider)
137
+ oidc_provider_id TEXT REFERENCES oidc_providers(id) ON DELETE SET NULL,
138
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
139
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
140
+ );
141
+
142
+ CREATE INDEX IF NOT EXISTS idx_users_oidc_sub
143
+ ON users(oidc_sub, oidc_provider_id);
144
+
145
+ -- OIDC provider configuration
146
+ CREATE TABLE IF NOT EXISTS oidc_providers (
147
+ id TEXT NOT NULL PRIMARY KEY, -- UUID
148
+ name TEXT NOT NULL,
149
+ issuer_url TEXT NOT NULL,
150
+ client_id TEXT NOT NULL,
151
+ client_secret TEXT NOT NULL, -- encrypted at rest
152
+ scopes TEXT NOT NULL DEFAULT 'openid profile email',
153
+ role_attribute TEXT, -- JSONPath to a claim, e.g. "$.starr_roles"
154
+ default_role TEXT NOT NULL DEFAULT 'user' CHECK(default_role IN ('admin', 'user')),
155
+ enabled INTEGER NOT NULL DEFAULT 1,
156
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
157
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
158
+ );
159
+
160
+ -- Temporary state for in-flight OIDC authorization flows
161
+ CREATE TABLE IF NOT EXISTS oidc_auth_states (
162
+ state TEXT NOT NULL PRIMARY KEY, -- random CSRF token
163
+ nonce TEXT NOT NULL,
164
+ code_verifier TEXT NOT NULL,
165
+ provider_id TEXT NOT NULL REFERENCES oidc_providers(id) ON DELETE CASCADE,
166
+ redirect_to TEXT NOT NULL DEFAULT '/browse',
167
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
168
+ );
169
+
170
+ CREATE INDEX IF NOT EXISTS idx_oidc_auth_states_created
171
+ ON oidc_auth_states(created_at);
172
+
173
+ -- Recreate sessions table without FK constraint so OIDC user UUIDs work
174
+ CREATE TABLE IF NOT EXISTS sessions_new (
175
+ session_id TEXT NOT NULL PRIMARY KEY,
176
+ user_id TEXT NOT NULL,
177
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
178
+ expires_at TEXT NOT NULL
179
+ );
180
+ INSERT INTO sessions_new (session_id, user_id, created_at, expires_at)
181
+ SELECT session_id, user_id, created_at, expires_at FROM sessions;
182
+ DROP TABLE sessions;
183
+ ALTER TABLE sessions_new RENAME TO sessions;
184
+
185
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id
186
+ ON sessions(user_id);
187
+
188
+ -- Migrate existing admin to users table
189
+ INSERT OR IGNORE INTO users (id, username, display_name, role, provider, created_at, updated_at)
190
+ SELECT
191
+ 'admin', -- id (fixed key for the local admin)
192
+ username, -- username from admin_accounts
193
+ username, -- display_name (same as username)
194
+ 'admin', -- role
195
+ 'local', -- provider
196
+ COALESCE(created_at, datetime('now')),
197
+ COALESCE(updated_at, datetime('now'))
198
+ FROM admin_accounts;
199
+ `,
200
+ },
201
+ {
202
+ id: 7,
203
+ name: "create_library_tracking",
204
+ up: `
205
+ -- Track the on-disk library path for each imported request
206
+ CREATE TABLE IF NOT EXISTS library_paths (
207
+ request_id TEXT NOT NULL PRIMARY KEY REFERENCES tracked_requests(request_id) ON DELETE CASCADE,
208
+ library_path TEXT NOT NULL,
209
+ library_root TEXT NOT NULL,
210
+ file_size INTEGER,
211
+ checksum TEXT,
212
+ imported_at TEXT NOT NULL DEFAULT (datetime('now')),
213
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
214
+ );
215
+
216
+ -- Add library_path column to tracked_requests for quick access
217
+ ALTER TABLE tracked_requests ADD COLUMN library_path TEXT;
218
+ `,
219
+ },
220
+ {
221
+ id: 8,
222
+ name: "create_library_cache",
223
+ up: `
224
+ -- Cache of imported item metadata for fast Browse loading.
225
+ -- Populated on import and refreshed on host start.
226
+ CREATE TABLE IF NOT EXISTS library_cache (
227
+ request_id TEXT NOT NULL PRIMARY KEY REFERENCES tracked_requests(request_id) ON DELETE CASCADE,
228
+ content_type_id TEXT NOT NULL,
229
+ content_identity TEXT NOT NULL,
230
+ title TEXT NOT NULL,
231
+ sort_title TEXT,
232
+ summary TEXT,
233
+ release_date TEXT,
234
+ images TEXT, -- JSON array of ContentImage
235
+ external_ids TEXT NOT NULL, -- JSON Record<string, string>
236
+ fields TEXT, -- JSON Record<string, unknown>
237
+ library_path TEXT NOT NULL,
238
+ file_size INTEGER,
239
+ imported_at TEXT NOT NULL,
240
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
241
+ );
242
+
243
+ CREATE INDEX IF NOT EXISTS idx_library_cache_content_type
244
+ ON library_cache(content_type_id);
245
+
246
+ CREATE INDEX IF NOT EXISTS idx_library_cache_identity
247
+ ON library_cache(content_identity);
248
+ `,
249
+ },
250
+ {
251
+ id: 9,
252
+ name: "create_libraries",
253
+ up: `
254
+ CREATE TABLE IF NOT EXISTS libraries (
255
+ id TEXT NOT NULL PRIMARY KEY,
256
+ name TEXT NOT NULL UNIQUE,
257
+ description TEXT,
258
+ is_default INTEGER NOT NULL DEFAULT 0,
259
+ default_library_root TEXT NOT NULL DEFAULT '/media/library',
260
+ roots TEXT NOT NULL DEFAULT '{}',
261
+ link_type TEXT NOT NULL DEFAULT 'hardlink' CHECK(link_type IN ('move','hardlink','symlink','copy')),
262
+ import_subtitles INTEGER NOT NULL DEFAULT 1,
263
+ subtitle_extensions TEXT NOT NULL DEFAULT '["srt","ass","ssa","sub","vtt"]',
264
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
265
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
266
+ );
267
+
268
+ CREATE TABLE IF NOT EXISTS library_content_type_defaults (
269
+ library_id TEXT NOT NULL REFERENCES libraries(id) ON DELETE CASCADE,
270
+ content_type_id TEXT NOT NULL,
271
+ PRIMARY KEY (content_type_id)
272
+ );
273
+
274
+ CREATE INDEX IF NOT EXISTS idx_library_content_type_defaults_library
275
+ ON library_content_type_defaults(library_id);
276
+ `,
277
+ },
278
+ {
279
+ id: 10,
280
+ name: "add_instance_id_to_plugin_tables",
281
+ up: `
282
+ CREATE TABLE IF NOT EXISTS plugin_config_new (
283
+ plugin_id TEXT NOT NULL,
284
+ instance_id TEXT NOT NULL DEFAULT 'default',
285
+ schema TEXT NOT NULL DEFAULT '{}',
286
+ "values" TEXT NOT NULL DEFAULT '{}',
287
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
288
+ PRIMARY KEY (plugin_id, instance_id)
289
+ );
290
+
291
+ INSERT INTO plugin_config_new (plugin_id, instance_id, schema, "values", updated_at)
292
+ SELECT plugin_id, 'default', schema, "values", updated_at FROM plugin_config;
293
+
294
+ DROP TABLE plugin_config;
295
+ ALTER TABLE plugin_config_new RENAME TO plugin_config;
296
+
297
+ CREATE TABLE IF NOT EXISTS plugin_storage_new (
298
+ plugin_id TEXT NOT NULL,
299
+ instance_id TEXT NOT NULL DEFAULT 'default',
300
+ key TEXT NOT NULL,
301
+ value TEXT NOT NULL,
302
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
303
+ PRIMARY KEY (plugin_id, instance_id, key)
304
+ );
305
+
306
+ INSERT INTO plugin_storage_new (plugin_id, instance_id, key, value, updated_at)
307
+ SELECT plugin_id, 'default', key, value, updated_at FROM plugin_storage;
308
+
309
+ DROP TABLE plugin_storage;
310
+ ALTER TABLE plugin_storage_new RENAME TO plugin_storage;
311
+ `,
312
+ },
313
+ {
314
+ id: 11,
315
+ name: "add_library_id_to_tracked_requests",
316
+ up: `
317
+ ALTER TABLE tracked_requests ADD COLUMN library_id TEXT REFERENCES libraries(id);
318
+ `,
319
+ },
320
+ {
321
+ id: 12,
322
+ name: "migrate_existing_imported_to_downloaded",
323
+ up: `
324
+ UPDATE tracked_requests
325
+ SET status = 'downloaded', updated_at = datetime('now')
326
+ WHERE status = 'imported';
327
+ `,
328
+ },
329
+ {
330
+ id: 13,
331
+ name: "remove_orphaned_core_configs",
332
+ up: `
333
+ -- Remove leftover _core.* config rows that were replaced by the
334
+ -- library service and multi-instance systems (Task 039).
335
+ DELETE FROM plugin_config WHERE plugin_id IN ('_core.library', '_core.downloads');
336
+ DELETE FROM plugin_storage WHERE plugin_id IN ('_core.library', '_core.downloads');
337
+ `,
338
+ },
339
+ ];
340
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/datastore/migrations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { PluginStorage } from "../types/plugin.js";
3
+ export declare function createPluginStorage(db: Database.Database, pluginId: string, instanceId?: string): PluginStorage;
4
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/datastore/storage.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,UAAU,SAAY,GACrB,aAAa,CAwEf"}
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+ export function createPluginStorage(db, pluginId, instanceId = "default") {
4
+ const getStmt = db.prepare("SELECT value FROM plugin_storage WHERE plugin_id = @pluginId AND instance_id = @instanceId AND key = @key");
5
+ const setStmt = db.prepare(`
6
+ INSERT INTO plugin_storage (plugin_id, instance_id, key, value, updated_at)
7
+ VALUES (@pluginId, @instanceId, @key, @value, datetime('now'))
8
+ ON CONFLICT(plugin_id, instance_id, key) DO UPDATE SET
9
+ value = @value,
10
+ updated_at = datetime('now')
11
+ `);
12
+ const deleteStmt = db.prepare("DELETE FROM plugin_storage WHERE plugin_id = @pluginId AND instance_id = @instanceId AND key = @key");
13
+ const listAllStmt = db.prepare(`SELECT key, value FROM plugin_storage
14
+ WHERE plugin_id = @pluginId AND instance_id = @instanceId
15
+ ORDER BY key`);
16
+ const listPrefixStmt = db.prepare(`SELECT key, value FROM plugin_storage
17
+ WHERE plugin_id = @pluginId AND instance_id = @instanceId AND key LIKE @prefix
18
+ ORDER BY key`);
19
+ const deleteAllStmt = db.prepare("DELETE FROM plugin_storage WHERE plugin_id = @pluginId AND instance_id = @instanceId");
20
+ return {
21
+ async get(key) {
22
+ const row = getStmt.get({ pluginId, instanceId, key });
23
+ if (!row)
24
+ return undefined;
25
+ try {
26
+ return JSON.parse(row.value);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ },
32
+ async set(key, value) {
33
+ setStmt.run({ pluginId, instanceId, key, value: JSON.stringify(value) });
34
+ },
35
+ async delete(key) {
36
+ deleteStmt.run({ pluginId, instanceId, key });
37
+ },
38
+ async list(prefix) {
39
+ const stmt = prefix !== undefined ? listPrefixStmt : listAllStmt;
40
+ const params = prefix !== undefined
41
+ ? { pluginId, instanceId, prefix: `${prefix}%` }
42
+ : { pluginId, instanceId };
43
+ const rows = stmt.all(params);
44
+ return rows.map((row) => {
45
+ let value;
46
+ try {
47
+ value = JSON.parse(row.value);
48
+ }
49
+ catch {
50
+ value = undefined;
51
+ }
52
+ return { key: row.key, value: value };
53
+ });
54
+ },
55
+ async deleteAll() {
56
+ deleteAllStmt.run({ pluginId, instanceId });
57
+ },
58
+ };
59
+ }
60
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/datastore/storage.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,4DAA4D;AAK5D,MAAM,UAAU,mBAAmB,CACjC,EAAqB,EACrB,QAAgB,EAChB,UAAU,GAAG,SAAS;IAEtB,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CACxB,2GAA2G,CAC5G,CAAC;IACF,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;GAM1B,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,EAAE,CAAC,OAAO,CAC3B,qGAAqG,CACtG,CAAC;IACF,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAC5B;;kBAEc,CACf,CAAC;IACF,MAAM,cAAc,GAAG,EAAE,CAAC,OAAO,CAC/B;;kBAEc,CACf,CAAC;IACF,MAAM,aAAa,GAAG,EAAE,CAAC,OAAO,CAC9B,sFAAsF,CACvF,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,GAAG,CAAI,GAAW;YACtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,CAExC,CAAC;YACd,IAAI,CAAC,GAAG;gBAAE,OAAO,SAAS,CAAC;YAC3B,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAM,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAED,KAAK,CAAC,GAAG,CAAI,GAAW,EAAE,KAAQ;YAChC,OAAO,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,GAAW;YACtB,UAAU,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QAChD,CAAC;QAED,KAAK,CAAC,IAAI,CACR,MAAe;YAEf,MAAM,IAAI,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC;YACjE,MAAM,MAAM,GAAG,MAAM,KAAK,SAAS;gBACjC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,EAAE;gBAChD,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAA0C,CAAC;YACvE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBACtB,IAAI,KAAoB,CAAC;gBACzB,IAAI,CAAC;oBACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAM,CAAC;gBACrC,CAAC;gBAAC,MAAM,CAAC;oBACP,KAAK,GAAG,SAAS,CAAC;gBACpB,CAAC;gBACD,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,KAAU,EAAE,CAAC;YAC7C,CAAC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,SAAS;YACb,aAAa,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;QAC9C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Logger } from "../types/plugin.js";
2
+ import type { EventBus } from "../types/plugin.js";
3
+ export declare class EventBusImpl {
4
+ private subscriptions;
5
+ private logger;
6
+ constructor(logger: Logger);
7
+ /** Returns an EventBus bound to the given plugin id. */
8
+ pluginBus(pluginId: string): EventBus;
9
+ /** Returns an EventBus with source="core". */
10
+ coreBus(): EventBus;
11
+ private _on;
12
+ private _emit;
13
+ private _request;
14
+ }
15
+ //# sourceMappingURL=event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/events/event-bus.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAEV,QAAQ,EAKT,MAAM,oBAAoB,CAAC;AAI5B,qBAAa,YAAY;IACvB,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,MAAM;IAM1B,wDAAwD;IACxD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ;IAUrC,8CAA8C;IAC9C,OAAO,IAAI,QAAQ;IAMnB,OAAO,CAAC,GAAG;IAcX,OAAO,CAAC,KAAK;IAuEb,OAAO,CAAC,QAAQ;CAkDjB"}
@@ -0,0 +1,140 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+ import { randomUUID } from "node:crypto";
4
+ import { runInTraceContext, getCurrentTraceId } from "./trace.js";
5
+ import { getSpineSchemaVersion, validateSpineEmission } from "./spine.js";
6
+ export class EventBusImpl {
7
+ subscriptions = new Map();
8
+ logger;
9
+ constructor(logger) {
10
+ this.logger = logger.child("event-bus");
11
+ }
12
+ // ---- Public factory methods ---- //
13
+ /** Returns an EventBus bound to the given plugin id. */
14
+ pluginBus(pluginId) {
15
+ return {
16
+ on: (type, handler) => this._on(type, handler),
17
+ emit: (type, payload, opts) => this._emit(pluginId, type, payload, opts),
18
+ request: (type, payload, opts) => this._request(pluginId, type, payload, opts),
19
+ };
20
+ }
21
+ /** Returns an EventBus with source="core". */
22
+ coreBus() {
23
+ return this.pluginBus("core");
24
+ }
25
+ // ---- Private implementation ---- //
26
+ _on(type, handler) {
27
+ if (!this.subscriptions.has(type)) {
28
+ this.subscriptions.set(type, new Set());
29
+ }
30
+ const handlers = this.subscriptions.get(type);
31
+ handlers.add(handler);
32
+ return () => {
33
+ handlers.delete(handler);
34
+ if (handlers.size === 0) {
35
+ this.subscriptions.delete(type);
36
+ }
37
+ };
38
+ }
39
+ _emit(source, type, payload, opts) {
40
+ const event = {
41
+ id: randomUUID(),
42
+ traceId: getCurrentTraceId(),
43
+ correlationId: opts?.correlationId,
44
+ source,
45
+ type,
46
+ timestamp: new Date().toISOString(),
47
+ schemaVersion: getSpineSchemaVersion(type),
48
+ payload,
49
+ reply: (replyType, replyPayload) => {
50
+ this._emit(source, replyType, replyPayload, {
51
+ correlationId: event.correlationId,
52
+ });
53
+ },
54
+ };
55
+ // Immutability (EN2)
56
+ Object.freeze(event);
57
+ this.logger.debug("emitting event", {
58
+ eventType: type,
59
+ source,
60
+ correlationId: event.correlationId,
61
+ });
62
+ // Spine validation — log at warn level if suspect (EBQ4, §9 CE2–CE3)
63
+ const violation = validateSpineEmission(source, type);
64
+ if (violation) {
65
+ this.logger.warn("spine emission warning", {
66
+ source,
67
+ eventType: type,
68
+ detail: violation,
69
+ });
70
+ }
71
+ // Look up handlers
72
+ const handlers = this.subscriptions.get(type);
73
+ if (!handlers || handlers.size === 0) {
74
+ return; // silently dropped (DL5)
75
+ }
76
+ // Dispatch to each handler — error isolation per handler (DL2)
77
+ const logger = this.logger;
78
+ for (const handler of handlers) {
79
+ try {
80
+ const result = runInTraceContext(event, () => handler(event));
81
+ if (result instanceof Promise) {
82
+ result.catch((err) => {
83
+ logger.error("event handler rejected", {
84
+ eventType: type,
85
+ handlerSource: source,
86
+ error: String(err),
87
+ });
88
+ });
89
+ }
90
+ }
91
+ catch (err) {
92
+ logger.error("event handler threw", {
93
+ eventType: type,
94
+ handlerSource: source,
95
+ error: String(err),
96
+ });
97
+ }
98
+ }
99
+ }
100
+ _request(source, type, payload, opts) {
101
+ // Validate opts — programming errors, fail fast
102
+ if (!opts.responseType || opts.responseType.trim() === "") {
103
+ throw new Error("request(): responseType is required and must be non-empty");
104
+ }
105
+ if (!Number.isFinite(opts.windowMs) || opts.windowMs <= 0) {
106
+ throw new Error(`request(): windowMs must be a positive number, got ${opts.windowMs}`);
107
+ }
108
+ if (opts.max !== undefined && (!Number.isInteger(opts.max) || opts.max < 1)) {
109
+ throw new Error(`request(): max must be a positive integer, got ${opts.max}`);
110
+ }
111
+ const correlationId = randomUUID();
112
+ const replies = [];
113
+ return new Promise((resolve) => {
114
+ let settled = false;
115
+ const timer = setTimeout(() => {
116
+ if (settled)
117
+ return;
118
+ settled = true;
119
+ unsub();
120
+ resolve(replies);
121
+ }, opts.windowMs);
122
+ const unsub = this._on(opts.responseType, (evt) => {
123
+ if (settled)
124
+ return;
125
+ if (evt.correlationId === correlationId) {
126
+ replies.push(evt);
127
+ if (opts.max !== undefined && replies.length >= opts.max) {
128
+ settled = true;
129
+ clearTimeout(timer);
130
+ unsub();
131
+ resolve(replies);
132
+ }
133
+ }
134
+ });
135
+ // Emit AFTER subscribing so synchronous replies are not missed
136
+ this._emit(source, type, payload, { correlationId });
137
+ });
138
+ }
139
+ }
140
+ //# sourceMappingURL=event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../src/events/event-bus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Reserved spine namespace prefixes.
3
+ * The dotted unprefixed namespace is reserved for the core spine.
4
+ */
5
+ export declare const SPINE_NAMESPACES: readonly ["content.", "release.", "download.", "import.", "library.", "discovery.", "item."];
6
+ export interface SpineEventMeta {
7
+ kind: "request" | "reply" | "notification";
8
+ /** Who is allowed to emit this event. */
9
+ producers: "core" | "metadata-plugin" | "indexer-plugin" | "selection-plugin" | "downloader-plugin";
10
+ schemaVersion: number;
11
+ }
12
+ /**
13
+ * The v1 spine event catalog (spec §8).
14
+ */
15
+ export declare const SPINE_EVENTS: Record<string, SpineEventMeta>;
16
+ /**
17
+ * Check whether an event type falls within the reserved spine namespace.
18
+ */
19
+ export declare function isSpineType(type: string): boolean;
20
+ /**
21
+ * Return the schemaVersion for a spine type, or 1 as default for custom events.
22
+ */
23
+ export declare function getSpineSchemaVersion(type: string): number;
24
+ /**
25
+ * Validate whether a source is allowed to emit the given spine event type.
26
+ *
27
+ * Returns an error message string if the emission is suspect, or null if it is fine.
28
+ * Per EBQ4 resolution, this only logs warnings; it does not reject.
29
+ */
30
+ export declare function validateSpineEmission(source: string, type: string): string | null;
31
+ //# sourceMappingURL=spine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spine.d.ts","sourceRoot":"","sources":["../../src/events/spine.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,eAAO,MAAM,gBAAgB,8FAQnB,CAAC;AAEX,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;IAC3C,yCAAyC;IACzC,SAAS,EACL,MAAM,GACN,iBAAiB,GACjB,gBAAgB,GAChB,kBAAkB,GAClB,mBAAmB,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAiBvD,CAAC;AAEF;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAejF"}