@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,345 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import Database from "better-sqlite3";
6
+ import { mkdtempSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+ import { randomBytes } from "node:crypto";
10
+
11
+ import { runMigrations, CORE_MIGRATIONS, type Migration } from "../src/datastore/migrations.js";
12
+ import { createPluginStorage } from "../src/datastore/storage.js";
13
+ import { loadEncryptionKey, encrypt, decrypt, resetEncryptionKey } from "../src/datastore/encrypt.js";
14
+ import { createContext } from "../src/plugins/context.js";
15
+ import { ConfigService } from "../src/config/config-service.js";
16
+ import { createLogger } from "../src/logging/logger.js";
17
+ import { Starr } from "../src/host.js";
18
+
19
+ // ── Helper: fresh in-memory database with migrations applied ───────────
20
+
21
+ function createTestDb(): Database.Database {
22
+ const db = new Database(":memory:");
23
+ db.pragma("journal_mode = WAL");
24
+ db.pragma("foreign_keys = ON");
25
+ return db;
26
+ }
27
+
28
+ const testLogger = createLogger("test", "silent");
29
+
30
+ // ── 7a. Migrations tests ───────────────────────────────────────────────
31
+
32
+ describe("migrations", () => {
33
+ it("creates _migrations and plugin_storage tables", () => {
34
+ const db = createTestDb();
35
+ runMigrations(db, CORE_MIGRATIONS, testLogger);
36
+
37
+ const tables = db
38
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
39
+ .all() as Array<{ name: string }>;
40
+ const tableNames = tables.map((t) => t.name);
41
+ expect(tableNames).toContain("_migrations");
42
+ expect(tableNames).toContain("plugin_storage");
43
+ });
44
+
45
+ it("is idempotent — second run does not error", () => {
46
+ const db = createTestDb();
47
+ runMigrations(db, CORE_MIGRATIONS, testLogger);
48
+ expect(() => runMigrations(db, CORE_MIGRATIONS, testLogger)).not.toThrow();
49
+
50
+ // Verify correct number of rows in _migrations (should match CORE_MIGRATIONS count)
51
+ const rows = db.prepare("SELECT COUNT(*) as cnt FROM _migrations").get() as { cnt: number };
52
+ expect(rows.cnt).toBe(CORE_MIGRATIONS.length);
53
+ });
54
+
55
+ it("applies custom migration and its side effects are visible", () => {
56
+ const db = createTestDb();
57
+ const customMigrations: Migration[] = [
58
+ {
59
+ id: 99,
60
+ name: "test_data",
61
+ up: "CREATE TABLE test_data (id INTEGER PRIMARY KEY, val TEXT); INSERT INTO test_data VALUES (1, 'hello');",
62
+ },
63
+ ];
64
+
65
+ runMigrations(db, [...CORE_MIGRATIONS, ...customMigrations], testLogger);
66
+
67
+ const row = db.prepare("SELECT val FROM test_data WHERE id = 1").get() as { val: string } | undefined;
68
+ expect(row?.val).toBe("hello");
69
+ });
70
+
71
+ it("runs migrations in id order", () => {
72
+ const db = createTestDb();
73
+ const order: number[] = [];
74
+
75
+ const orderedMigrations: Migration[] = [
76
+ {
77
+ id: 1,
78
+ name: "first",
79
+ up: `CREATE TABLE IF NOT EXISTS _order_test (step INTEGER); INSERT INTO _order_test VALUES (1);`,
80
+ },
81
+ {
82
+ id: 2,
83
+ name: "second",
84
+ up: `INSERT INTO _order_test VALUES (2);`,
85
+ },
86
+ {
87
+ id: 3,
88
+ name: "third",
89
+ up: `INSERT INTO _order_test VALUES (3);`,
90
+ },
91
+ ];
92
+
93
+ runMigrations(db, orderedMigrations, testLogger);
94
+
95
+ const steps = db.prepare("SELECT step FROM _order_test ORDER BY step").all() as Array<{ step: number }>;
96
+ expect(steps.map((s) => s.step)).toEqual([1, 2, 3]);
97
+ });
98
+ });
99
+
100
+ // ── 7b. Plugin storage tests ──────────────────────────────────────────
101
+
102
+ describe("plugin storage", () => {
103
+ let db: Database.Database;
104
+
105
+ beforeEach(() => {
106
+ db = createTestDb();
107
+ runMigrations(db, CORE_MIGRATIONS, testLogger);
108
+ });
109
+
110
+ it("get returns undefined for non-existent key", async () => {
111
+ const storage = createPluginStorage(db, "plugin-a");
112
+ const result = await storage.get("nonexistent");
113
+ expect(result).toBeUndefined();
114
+ });
115
+
116
+ it("set then get round-trips a value", async () => {
117
+ const storage = createPluginStorage(db, "plugin-a");
118
+ await storage.set("key1", { name: "hello", count: 42 });
119
+ const result = await storage.get<{ name: string; count: number }>("key1");
120
+ expect(result).toEqual({ name: "hello", count: 42 });
121
+ });
122
+
123
+ it("set overwrites an existing key", async () => {
124
+ const storage = createPluginStorage(db, "plugin-a");
125
+ await storage.set("key1", "first");
126
+ await storage.set("key1", "second");
127
+ const result = await storage.get<string>("key1");
128
+ expect(result).toBe("second");
129
+ });
130
+
131
+ it("delete removes a key; subsequent get returns undefined", async () => {
132
+ const storage = createPluginStorage(db, "plugin-a");
133
+ await storage.set("key1", "value");
134
+ await storage.delete("key1");
135
+ const result = await storage.get("key1");
136
+ expect(result).toBeUndefined();
137
+ });
138
+
139
+ it("delete on non-existent key does not throw", async () => {
140
+ const storage = createPluginStorage(db, "plugin-a");
141
+ await expect(storage.delete("nonexistent")).resolves.toBeUndefined();
142
+ });
143
+
144
+ it("list() returns all keys in the namespace", async () => {
145
+ const storage = createPluginStorage(db, "plugin-a");
146
+ await storage.set("a", 1);
147
+ await storage.set("b", 2);
148
+ await storage.set("c", 3);
149
+
150
+ const results = await storage.list<number>();
151
+ expect(results).toHaveLength(3);
152
+ expect(results.map((r) => r.key)).toEqual(["a", "b", "c"]);
153
+ expect(results.map((r) => r.value)).toEqual([1, 2, 3]);
154
+ });
155
+
156
+ it("list(prefix) returns only keys with that prefix", async () => {
157
+ const storage = createPluginStorage(db, "plugin-a");
158
+ await storage.set("a/1", "a-one");
159
+ await storage.set("a/2", "a-two");
160
+ await storage.set("b/1", "b-one");
161
+
162
+ const results = await storage.list<string>("a/");
163
+ expect(results).toHaveLength(2);
164
+ expect(results.map((r) => r.key)).toEqual(["a/1", "a/2"]);
165
+ });
166
+
167
+ it("isolation: plugin A cannot read plugin B's keys", async () => {
168
+ const storageA = createPluginStorage(db, "plugin-a");
169
+ const storageB = createPluginStorage(db, "plugin-b");
170
+
171
+ await storageA.set("secret", "only-a");
172
+ await storageB.set("secret", "only-b");
173
+
174
+ const aVal = await storageA.get<string>("secret");
175
+ const bVal = await storageB.get<string>("secret");
176
+
177
+ expect(aVal).toBe("only-a");
178
+ expect(bVal).toBe("only-b");
179
+ });
180
+
181
+ it("isolation: list for plugin A does not include plugin B keys", async () => {
182
+ const storageA = createPluginStorage(db, "plugin-a");
183
+ const storageB = createPluginStorage(db, "plugin-b");
184
+
185
+ await storageA.set("key-a", 1);
186
+ await storageB.set("key-b", 2);
187
+
188
+ const listA = await storageA.list<number>();
189
+ const listB = await storageB.list<number>();
190
+
191
+ expect(listA.map((r) => r.key)).toEqual(["key-a"]);
192
+ expect(listB.map((r) => r.key)).toEqual(["key-b"]);
193
+ });
194
+
195
+ it("list returns results sorted lexically", async () => {
196
+ const storage = createPluginStorage(db, "plugin-a");
197
+ await storage.set("z", 1);
198
+ await storage.set("a", 2);
199
+ await storage.set("m", 3);
200
+
201
+ const results = await storage.list<number>();
202
+ expect(results.map((r) => r.key)).toEqual(["a", "m", "z"]);
203
+ });
204
+ });
205
+
206
+ // ── 7c. Encryption tests ──────────────────────────────────────────────
207
+
208
+ describe("encryption", () => {
209
+ let dataDir: string;
210
+
211
+ beforeEach(() => {
212
+ dataDir = mkdtempSync(path.join(tmpdir(), "starr-encrypt-"));
213
+ resetEncryptionKey();
214
+ });
215
+
216
+ afterEach(() => {
217
+ // Clean up temp directory
218
+ try {
219
+ const keyPath = path.join(dataDir, "starr.key");
220
+ if (existsSync(keyPath)) unlinkSync(keyPath);
221
+ rmdirSync(dataDir);
222
+ } catch {
223
+ // Ignore cleanup failures
224
+ }
225
+ resetEncryptionKey();
226
+ });
227
+
228
+ it("encrypt/decrypt round-trips a plaintext string", () => {
229
+ // Load key in auto-generate mode
230
+ loadEncryptionKey(dataDir, testLogger);
231
+
232
+ const original = "hello world";
233
+ const encrypted = encrypt(original);
234
+ const decrypted = decrypt(encrypted);
235
+ expect(decrypted).toBe(original);
236
+ });
237
+
238
+ it("decrypt with tampered blob throws", () => {
239
+ loadEncryptionKey(dataDir, testLogger);
240
+ const encrypted = encrypt("test data");
241
+
242
+ // Mutate the blob
243
+ const buf = Buffer.from(encrypted, "base64");
244
+ buf[0] ^= 0xff; // flip bits in IV
245
+ const tampered = buf.toString("base64");
246
+
247
+ expect(() => decrypt(tampered)).toThrow();
248
+ });
249
+
250
+ it("encrypt produces different outputs for same plaintext (random IV)", () => {
251
+ loadEncryptionKey(dataDir, testLogger);
252
+ const a = encrypt("same");
253
+ const b = encrypt("same");
254
+ expect(a).not.toBe(b);
255
+ });
256
+
257
+ it("loadEncryptionKey loads from env var", () => {
258
+ const key = randomBytes(32).toString("base64");
259
+ process.env.STARR_ENCRYPTION_KEY = key;
260
+ try {
261
+ const loaded = loadEncryptionKey(dataDir, testLogger);
262
+ expect(loaded.toString("base64")).toBe(key);
263
+ } finally {
264
+ delete process.env.STARR_ENCRYPTION_KEY;
265
+ }
266
+ });
267
+
268
+ it("loadEncryptionKey loads from file", () => {
269
+ const key = randomBytes(32).toString("base64");
270
+ const keyPath = path.join(dataDir, "starr.key");
271
+ writeFileSync(keyPath, key + "\n", { mode: 0o600 });
272
+
273
+ const loaded = loadEncryptionKey(dataDir, testLogger);
274
+ expect(loaded.toString("base64")).toBe(key);
275
+ });
276
+
277
+ it("loadEncryptionKey auto-generates when no env var or file exists", () => {
278
+ // Ensure env var is not set
279
+ delete process.env.STARR_ENCRYPTION_KEY;
280
+
281
+ const loaded = loadEncryptionKey(dataDir, testLogger);
282
+ expect(loaded.length).toBe(32);
283
+
284
+ // Verify key file was written
285
+ const keyPath = path.join(dataDir, "starr.key");
286
+ expect(existsSync(keyPath)).toBe(true);
287
+ const fileContent = readFileSync(keyPath, "utf8").trim();
288
+ expect(fileContent).toBe(loaded.toString("base64"));
289
+ });
290
+
291
+ it("loadEncryptionKey is idempotent", () => {
292
+ delete process.env.STARR_ENCRYPTION_KEY;
293
+ const first = loadEncryptionKey(dataDir, testLogger);
294
+ const second = loadEncryptionKey(dataDir, testLogger);
295
+ expect(first).toBe(second); // Same buffer reference
296
+ resetEncryptionKey();
297
+ });
298
+ });
299
+
300
+ // ── 7d. Context integration test ──────────────────────────────────────
301
+
302
+ describe("context integration", () => {
303
+ let db: Database.Database;
304
+
305
+ beforeEach(() => {
306
+ db = createTestDb();
307
+ runMigrations(db, CORE_MIGRATIONS, testLogger);
308
+ });
309
+
310
+ function createTestConfigService(): ConfigService {
311
+ return new ConfigService(db, testLogger);
312
+ }
313
+
314
+ it("ctx.storage.set and ctx.storage.get persist data", async () => {
315
+ const logger = createLogger("core", "silent");
316
+ const starr = new Starr({ pluginsDir: "/tmp", apiVersion: "1.0.0", logger });
317
+ const ctx = createContext(starr, "/tmp/test-plugin", db, createTestConfigService());
318
+
319
+ ctx.define({ id: "test-plugin", version: "1.0.0", starrApiVersion: "^1.0.0" });
320
+ await ctx.storage.set("mykey", { foo: "bar" });
321
+ const result = await ctx.storage.get<{ foo: string }>("mykey");
322
+ expect(result).toEqual({ foo: "bar" });
323
+ });
324
+
325
+ it("ctx.storage.list works with real storage", async () => {
326
+ const logger = createLogger("core", "silent");
327
+ const starr = new Starr({ pluginsDir: "/tmp", apiVersion: "1.0.0", logger });
328
+ const ctx = createContext(starr, "/tmp/test-plugin", db, createTestConfigService());
329
+
330
+ ctx.define({ id: "test-plugin", version: "1.0.0", starrApiVersion: "^1.0.0" });
331
+ await ctx.storage.set("a", 1);
332
+ await ctx.storage.set("b", 2);
333
+ const results = await ctx.storage.list<number>();
334
+ expect(results).toHaveLength(2);
335
+ });
336
+
337
+ it("calling ctx.storage.* before ctx.define throws", () => {
338
+ const logger = createLogger("core", "silent");
339
+ const starr = new Starr({ pluginsDir: "/tmp", apiVersion: "1.0.0", logger });
340
+ const ctx = createContext(starr, "/tmp/test-plugin", db, createTestConfigService());
341
+
342
+ // Attempt to access storage before define — getter throws synchronously
343
+ expect(() => ctx.storage).toThrow("ctx.storage called before ctx.define");
344
+ });
345
+ });