@selvajs/local-provider 0.11.0

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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/auth/LocalAuthProvider.d.ts +28 -0
  4. package/dist/auth/LocalAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/LocalAuthProvider.js +142 -0
  6. package/dist/auth/LocalAuthProvider.js.map +1 -0
  7. package/dist/auth/__tests__/conformance.test.d.ts +2 -0
  8. package/dist/auth/__tests__/conformance.test.d.ts.map +1 -0
  9. package/dist/auth/__tests__/conformance.test.js +36 -0
  10. package/dist/auth/__tests__/conformance.test.js.map +1 -0
  11. package/dist/auth/hmac.d.ts +18 -0
  12. package/dist/auth/hmac.d.ts.map +1 -0
  13. package/dist/auth/hmac.js +41 -0
  14. package/dist/auth/hmac.js.map +1 -0
  15. package/dist/auth/index.d.ts +6 -0
  16. package/dist/auth/index.d.ts.map +1 -0
  17. package/dist/auth/index.js +4 -0
  18. package/dist/auth/index.js.map +1 -0
  19. package/dist/auth/users.d.ts +38 -0
  20. package/dist/auth/users.d.ts.map +1 -0
  21. package/dist/auth/users.js +100 -0
  22. package/dist/auth/users.js.map +1 -0
  23. package/dist/compute/FilesystemComputeProvider.d.ts +16 -0
  24. package/dist/compute/FilesystemComputeProvider.d.ts.map +1 -0
  25. package/dist/compute/FilesystemComputeProvider.js +51 -0
  26. package/dist/compute/FilesystemComputeProvider.js.map +1 -0
  27. package/dist/compute/SingleComputeServerProvider.d.ts +15 -0
  28. package/dist/compute/SingleComputeServerProvider.d.ts.map +1 -0
  29. package/dist/compute/SingleComputeServerProvider.js +26 -0
  30. package/dist/compute/SingleComputeServerProvider.js.map +1 -0
  31. package/dist/compute/types.d.ts +30 -0
  32. package/dist/compute/types.d.ts.map +1 -0
  33. package/dist/compute/types.js +2 -0
  34. package/dist/compute/types.js.map +1 -0
  35. package/dist/computeServer/FilesystemComputeServerStore.d.ts +14 -0
  36. package/dist/computeServer/FilesystemComputeServerStore.d.ts.map +1 -0
  37. package/dist/computeServer/FilesystemComputeServerStore.js +35 -0
  38. package/dist/computeServer/FilesystemComputeServerStore.js.map +1 -0
  39. package/dist/computeServer/LocalComputeServerProvider.d.ts +18 -0
  40. package/dist/computeServer/LocalComputeServerProvider.d.ts.map +1 -0
  41. package/dist/computeServer/LocalComputeServerProvider.js +29 -0
  42. package/dist/computeServer/LocalComputeServerProvider.js.map +1 -0
  43. package/dist/computeServer/__tests__/conformance.test.d.ts +2 -0
  44. package/dist/computeServer/__tests__/conformance.test.d.ts.map +1 -0
  45. package/dist/computeServer/__tests__/conformance.test.js +20 -0
  46. package/dist/computeServer/__tests__/conformance.test.js.map +1 -0
  47. package/dist/computeServer/index.d.ts +2 -0
  48. package/dist/computeServer/index.d.ts.map +1 -0
  49. package/dist/computeServer/index.js +2 -0
  50. package/dist/computeServer/index.js.map +1 -0
  51. package/dist/data/LocalComputeServerStore.d.ts +72 -0
  52. package/dist/data/LocalComputeServerStore.d.ts.map +1 -0
  53. package/dist/data/LocalComputeServerStore.js +207 -0
  54. package/dist/data/LocalComputeServerStore.js.map +1 -0
  55. package/dist/data/LocalDataProvider.d.ts +47 -0
  56. package/dist/data/LocalDataProvider.d.ts.map +1 -0
  57. package/dist/data/LocalDataProvider.js +118 -0
  58. package/dist/data/LocalDataProvider.js.map +1 -0
  59. package/dist/data/LocalDefinitionMetaProvider.d.ts +22 -0
  60. package/dist/data/LocalDefinitionMetaProvider.d.ts.map +1 -0
  61. package/dist/data/LocalDefinitionMetaProvider.js +131 -0
  62. package/dist/data/LocalDefinitionMetaProvider.js.map +1 -0
  63. package/dist/data/LocalDefinitionStore.d.ts +33 -0
  64. package/dist/data/LocalDefinitionStore.d.ts.map +1 -0
  65. package/dist/data/LocalDefinitionStore.js +274 -0
  66. package/dist/data/LocalDefinitionStore.js.map +1 -0
  67. package/dist/data/LocalInviteStore.d.ts +23 -0
  68. package/dist/data/LocalInviteStore.d.ts.map +1 -0
  69. package/dist/data/LocalInviteStore.js +98 -0
  70. package/dist/data/LocalInviteStore.js.map +1 -0
  71. package/dist/data/LocalOrgStore.d.ts +67 -0
  72. package/dist/data/LocalOrgStore.d.ts.map +1 -0
  73. package/dist/data/LocalOrgStore.js +255 -0
  74. package/dist/data/LocalOrgStore.js.map +1 -0
  75. package/dist/data/LocalPlatformProjectGrantStore.d.ts +14 -0
  76. package/dist/data/LocalPlatformProjectGrantStore.d.ts.map +1 -0
  77. package/dist/data/LocalPlatformProjectGrantStore.js +62 -0
  78. package/dist/data/LocalPlatformProjectGrantStore.js.map +1 -0
  79. package/dist/data/LocalProjectStore.d.ts +30 -0
  80. package/dist/data/LocalProjectStore.d.ts.map +1 -0
  81. package/dist/data/LocalProjectStore.js +171 -0
  82. package/dist/data/LocalProjectStore.js.map +1 -0
  83. package/dist/data/LocalShareLinkStore.d.ts +39 -0
  84. package/dist/data/LocalShareLinkStore.d.ts.map +1 -0
  85. package/dist/data/LocalShareLinkStore.js +108 -0
  86. package/dist/data/LocalShareLinkStore.js.map +1 -0
  87. package/dist/data/__tests__/LocalDefinitionMetaProvider.test.d.ts +2 -0
  88. package/dist/data/__tests__/LocalDefinitionMetaProvider.test.d.ts.map +1 -0
  89. package/dist/data/__tests__/LocalDefinitionMetaProvider.test.js +21 -0
  90. package/dist/data/__tests__/LocalDefinitionMetaProvider.test.js.map +1 -0
  91. package/dist/data/__tests__/cascade.test.d.ts +2 -0
  92. package/dist/data/__tests__/cascade.test.d.ts.map +1 -0
  93. package/dist/data/__tests__/cascade.test.js +265 -0
  94. package/dist/data/__tests__/cascade.test.js.map +1 -0
  95. package/dist/data/__tests__/compute-server-conformance.test.d.ts +2 -0
  96. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +1 -0
  97. package/dist/data/__tests__/compute-server-conformance.test.js +21 -0
  98. package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
  99. package/dist/data/__tests__/compute-server-encryption.test.d.ts +2 -0
  100. package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +1 -0
  101. package/dist/data/__tests__/compute-server-encryption.test.js +131 -0
  102. package/dist/data/__tests__/compute-server-encryption.test.js.map +1 -0
  103. package/dist/data/__tests__/definition-conformance.test.d.ts +2 -0
  104. package/dist/data/__tests__/definition-conformance.test.d.ts.map +1 -0
  105. package/dist/data/__tests__/definition-conformance.test.js +20 -0
  106. package/dist/data/__tests__/definition-conformance.test.js.map +1 -0
  107. package/dist/data/__tests__/event-sink-conformance.test.d.ts +2 -0
  108. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +1 -0
  109. package/dist/data/__tests__/event-sink-conformance.test.js +24 -0
  110. package/dist/data/__tests__/event-sink-conformance.test.js.map +1 -0
  111. package/dist/data/__tests__/invite-conformance.test.d.ts +2 -0
  112. package/dist/data/__tests__/invite-conformance.test.d.ts.map +1 -0
  113. package/dist/data/__tests__/invite-conformance.test.js +21 -0
  114. package/dist/data/__tests__/invite-conformance.test.js.map +1 -0
  115. package/dist/data/__tests__/org-conformance.test.d.ts +2 -0
  116. package/dist/data/__tests__/org-conformance.test.d.ts.map +1 -0
  117. package/dist/data/__tests__/org-conformance.test.js +36 -0
  118. package/dist/data/__tests__/org-conformance.test.js.map +1 -0
  119. package/dist/data/__tests__/platform-project-grant-conformance.test.d.ts +2 -0
  120. package/dist/data/__tests__/platform-project-grant-conformance.test.d.ts.map +1 -0
  121. package/dist/data/__tests__/platform-project-grant-conformance.test.js +20 -0
  122. package/dist/data/__tests__/platform-project-grant-conformance.test.js.map +1 -0
  123. package/dist/data/__tests__/project-conformance.test.d.ts +2 -0
  124. package/dist/data/__tests__/project-conformance.test.d.ts.map +1 -0
  125. package/dist/data/__tests__/project-conformance.test.js +53 -0
  126. package/dist/data/__tests__/project-conformance.test.js.map +1 -0
  127. package/dist/data/__tests__/rules.test.d.ts +2 -0
  128. package/dist/data/__tests__/rules.test.d.ts.map +1 -0
  129. package/dist/data/__tests__/rules.test.js +484 -0
  130. package/dist/data/__tests__/rules.test.js.map +1 -0
  131. package/dist/data/__tests__/share-link-conformance.test.d.ts +2 -0
  132. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +1 -0
  133. package/dist/data/__tests__/share-link-conformance.test.js +20 -0
  134. package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
  135. package/dist/data/fsJson.d.ts +12 -0
  136. package/dist/data/fsJson.d.ts.map +1 -0
  137. package/dist/data/fsJson.js +29 -0
  138. package/dist/data/fsJson.js.map +1 -0
  139. package/dist/data/index.d.ts +13 -0
  140. package/dist/data/index.d.ts.map +1 -0
  141. package/dist/data/index.js +9 -0
  142. package/dist/data/index.js.map +1 -0
  143. package/dist/data/pagination.d.ts +15 -0
  144. package/dist/data/pagination.d.ts.map +1 -0
  145. package/dist/data/pagination.js +36 -0
  146. package/dist/data/pagination.js.map +1 -0
  147. package/dist/data/secretCrypto.d.ts +23 -0
  148. package/dist/data/secretCrypto.d.ts.map +1 -0
  149. package/dist/data/secretCrypto.js +64 -0
  150. package/dist/data/secretCrypto.js.map +1 -0
  151. package/dist/data/userData.d.ts +40 -0
  152. package/dist/data/userData.d.ts.map +1 -0
  153. package/dist/data/userData.js +84 -0
  154. package/dist/data/userData.js.map +1 -0
  155. package/dist/definitions/LocalDefinitionMetaProvider.d.ts +27 -0
  156. package/dist/definitions/LocalDefinitionMetaProvider.d.ts.map +1 -0
  157. package/dist/definitions/LocalDefinitionMetaProvider.js +188 -0
  158. package/dist/definitions/LocalDefinitionMetaProvider.js.map +1 -0
  159. package/dist/definitions/__tests__/conformance.test.d.ts +2 -0
  160. package/dist/definitions/__tests__/conformance.test.d.ts.map +1 -0
  161. package/dist/definitions/__tests__/conformance.test.js +20 -0
  162. package/dist/definitions/__tests__/conformance.test.js.map +1 -0
  163. package/dist/definitions/index.d.ts +2 -0
  164. package/dist/definitions/index.d.ts.map +1 -0
  165. package/dist/definitions/index.js +2 -0
  166. package/dist/definitions/index.js.map +1 -0
  167. package/dist/definitions/providers/filesystem-files.d.ts +24 -0
  168. package/dist/definitions/providers/filesystem-files.d.ts.map +1 -0
  169. package/dist/definitions/providers/filesystem-files.js +170 -0
  170. package/dist/definitions/providers/filesystem-files.js.map +1 -0
  171. package/dist/definitions/providers/filesystem-meta.d.ts +17 -0
  172. package/dist/definitions/providers/filesystem-meta.d.ts.map +1 -0
  173. package/dist/definitions/providers/filesystem-meta.js +216 -0
  174. package/dist/definitions/providers/filesystem-meta.js.map +1 -0
  175. package/dist/fsJson.d.ts +12 -0
  176. package/dist/fsJson.d.ts.map +1 -0
  177. package/dist/fsJson.js +29 -0
  178. package/dist/fsJson.js.map +1 -0
  179. package/dist/index.d.ts +13 -0
  180. package/dist/index.d.ts.map +1 -0
  181. package/dist/index.js +16 -0
  182. package/dist/index.js.map +1 -0
  183. package/dist/invites/LocalInviteProvider.d.ts +24 -0
  184. package/dist/invites/LocalInviteProvider.d.ts.map +1 -0
  185. package/dist/invites/LocalInviteProvider.js +89 -0
  186. package/dist/invites/LocalInviteProvider.js.map +1 -0
  187. package/dist/invites/__tests__/conformance.test.d.ts +2 -0
  188. package/dist/invites/__tests__/conformance.test.d.ts.map +1 -0
  189. package/dist/invites/__tests__/conformance.test.js +21 -0
  190. package/dist/invites/__tests__/conformance.test.js.map +1 -0
  191. package/dist/organizations/LocalOrganizationProvider.d.ts +41 -0
  192. package/dist/organizations/LocalOrganizationProvider.d.ts.map +1 -0
  193. package/dist/organizations/LocalOrganizationProvider.js +198 -0
  194. package/dist/organizations/LocalOrganizationProvider.js.map +1 -0
  195. package/dist/organizations/__tests__/conformance.test.d.ts +2 -0
  196. package/dist/organizations/__tests__/conformance.test.d.ts.map +1 -0
  197. package/dist/organizations/__tests__/conformance.test.js +20 -0
  198. package/dist/organizations/__tests__/conformance.test.js.map +1 -0
  199. package/dist/organizations/index.d.ts +2 -0
  200. package/dist/organizations/index.d.ts.map +1 -0
  201. package/dist/organizations/index.js +2 -0
  202. package/dist/organizations/index.js.map +1 -0
  203. package/dist/pagination.d.ts +15 -0
  204. package/dist/pagination.d.ts.map +1 -0
  205. package/dist/pagination.js +36 -0
  206. package/dist/pagination.js.map +1 -0
  207. package/dist/permissions/LocalPlatformPermissionStore.d.ts +39 -0
  208. package/dist/permissions/LocalPlatformPermissionStore.d.ts.map +1 -0
  209. package/dist/permissions/LocalPlatformPermissionStore.js +117 -0
  210. package/dist/permissions/LocalPlatformPermissionStore.js.map +1 -0
  211. package/dist/permissions/__tests__/conformance.test.d.ts +2 -0
  212. package/dist/permissions/__tests__/conformance.test.d.ts.map +1 -0
  213. package/dist/permissions/__tests__/conformance.test.js +37 -0
  214. package/dist/permissions/__tests__/conformance.test.js.map +1 -0
  215. package/dist/permissions/index.d.ts +2 -0
  216. package/dist/permissions/index.d.ts.map +1 -0
  217. package/dist/permissions/index.js +2 -0
  218. package/dist/permissions/index.js.map +1 -0
  219. package/dist/projects/LocalProjectProvider.d.ts +21 -0
  220. package/dist/projects/LocalProjectProvider.d.ts.map +1 -0
  221. package/dist/projects/LocalProjectProvider.js +125 -0
  222. package/dist/projects/LocalProjectProvider.js.map +1 -0
  223. package/dist/projects/__tests__/conformance.test.d.ts +2 -0
  224. package/dist/projects/__tests__/conformance.test.d.ts.map +1 -0
  225. package/dist/projects/__tests__/conformance.test.js +44 -0
  226. package/dist/projects/__tests__/conformance.test.js.map +1 -0
  227. package/dist/projects/index.d.ts +2 -0
  228. package/dist/projects/index.d.ts.map +1 -0
  229. package/dist/projects/index.js +2 -0
  230. package/dist/projects/index.js.map +1 -0
  231. package/dist/storage/LocalStorageProvider.d.ts +27 -0
  232. package/dist/storage/LocalStorageProvider.d.ts.map +1 -0
  233. package/dist/storage/LocalStorageProvider.js +74 -0
  234. package/dist/storage/LocalStorageProvider.js.map +1 -0
  235. package/dist/storage/__tests__/conformance.test.d.ts +2 -0
  236. package/dist/storage/__tests__/conformance.test.d.ts.map +1 -0
  237. package/dist/storage/__tests__/conformance.test.js +20 -0
  238. package/dist/storage/__tests__/conformance.test.js.map +1 -0
  239. package/dist/storage/index.d.ts +2 -0
  240. package/dist/storage/index.d.ts.map +1 -0
  241. package/dist/storage/index.js +2 -0
  242. package/dist/storage/index.js.map +1 -0
  243. package/dist/userProfile/LocalUserProfileProvider.d.ts +25 -0
  244. package/dist/userProfile/LocalUserProfileProvider.d.ts.map +1 -0
  245. package/dist/userProfile/LocalUserProfileProvider.js +110 -0
  246. package/dist/userProfile/LocalUserProfileProvider.js.map +1 -0
  247. package/dist/userProfile/__tests__/conformance.test.d.ts +2 -0
  248. package/dist/userProfile/__tests__/conformance.test.d.ts.map +1 -0
  249. package/dist/userProfile/__tests__/conformance.test.js +40 -0
  250. package/dist/userProfile/__tests__/conformance.test.js.map +1 -0
  251. package/dist/userProfile/index.d.ts +2 -0
  252. package/dist/userProfile/index.d.ts.map +1 -0
  253. package/dist/userProfile/index.js +2 -0
  254. package/dist/userProfile/index.js.map +1 -0
  255. package/package.json +70 -0
  256. package/src/README.md +37 -0
  257. package/src/auth/LocalAuthProvider.ts +165 -0
  258. package/src/auth/__tests__/conformance.test.ts +40 -0
  259. package/src/auth/hmac.ts +53 -0
  260. package/src/auth/index.ts +5 -0
  261. package/src/auth/users.ts +151 -0
  262. package/src/data/LocalComputeServerStore.ts +290 -0
  263. package/src/data/LocalDataProvider.ts +148 -0
  264. package/src/data/LocalDefinitionStore.ts +369 -0
  265. package/src/data/LocalInviteStore.ts +117 -0
  266. package/src/data/LocalOrgStore.ts +356 -0
  267. package/src/data/LocalPlatformProjectGrantStore.ts +85 -0
  268. package/src/data/LocalProjectStore.ts +274 -0
  269. package/src/data/LocalShareLinkStore.ts +138 -0
  270. package/src/data/__tests__/cascade.test.ts +300 -0
  271. package/src/data/__tests__/compute-server-conformance.test.ts +26 -0
  272. package/src/data/__tests__/compute-server-encryption.test.ts +185 -0
  273. package/src/data/__tests__/definition-conformance.test.ts +23 -0
  274. package/src/data/__tests__/event-sink-conformance.test.ts +28 -0
  275. package/src/data/__tests__/invite-conformance.test.ts +24 -0
  276. package/src/data/__tests__/org-conformance.test.ts +43 -0
  277. package/src/data/__tests__/platform-project-grant-conformance.test.ts +24 -0
  278. package/src/data/__tests__/project-conformance.test.ts +64 -0
  279. package/src/data/__tests__/rules.test.ts +682 -0
  280. package/src/data/__tests__/share-link-conformance.test.ts +23 -0
  281. package/src/data/fsJson.ts +28 -0
  282. package/src/data/index.ts +16 -0
  283. package/src/data/pagination.ts +48 -0
  284. package/src/data/secretCrypto.ts +69 -0
  285. package/src/data/userData.ts +134 -0
  286. package/src/index.ts +42 -0
  287. package/src/permissions/LocalPlatformPermissionStore.ts +129 -0
  288. package/src/permissions/__tests__/conformance.test.ts +40 -0
  289. package/src/permissions/index.ts +1 -0
  290. package/src/storage/LocalStorageProvider.ts +78 -0
  291. package/src/storage/__tests__/conformance.test.ts +23 -0
  292. package/src/storage/index.ts +1 -0
  293. package/src/userProfile/LocalUserProfileProvider.ts +135 -0
  294. package/src/userProfile/__tests__/conformance.test.ts +43 -0
  295. package/src/userProfile/index.ts +1 -0
@@ -0,0 +1,138 @@
1
+ import * as path from 'node:path';
2
+ import type {
3
+ IShareLinkStore,
4
+ IDefinitionStore,
5
+ IEventSink,
6
+ ShareLink,
7
+ RequestContext,
8
+ ListOptions,
9
+ Page
10
+ } from '@selvajs/platform';
11
+ import { ProviderError, SYSTEM_CONTEXT, actorFrom, NoopEventSink } from '@selvajs/platform';
12
+ import { paginate } from './pagination.js';
13
+ import { readJsonFile, writeJsonFile } from './fsJson.js';
14
+
15
+ interface OnDiskShape {
16
+ links: Record<string, ShareLink>;
17
+ }
18
+
19
+ const empty = (): OnDiskShape => ({ links: {} });
20
+
21
+ export interface LocalShareLinkStoreOptions {
22
+ /** Absolute path to the JSON file backing this store. */
23
+ filePath: string;
24
+ events?: IEventSink;
25
+ }
26
+
27
+ /**
28
+ * Spec §7 — share-link store backed by share-links.json.
29
+ *
30
+ * `tryIncrementSolveCount` is the load-bearing race-sensitive method; in
31
+ * the local provider we read-modify-write under a single fs round-trip,
32
+ * which is acceptable at single-node scale. Postgres adapters use a true
33
+ * atomic UPDATE.
34
+ */
35
+ export class LocalShareLinkStore implements IShareLinkStore {
36
+ private definitionProvider?: IDefinitionStore;
37
+ private readonly events: IEventSink;
38
+ private readonly configFilePath: string;
39
+
40
+ static fromEnv(env: Record<string, string | undefined>): LocalShareLinkStore {
41
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
42
+ return new LocalShareLinkStore({
43
+ filePath: path.join(env.DATA_PATH, 'share-links.json')
44
+ });
45
+ }
46
+
47
+ constructor(opts: LocalShareLinkStoreOptions) {
48
+ this.configFilePath = opts.filePath;
49
+ this.events = opts.events ?? new NoopEventSink();
50
+ }
51
+
52
+ /**
53
+ * Wire the definition store so token resolution can check the parent
54
+ * definition's `deletedAt` (Permissions.md §7 cascade contract). Mirrors
55
+ * Supabase, which performs the equivalent JOIN. Optional: when unset, the
56
+ * store falls back to the local-only revoke check; the route layer in
57
+ * the selva app does the parent lookup as a safety net either way.
58
+ */
59
+ setDefinitionProvider(definitions: IDefinitionStore): void {
60
+ this.definitionProvider = definitions;
61
+ }
62
+
63
+ private async readAll(): Promise<OnDiskShape> {
64
+ return readJsonFile<OnDiskShape>(this.configFilePath, empty());
65
+ }
66
+
67
+ private async writeAll(data: OnDiskShape): Promise<void> {
68
+ await writeJsonFile(this.configFilePath, data);
69
+ }
70
+
71
+ private isLive(l: ShareLink | undefined | null): l is ShareLink {
72
+ return Boolean(l && l.revokedAt == null);
73
+ }
74
+
75
+ async create(ctx: RequestContext, link: ShareLink): Promise<void> {
76
+ const all = await this.readAll();
77
+ if (all.links[link.id]) {
78
+ throw new ProviderError(`Share link '${link.id}' already exists`, 409);
79
+ }
80
+ all.links[link.id] = { ...link, revokedAt: null };
81
+ await this.writeAll(all);
82
+ await this.events.emit({
83
+ type: 'share_link.minted',
84
+ linkId: link.id,
85
+ definitionId: link.definitionId,
86
+ actorId: actorFrom(ctx)
87
+ });
88
+ }
89
+
90
+ async listByDefinition(
91
+ _ctx: RequestContext,
92
+ definitionId: string,
93
+ opts?: ListOptions
94
+ ): Promise<Page<ShareLink>> {
95
+ const all = await this.readAll();
96
+ const rows = Object.values(all.links)
97
+ .filter((l) => l.definitionId === definitionId && this.isLive(l))
98
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
99
+ return paginate(rows, opts);
100
+ }
101
+
102
+ async getById(_ctx: RequestContext, id: string): Promise<ShareLink | null> {
103
+ const all = await this.readAll();
104
+ return all.links[id] ?? null;
105
+ }
106
+
107
+ async getByTokenHash(_ctx: RequestContext, tokenHash: string): Promise<ShareLink | null> {
108
+ const all = await this.readAll();
109
+ const found = Object.values(all.links).find((l) => l.tokenHash === tokenHash);
110
+ if (!this.isLive(found)) return null;
111
+ // §7: token resolution MUST NOT see links whose parent definition is
112
+ // soft-deleted. Supabase enforces this via JOIN; here we look up.
113
+ if (this.definitionProvider) {
114
+ const parent = await this.definitionProvider.get(SYSTEM_CONTEXT, found.definitionId);
115
+ if (!parent) return null;
116
+ }
117
+ return found;
118
+ }
119
+
120
+ async revoke(ctx: RequestContext, id: string): Promise<void> {
121
+ const all = await this.readAll();
122
+ const l = all.links[id];
123
+ if (!l || !this.isLive(l)) return; // idempotent
124
+ l.revokedAt = new Date().toISOString();
125
+ await this.writeAll(all);
126
+ await this.events.emit({ type: 'share_link.revoked', linkId: id, actorId: actorFrom(ctx) });
127
+ }
128
+
129
+ async tryIncrementSolveCount(_ctx: RequestContext, id: string): Promise<number | null> {
130
+ const all = await this.readAll();
131
+ const l = all.links[id];
132
+ if (!l || !this.isLive(l)) return null;
133
+ if (l.maxSolves != null && l.solveCount >= l.maxSolves) return null;
134
+ l.solveCount += 1;
135
+ await this.writeAll(all);
136
+ return l.solveCount;
137
+ }
138
+ }
@@ -0,0 +1,300 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { randomUUID } from 'node:crypto';
6
+ import {
7
+ ALL_ORG_PERMISSIONS,
8
+ ALL_PLATFORM_PERMISSIONS,
9
+ SYSTEM_CONTEXT,
10
+ type RequestContext,
11
+ type DefinitionRecord,
12
+ type ShareLink
13
+ } from '@selvajs/platform';
14
+ import { LocalOrgStore, LocalOrgStoreLoader } from '../LocalOrgStore.js';
15
+ import { LocalProjectStore } from '../LocalProjectStore.js';
16
+ import { LocalDefinitionStore } from '../LocalDefinitionStore.js';
17
+ import { LocalShareLinkStore } from '../LocalShareLinkStore.js';
18
+ import { LocalInviteStore } from '../LocalInviteStore.js';
19
+ import { LocalComputeServerStore } from '../LocalComputeServerStore.js';
20
+ import { LocalPlatformProjectGrantStore } from '../LocalPlatformProjectGrantStore.js';
21
+
22
+ /**
23
+ * Cross-store cascade behavior. Lives outside the per-store conformance suites
24
+ * because the invariants involve both `IOrgStore` and `IProjectStore`.
25
+ */
26
+ describe('Cross-store cascade', () => {
27
+ let tempDir: string;
28
+ let orgs: LocalOrgStore;
29
+ let projects: LocalProjectStore;
30
+
31
+ beforeEach(async () => {
32
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-test-'));
33
+ const loader = new LocalOrgStoreLoader(tempDir);
34
+ const invites = new LocalInviteStore(tempDir);
35
+ const computeServer = new LocalComputeServerStore(
36
+ path.join(tempDir, 'compute.config.json'),
37
+ Buffer.alloc(32, 0x42)
38
+ );
39
+ const grants = new LocalPlatformProjectGrantStore(
40
+ path.join(tempDir, 'platform-project-grants.json')
41
+ );
42
+ orgs = new LocalOrgStore({ loader, invites, computeServer, grants });
43
+ projects = new LocalProjectStore({ loader, grants });
44
+ });
45
+
46
+ afterEach(async () => {
47
+ await fs.rm(tempDir, { recursive: true, force: true });
48
+ });
49
+
50
+ function ctxFor(userId: string): RequestContext {
51
+ return {
52
+ userId,
53
+ platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
54
+ orgPermissions: [...ALL_ORG_PERMISSIONS]
55
+ };
56
+ }
57
+
58
+ it('removeOrgMember soft-deletes the user’s project memberships in that org (§9)', async () => {
59
+ const ownerId = randomUUID();
60
+ const memberId = randomUUID();
61
+ const orgId = randomUUID();
62
+ const projectAId = randomUUID();
63
+ const projectBId = randomUUID();
64
+ const now = new Date().toISOString();
65
+ const ownerCtx = ctxFor(ownerId);
66
+
67
+ await orgs.createOrg(ownerCtx, {
68
+ id: orgId,
69
+ name: 'Acme',
70
+ slug: 'acme',
71
+ ownerId,
72
+ createdBy: ownerId,
73
+ updatedBy: ownerId,
74
+ createdAt: now,
75
+ updatedAt: now,
76
+ deletedAt: null
77
+ });
78
+ await orgs.addOrgMember(ownerCtx, {
79
+ orgId,
80
+ userId: memberId,
81
+ role: 'member',
82
+ permissions: [],
83
+ joinedAt: now,
84
+ updatedAt: now,
85
+ updatedBy: ownerId,
86
+ deletedAt: null
87
+ });
88
+
89
+ for (const id of [projectAId, projectBId]) {
90
+ await projects.createProject(ownerCtx, {
91
+ id,
92
+ orgId,
93
+ ownerId,
94
+ name: `Project ${id.slice(0, 4)}`,
95
+ slug: `p-${id.slice(0, 4)}`,
96
+ visibility: 'private',
97
+ autoJoinOnUpload: false,
98
+ createdBy: ownerId,
99
+ updatedBy: ownerId,
100
+ createdAt: now,
101
+ updatedAt: now,
102
+ deletedAt: null
103
+ });
104
+ await projects.addProjectMember(ownerCtx, {
105
+ projectId: id,
106
+ userId: memberId,
107
+ role: 'editor',
108
+ joinedAt: now,
109
+ updatedAt: now,
110
+ updatedBy: ownerId,
111
+ deletedAt: null
112
+ });
113
+ }
114
+
115
+ // Sanity: memberships are live before the cascade fires.
116
+ expect(await projects.getProjectMember(ownerCtx, projectAId, memberId)).not.toBeNull();
117
+ expect(await projects.getProjectMember(ownerCtx, projectBId, memberId)).not.toBeNull();
118
+
119
+ await orgs.removeOrgMember(ownerCtx, orgId, memberId);
120
+
121
+ expect(await orgs.getOrgMember(ownerCtx, orgId, memberId)).toBeNull();
122
+ expect(await projects.getProjectMember(ownerCtx, projectAId, memberId)).toBeNull();
123
+ expect(await projects.getProjectMember(ownerCtx, projectBId, memberId)).toBeNull();
124
+ });
125
+
126
+ it('removeOrgMember does not touch project memberships in other orgs', async () => {
127
+ const ownerAId = randomUUID();
128
+ const ownerBId = randomUUID();
129
+ const memberId = randomUUID();
130
+ const orgAId = randomUUID();
131
+ const orgBId = randomUUID();
132
+ const projectAId = randomUUID();
133
+ const projectBId = randomUUID();
134
+ const now = new Date().toISOString();
135
+
136
+ await orgs.createOrg(ctxFor(ownerAId), {
137
+ id: orgAId,
138
+ name: 'Acme',
139
+ slug: 'acme',
140
+ ownerId: ownerAId,
141
+ createdBy: ownerAId,
142
+ updatedBy: ownerAId,
143
+ createdAt: now,
144
+ updatedAt: now,
145
+ deletedAt: null
146
+ });
147
+ await orgs.createOrg(ctxFor(ownerBId), {
148
+ id: orgBId,
149
+ name: 'BigClient',
150
+ slug: 'bigclient',
151
+ ownerId: ownerBId,
152
+ createdBy: ownerBId,
153
+ updatedBy: ownerBId,
154
+ createdAt: now,
155
+ updatedAt: now,
156
+ deletedAt: null
157
+ });
158
+
159
+ for (const [orgOwner, orgId] of [
160
+ [ownerAId, orgAId],
161
+ [ownerBId, orgBId]
162
+ ]) {
163
+ await orgs.addOrgMember(ctxFor(orgOwner), {
164
+ orgId,
165
+ userId: memberId,
166
+ role: 'member',
167
+ permissions: [],
168
+ joinedAt: now,
169
+ updatedAt: now,
170
+ updatedBy: orgOwner,
171
+ deletedAt: null
172
+ });
173
+ }
174
+
175
+ for (const [orgOwner, orgId, projectId] of [
176
+ [ownerAId, orgAId, projectAId],
177
+ [ownerBId, orgBId, projectBId]
178
+ ]) {
179
+ await projects.createProject(ctxFor(orgOwner), {
180
+ id: projectId,
181
+ orgId,
182
+ ownerId: orgOwner,
183
+ name: `Project ${projectId.slice(0, 4)}`,
184
+ slug: `p-${projectId.slice(0, 4)}`,
185
+ visibility: 'private',
186
+ autoJoinOnUpload: false,
187
+ createdBy: orgOwner,
188
+ updatedBy: orgOwner,
189
+ createdAt: now,
190
+ updatedAt: now,
191
+ deletedAt: null
192
+ });
193
+ await projects.addProjectMember(ctxFor(orgOwner), {
194
+ projectId,
195
+ userId: memberId,
196
+ role: 'editor',
197
+ joinedAt: now,
198
+ updatedAt: now,
199
+ updatedBy: orgOwner,
200
+ deletedAt: null
201
+ });
202
+ }
203
+
204
+ // Remove from org A only.
205
+ await orgs.removeOrgMember(ctxFor(ownerAId), orgAId, memberId);
206
+
207
+ expect(await projects.getProjectMember(ctxFor(ownerAId), projectAId, memberId)).toBeNull();
208
+ // Org B membership and its project membership are untouched.
209
+ expect(await orgs.getOrgMember(ctxFor(ownerBId), orgBId, memberId)).not.toBeNull();
210
+ expect(await projects.getProjectMember(ctxFor(ownerBId), projectBId, memberId)).not.toBeNull();
211
+ });
212
+
213
+ it('share-link getByTokenHash returns null when parent definition is soft-deleted (§7)', async () => {
214
+ // §7: token resolution MUST fail closed when its parent definition is
215
+ // soft-deleted. Supabase enforces this via JOIN; the local store gets
216
+ // the same behavior by injecting a definition provider through the
217
+ // LocalDataProvider wiring (see LocalShareLinkStore.setDefinitionProvider).
218
+ const ownerId = randomUUID();
219
+ const orgId = randomUUID();
220
+ const projectId = randomUUID();
221
+ const definitionId = randomUUID();
222
+ const linkId = randomUUID();
223
+ const tokenHash = `hash-${randomUUID()}`;
224
+ const now = new Date().toISOString();
225
+ const ownerCtx = ctxFor(ownerId);
226
+
227
+ await orgs.createOrg(ownerCtx, {
228
+ id: orgId,
229
+ name: 'Acme',
230
+ slug: 'acme',
231
+ ownerId,
232
+ createdBy: ownerId,
233
+ updatedBy: ownerId,
234
+ createdAt: now,
235
+ updatedAt: now,
236
+ deletedAt: null
237
+ });
238
+ await projects.createProject(ownerCtx, {
239
+ id: projectId,
240
+ orgId,
241
+ ownerId,
242
+ name: 'P',
243
+ slug: 'p',
244
+ visibility: 'private',
245
+ autoJoinOnUpload: false,
246
+ createdBy: ownerId,
247
+ updatedBy: ownerId,
248
+ createdAt: now,
249
+ updatedAt: now,
250
+ deletedAt: null
251
+ });
252
+
253
+ const definitions = new LocalDefinitionStore(tempDir);
254
+ const shareLinks = new LocalShareLinkStore({
255
+ filePath: path.join(tempDir, 'share-links.json')
256
+ });
257
+ shareLinks.setDefinitionProvider(definitions);
258
+
259
+ const definition: DefinitionRecord = {
260
+ guid: definitionId,
261
+ projectId,
262
+ ownerId,
263
+ createdBy: ownerId,
264
+ updatedBy: ownerId,
265
+ displayName: 'Def',
266
+ status: 'published',
267
+ solveCount: 0,
268
+ liveVersionId: null,
269
+ draftVersionId: null,
270
+ createdAt: now,
271
+ updatedAt: now,
272
+ deletedAt: null
273
+ };
274
+ await definitions.create(ownerCtx, definition);
275
+
276
+ const link: ShareLink = {
277
+ id: linkId,
278
+ definitionId,
279
+ channel: 'live',
280
+ tokenHash,
281
+ createdBy: ownerId,
282
+ createdAt: now,
283
+ expiresAt: null,
284
+ revokedAt: null,
285
+ allowSolve: true,
286
+ maxSolves: null,
287
+ solveCount: 0
288
+ };
289
+ await shareLinks.create(SYSTEM_CONTEXT, link);
290
+
291
+ // Sanity: token resolves while parent is live.
292
+ expect(await shareLinks.getByTokenHash(SYSTEM_CONTEXT, tokenHash)).not.toBeNull();
293
+
294
+ // Soft-delete the parent definition.
295
+ await definitions.delete(ownerCtx, definitionId);
296
+
297
+ // Cascade contract: token resolution must now fail closed.
298
+ expect(await shareLinks.getByTokenHash(SYSTEM_CONTEXT, tokenHash)).toBeNull();
299
+ });
300
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { runComputeServerStoreConformance } from '@selvajs/platform/testing';
6
+ import { LocalComputeServerStore } from '../LocalComputeServerStore.js';
7
+
8
+ const TEST_SECRET_KEY = Buffer.alloc(32, 0x42);
9
+
10
+ describe('LocalComputeServerStore', () => {
11
+ let tempDir: string;
12
+
13
+ beforeEach(async () => {
14
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-compute-'));
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await fs.rm(tempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ runComputeServerStoreConformance({
22
+ name: 'LocalComputeServerStore',
23
+ createStore: () =>
24
+ new LocalComputeServerStore(path.join(tempDir, 'compute.config.json'), TEST_SECRET_KEY)
25
+ });
26
+ });
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { SYSTEM_CONTEXT, type PlatformComputeServer } from '@selvajs/platform';
6
+ import { LocalComputeServerStore } from '../LocalComputeServerStore.js';
7
+
8
+ const TEST_SECRET_KEY = Buffer.alloc(32, 0x42);
9
+ const OTHER_KEY = Buffer.alloc(32, 0x99);
10
+
11
+ function makePlatformServer(
12
+ overrides: Partial<Omit<PlatformComputeServer, 'scope'>> = {}
13
+ ): PlatformComputeServer {
14
+ return {
15
+ id: overrides.id ?? 'a1',
16
+ scope: 'platform',
17
+ sharedWith: overrides.sharedWith ?? 'all',
18
+ label: overrides.label ?? 'Test',
19
+ serverUrl: overrides.serverUrl ?? 'http://localhost:5000',
20
+ apiKey: overrides.apiKey,
21
+ timeoutMs: overrides.timeoutMs,
22
+ retryCount: overrides.retryCount
23
+ };
24
+ }
25
+
26
+ describe('LocalComputeServerStore — apiKey encryption at rest', () => {
27
+ let tempDir: string;
28
+ let configPath: string;
29
+
30
+ beforeEach(async () => {
31
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-compute-enc-'));
32
+ configPath = path.join(tempDir, 'compute.config.json');
33
+ });
34
+
35
+ afterEach(async () => {
36
+ await fs.rm(tempDir, { recursive: true, force: true });
37
+ });
38
+
39
+ it('round-trips apiKey plaintext through the store', async () => {
40
+ const store = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
41
+ await store.savePlatformServers(
42
+ SYSTEM_CONTEXT,
43
+ [makePlatformServer({ apiKey: 'super-secret-value' })],
44
+ 'a1'
45
+ );
46
+
47
+ const got = await store.getConfig(SYSTEM_CONTEXT);
48
+ expect(got.servers[0].apiKey).toBe('super-secret-value');
49
+ });
50
+
51
+ it('does not write the plaintext apiKey to disk', async () => {
52
+ const store = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
53
+ const plaintext = 'plaintext-must-not-appear-on-disk';
54
+ await store.savePlatformServers(
55
+ SYSTEM_CONTEXT,
56
+ [makePlatformServer({ apiKey: plaintext })],
57
+ 'a1'
58
+ );
59
+
60
+ const onDisk = await fs.readFile(configPath, 'utf-8');
61
+ expect(onDisk).not.toContain(plaintext);
62
+ expect(onDisk).toContain('enc:v1:');
63
+ });
64
+
65
+ it('getConfig with the wrong key returns the server with apiKey omitted (does not throw)', async () => {
66
+ // Per-row tolerance: a key mismatch on `getConfig` must not blank out
67
+ // the entire compute-config response, because every page that reads
68
+ // it (e.g. /projects) would otherwise 500. Strict detection is the
69
+ // job of `verifySecrets` at boot.
70
+ const writer = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
71
+ await writer.savePlatformServers(
72
+ SYSTEM_CONTEXT,
73
+ [makePlatformServer({ apiKey: 'value' })],
74
+ 'a1'
75
+ );
76
+
77
+ const reader = new LocalComputeServerStore(configPath, OTHER_KEY);
78
+ const got = await reader.getConfig(SYSTEM_CONTEXT);
79
+ expect(got.servers).toHaveLength(1);
80
+ expect(got.servers[0].apiKey).toBeUndefined();
81
+ });
82
+
83
+ it('verifySecrets reports key_mismatch when the at-rest key has rotated', async () => {
84
+ const writer = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
85
+ await writer.savePlatformServers(
86
+ SYSTEM_CONTEXT,
87
+ [makePlatformServer({ id: 'a1', label: 'Prod', apiKey: 'value' })],
88
+ 'a1'
89
+ );
90
+
91
+ const reader = new LocalComputeServerStore(configPath, OTHER_KEY);
92
+ const report = await reader.verifySecrets();
93
+ expect(report.ok).toBe(false);
94
+ expect(report.plaintextFound).toBe(false);
95
+ expect(report.failures).toHaveLength(1);
96
+ expect(report.failures[0]).toMatchObject({
97
+ serverId: 'a1',
98
+ serverLabel: 'Prod',
99
+ reason: 'key_mismatch'
100
+ });
101
+ });
102
+
103
+ it('verifySecrets reports plaintext_on_disk for hand-edited config', async () => {
104
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
105
+ await fs.writeFile(
106
+ configPath,
107
+ JSON.stringify({
108
+ servers: [
109
+ {
110
+ id: 'a1',
111
+ scope: 'platform',
112
+ sharedWith: 'all',
113
+ label: 'Legacy',
114
+ serverUrl: 'http://localhost:5000',
115
+ apiKey: 'plaintext-leftover'
116
+ }
117
+ ]
118
+ }),
119
+ 'utf-8'
120
+ );
121
+
122
+ const store = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
123
+ const report = await store.verifySecrets();
124
+ expect(report.ok).toBe(false);
125
+ expect(report.plaintextFound).toBe(true);
126
+ expect(report.failures[0]).toMatchObject({
127
+ serverId: 'a1',
128
+ reason: 'plaintext_on_disk'
129
+ });
130
+ });
131
+
132
+ it('verifySecrets returns ok when every encrypted apiKey decrypts', async () => {
133
+ const store = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
134
+ await store.savePlatformServers(
135
+ SYSTEM_CONTEXT,
136
+ [
137
+ makePlatformServer({ id: 'a1', apiKey: 'one' }),
138
+ makePlatformServer({ id: 'a2', apiKey: 'two' }),
139
+ makePlatformServer({ id: 'a3' }) // no apiKey — ignored
140
+ ],
141
+ 'a1'
142
+ );
143
+
144
+ const report = await store.verifySecrets();
145
+ expect(report.ok).toBe(true);
146
+ expect(report.failures).toEqual([]);
147
+ expect(report.plaintextFound).toBe(false);
148
+ });
149
+
150
+ it('refuses to read a legacy plaintext apiKey from disk', async () => {
151
+ // Simulate a hand-edited or legacy file with a plaintext apiKey.
152
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
153
+ await fs.writeFile(
154
+ configPath,
155
+ JSON.stringify({
156
+ servers: [
157
+ {
158
+ id: 'a1',
159
+ scope: 'platform',
160
+ sharedWith: 'all',
161
+ label: 'Legacy',
162
+ serverUrl: 'http://localhost:5000',
163
+ apiKey: 'plaintext-leftover'
164
+ }
165
+ ]
166
+ }),
167
+ 'utf-8'
168
+ );
169
+
170
+ const store = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
171
+ await expect(store.getConfig(SYSTEM_CONTEXT)).rejects.toThrow(/unencrypted apiKey/);
172
+ });
173
+
174
+ it('preserves servers with no apiKey', async () => {
175
+ const store = new LocalComputeServerStore(configPath, TEST_SECRET_KEY);
176
+ await store.savePlatformServers(
177
+ SYSTEM_CONTEXT,
178
+ [makePlatformServer({ label: 'No Key' })],
179
+ 'a1'
180
+ );
181
+
182
+ const got = await store.getConfig(SYSTEM_CONTEXT);
183
+ expect(got.servers[0].apiKey).toBeUndefined();
184
+ });
185
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { runDefinitionStoreConformance } from '@selvajs/platform/testing';
6
+ import { LocalDefinitionStore } from '../LocalDefinitionStore.js';
7
+
8
+ describe('LocalDefinitionStore', () => {
9
+ let tempDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-test-'));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tempDir, { recursive: true, force: true });
17
+ });
18
+
19
+ runDefinitionStoreConformance({
20
+ name: 'LocalDefinitionStore',
21
+ createStore: () => new LocalDefinitionStore(tempDir)
22
+ });
23
+ });
@@ -0,0 +1,28 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { runEventSinkConformance, type RecordingEventSink } from '@selvajs/platform/testing';
6
+ import { LocalDataProvider } from '../LocalDataProvider.js';
7
+
8
+ const tempDirs: string[] = [];
9
+
10
+ runEventSinkConformance({
11
+ name: 'LocalDataProvider',
12
+ createProvider: async (sink: RecordingEventSink) => {
13
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-events-'));
14
+ tempDirs.push(tempDir);
15
+ return LocalDataProvider.fromEnv(
16
+ {
17
+ DATA_PATH: tempDir,
18
+ SELVA_AT_REST_KEY: '0'.repeat(64)
19
+ },
20
+ sink
21
+ );
22
+ },
23
+ createActorId: async () => ({ userId: randomUUID() }),
24
+ cleanup: async () => {
25
+ await Promise.all(tempDirs.map((d) => fs.rm(d, { recursive: true, force: true })));
26
+ tempDirs.length = 0;
27
+ }
28
+ });