@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,369 @@
1
+ import * as path from 'node:path';
2
+ import type {
3
+ IDefinitionStore,
4
+ IProjectStore,
5
+ IEventSink,
6
+ DefinitionRecord,
7
+ DefinitionRecordPatch,
8
+ DefinitionVersion,
9
+ RequestContext,
10
+ DefinitionListOptions,
11
+ ListOptions,
12
+ Page
13
+ } from '@selvajs/platform';
14
+ import {
15
+ ProviderError,
16
+ auditUpdate,
17
+ auditSoftDelete,
18
+ actorFrom,
19
+ NoopEventSink
20
+ } from '@selvajs/platform';
21
+ import { paginate, applyOrder } from './pagination.js';
22
+ import { readJsonFile, writeJsonFile } from './fsJson.js';
23
+
24
+ interface DefinitionsConfig {
25
+ definitions: Record<string, DefinitionRecord>;
26
+ definitionVersions: Record<string, DefinitionVersion>;
27
+ }
28
+
29
+ /** Always return a fresh object — `readJsonFile` returns its fallback by
30
+ * reference when the file is missing, so a shared singleton would let one
31
+ * read pollute the next. */
32
+ const empty = (): DefinitionsConfig => ({ definitions: {}, definitionVersions: {} });
33
+
34
+ export class LocalDefinitionStore implements IDefinitionStore {
35
+ private readonly configPath: string;
36
+ private readonly events: IEventSink;
37
+ private projectProvider?: IProjectStore;
38
+
39
+ static fromEnv(env: Record<string, string | undefined>): LocalDefinitionStore {
40
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
41
+ return new LocalDefinitionStore(env.DATA_PATH);
42
+ }
43
+
44
+ constructor(
45
+ definitionsPath: string,
46
+ projectProvider?: IProjectStore,
47
+ events: IEventSink = new NoopEventSink()
48
+ ) {
49
+ this.configPath = path.join(definitionsPath, 'definitions-config.json');
50
+ this.projectProvider = projectProvider;
51
+ this.events = events;
52
+ }
53
+
54
+ setProjectProvider(projectProvider: IProjectStore): void {
55
+ this.projectProvider = projectProvider;
56
+ }
57
+
58
+ private async readConfig(): Promise<DefinitionsConfig> {
59
+ return readJsonFile<DefinitionsConfig>(this.configPath, empty());
60
+ }
61
+
62
+ private live(record: DefinitionRecord | undefined | null): record is DefinitionRecord {
63
+ return Boolean(record && record.deletedAt == null);
64
+ }
65
+
66
+ private async writeConfig(config: DefinitionsConfig): Promise<void> {
67
+ await writeJsonFile(this.configPath, config);
68
+ }
69
+
70
+ private sortedRecords(
71
+ records: DefinitionRecord[],
72
+ opts?: DefinitionListOptions
73
+ ): DefinitionRecord[] {
74
+ const defaulted: DefinitionListOptions = {
75
+ ...opts,
76
+ orderBy: opts?.orderBy ?? 'name',
77
+ orderDir: opts?.orderDir ?? 'asc'
78
+ };
79
+ return applyOrder([...records], defaulted, (r, field) => {
80
+ if (field === 'name') return r.displayName.toLowerCase();
81
+ if (field === 'solveCount') return r.solveCount ?? 0;
82
+ return (r as unknown as Record<string, unknown>)[field];
83
+ });
84
+ }
85
+
86
+ private visibleRecords(
87
+ records: DefinitionRecord[],
88
+ opts?: DefinitionListOptions
89
+ ): DefinitionRecord[] {
90
+ const filtered = records.filter((r) => r?.displayName && this.live(r));
91
+ if (opts?.statuses?.length) {
92
+ const allowed = new Set(opts.statuses);
93
+ return filtered.filter((r) => allowed.has(r.status));
94
+ }
95
+ return filtered.filter((r) => {
96
+ if (r.status === 'pending' && !opts?.includePending) return false;
97
+ if (r.status === 'archived' && !opts?.includeArchived) return false;
98
+ return true;
99
+ });
100
+ }
101
+
102
+ async list(_ctx: RequestContext, opts?: DefinitionListOptions): Promise<Page<DefinitionRecord>> {
103
+ const config = await this.readConfig();
104
+ const records = this.visibleRecords(Object.values(config.definitions), opts);
105
+ return paginate(this.sortedRecords(records, opts), opts);
106
+ }
107
+
108
+ async listByProject(
109
+ _ctx: RequestContext,
110
+ projectId: string,
111
+ opts?: DefinitionListOptions
112
+ ): Promise<Page<DefinitionRecord>> {
113
+ const config = await this.readConfig();
114
+ const records = this.visibleRecords(
115
+ Object.values(config.definitions).filter((r) => r?.projectId === projectId),
116
+ opts
117
+ );
118
+ return paginate(this.sortedRecords(records, opts), opts);
119
+ }
120
+
121
+ async listPublic(
122
+ ctx: RequestContext,
123
+ opts?: DefinitionListOptions & { orgId?: string }
124
+ ): Promise<Page<DefinitionRecord>> {
125
+ if (!this.projectProvider) {
126
+ // Pre-wiring fallback: behave as if the default project is public, which
127
+ // matches the local bootstrap. Once setProjectProvider is called this
128
+ // branch stops executing.
129
+ return this.list(ctx, opts);
130
+ }
131
+
132
+ const config = await this.readConfig();
133
+ const records = Object.values(config.definitions).filter((r): r is DefinitionRecord =>
134
+ Boolean(r?.displayName && this.live(r))
135
+ );
136
+
137
+ const projectIds = Array.from(new Set(records.map((r) => r.projectId)));
138
+ const projects = await Promise.all(
139
+ projectIds.map((id) => this.projectProvider!.getProject(ctx, id))
140
+ );
141
+ const publicProjectIds = new Set(
142
+ projects
143
+ .filter(
144
+ (p): p is NonNullable<typeof p> =>
145
+ p !== null && p.visibility === 'public' && (!opts?.orgId || p.orgId === opts.orgId)
146
+ )
147
+ .map((p) => p.id)
148
+ );
149
+
150
+ const publicRecords = this.visibleRecords(
151
+ records.filter((r) => publicProjectIds.has(r.projectId)),
152
+ opts
153
+ );
154
+ return paginate(this.sortedRecords(publicRecords, opts), opts);
155
+ }
156
+
157
+ async get(_ctx: RequestContext, guid: string): Promise<DefinitionRecord | null> {
158
+ const config = await this.readConfig();
159
+ const r = config.definitions[guid];
160
+ return this.live(r) ? r : null;
161
+ }
162
+
163
+ async create(ctx: RequestContext, record: DefinitionRecord): Promise<void> {
164
+ const config = await this.readConfig();
165
+ const actor = ctx.userId || record.ownerId;
166
+ config.definitions[record.guid] = {
167
+ ...record,
168
+ createdBy: record.createdBy || actor,
169
+ updatedBy: record.updatedBy || actor,
170
+ liveVersionId: record.liveVersionId ?? null,
171
+ draftVersionId: record.draftVersionId ?? null,
172
+ deletedAt: null
173
+ };
174
+ await this.writeConfig(config);
175
+ await this.events.emit({
176
+ type: 'definition.created',
177
+ definitionId: record.guid,
178
+ projectId: record.projectId,
179
+ actorId: actorFrom(ctx)
180
+ });
181
+ }
182
+
183
+ async update(ctx: RequestContext, guid: string, patch: DefinitionRecordPatch): Promise<void> {
184
+ const config = await this.readConfig();
185
+ const existing = config.definitions[guid];
186
+ if (!this.live(existing)) throw new ProviderError(`Definition '${guid}' not found`, 404);
187
+
188
+ const clearable = (v: unknown) => (v === null ? undefined : v);
189
+ config.definitions[guid] = {
190
+ ...existing,
191
+ ...(patch.displayName !== undefined && { displayName: patch.displayName }),
192
+ ...(patch.description !== undefined && {
193
+ description: clearable(patch.description) as string | undefined
194
+ }),
195
+ ...(patch.category !== undefined && {
196
+ category: clearable(patch.category) as string | undefined
197
+ }),
198
+ ...(patch.tags !== undefined && { tags: clearable(patch.tags) as string[] | undefined }),
199
+ ...(patch.coverImage !== undefined && {
200
+ coverImage: clearable(patch.coverImage) as string | undefined
201
+ }),
202
+ ...(patch.projectId !== undefined && { projectId: patch.projectId }),
203
+ ...(patch.computeServerId !== undefined && {
204
+ computeServerId: clearable(patch.computeServerId) as string | undefined
205
+ }),
206
+ ...(patch.status !== undefined && { status: patch.status }),
207
+ ...(patch.ownerId !== undefined && { ownerId: patch.ownerId }),
208
+ ...auditUpdate(ctx, existing.updatedBy ?? existing.ownerId)
209
+ };
210
+ await this.writeConfig(config);
211
+ }
212
+
213
+ async delete(ctx: RequestContext, guid: string): Promise<void> {
214
+ const config = await this.readConfig();
215
+ const existing = config.definitions[guid];
216
+ if (!this.live(existing)) return;
217
+ Object.assign(existing, auditSoftDelete(ctx, existing.updatedBy ?? existing.ownerId));
218
+ await this.writeConfig(config);
219
+ await this.events.emit({
220
+ type: 'definition.deleted',
221
+ definitionId: guid,
222
+ actorId: actorFrom(ctx)
223
+ });
224
+ }
225
+
226
+ async incrementSolveCount(_ctx: RequestContext, guid: string): Promise<void> {
227
+ const config = await this.readConfig();
228
+ const existing = config.definitions[guid];
229
+ if (!this.live(existing)) return;
230
+ existing.solveCount = (existing.solveCount ?? 0) + 1;
231
+ existing.updatedAt = new Date().toISOString();
232
+ await this.writeConfig(config);
233
+ }
234
+
235
+ // ============================================================================
236
+ // Versions (spec §6)
237
+ // ============================================================================
238
+
239
+ async createVersion(ctx: RequestContext, version: DefinitionVersion): Promise<void> {
240
+ const config = await this.readConfig();
241
+ const parent = config.definitions[version.definitionId];
242
+ if (!this.live(parent)) {
243
+ throw new ProviderError(`Definition '${version.definitionId}' not found`, 404);
244
+ }
245
+ if (config.definitionVersions[version.id]) {
246
+ throw new ProviderError(`Version '${version.id}' already exists`, 409);
247
+ }
248
+ config.definitionVersions[version.id] = { ...version };
249
+ await this.writeConfig(config);
250
+ await this.events.emit({
251
+ type: 'definition_version.created',
252
+ versionId: version.id,
253
+ definitionId: version.definitionId,
254
+ actorId: actorFrom(ctx)
255
+ });
256
+ }
257
+
258
+ async listVersions(
259
+ _ctx: RequestContext,
260
+ definitionId: string,
261
+ opts?: ListOptions
262
+ ): Promise<Page<DefinitionVersion>> {
263
+ const config = await this.readConfig();
264
+ const parent = config.definitions[definitionId];
265
+ if (!this.live(parent)) return paginate([], opts);
266
+ const rows = Object.values(config.definitionVersions)
267
+ .filter((v) => v.definitionId === definitionId)
268
+ .sort((a, b) => b.versionNumber - a.versionNumber);
269
+ return paginate(rows, opts);
270
+ }
271
+
272
+ async getVersion(_ctx: RequestContext, versionId: string): Promise<DefinitionVersion | null> {
273
+ const config = await this.readConfig();
274
+ return config.definitionVersions[versionId] ?? null;
275
+ }
276
+
277
+ async deleteVersion(ctx: RequestContext, versionId: string): Promise<void> {
278
+ const config = await this.readConfig();
279
+ const version = config.definitionVersions[versionId];
280
+ if (!version) return;
281
+ const parent = config.definitions[version.definitionId];
282
+ // §6 deletion protection — cannot delete a version while it's serving
283
+ // either channel. Caller must repoint live/draft first.
284
+ if (parent && (parent.liveVersionId === versionId || parent.draftVersionId === versionId)) {
285
+ throw new ProviderError(
286
+ `Version '${versionId}' is referenced by liveVersionId or draftVersionId`,
287
+ 409
288
+ );
289
+ }
290
+ delete config.definitionVersions[versionId];
291
+ await this.writeConfig(config);
292
+ await this.events.emit({
293
+ type: 'definition_version.deleted',
294
+ versionId,
295
+ actorId: actorFrom(ctx)
296
+ });
297
+ }
298
+
299
+ async setLiveVersion(
300
+ ctx: RequestContext,
301
+ definitionId: string,
302
+ versionId: string
303
+ ): Promise<void> {
304
+ await this.repoint('live', ctx, definitionId, versionId);
305
+ }
306
+
307
+ async setDraftVersion(
308
+ ctx: RequestContext,
309
+ definitionId: string,
310
+ versionId: string
311
+ ): Promise<void> {
312
+ await this.repoint('draft', ctx, definitionId, versionId);
313
+ }
314
+
315
+ async attachInitialVersion(
316
+ ctx: RequestContext,
317
+ definitionId: string,
318
+ versionId: string
319
+ ): Promise<void> {
320
+ // Single read-modify-write pass: both pointers + status updated together.
321
+ // `writeConfig` is one fs operation, so the 'pending' → 'draft'
322
+ // transition is atomic from any subsequent reader's perspective.
323
+ const config = await this.readConfig();
324
+ const record = config.definitions[definitionId];
325
+ if (!this.live(record)) throw new ProviderError(`Definition '${definitionId}' not found`, 404);
326
+ const version = config.definitionVersions[versionId];
327
+ if (!version || version.definitionId !== definitionId) {
328
+ throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
329
+ }
330
+ record.liveVersionId = versionId;
331
+ record.draftVersionId = versionId;
332
+ record.status = 'draft';
333
+ Object.assign(record, auditUpdate(ctx, record.updatedBy ?? record.ownerId));
334
+ await this.writeConfig(config);
335
+ // No `definition.published` event — see interface doc. The parent's
336
+ // `definition.created` + `definition_version.created` (emitted earlier
337
+ // in this transaction) cover the bootstrap.
338
+ }
339
+
340
+ private async repoint(
341
+ channel: 'live' | 'draft',
342
+ ctx: RequestContext,
343
+ definitionId: string,
344
+ versionId: string
345
+ ): Promise<void> {
346
+ const config = await this.readConfig();
347
+ const record = config.definitions[definitionId];
348
+ if (!this.live(record)) throw new ProviderError(`Definition '${definitionId}' not found`, 404);
349
+ const version = config.definitionVersions[versionId];
350
+ if (!version || version.definitionId !== definitionId) {
351
+ throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
352
+ }
353
+ if (channel === 'live') record.liveVersionId = versionId;
354
+ else record.draftVersionId = versionId;
355
+ Object.assign(record, auditUpdate(ctx, record.updatedBy ?? record.ownerId));
356
+ await this.writeConfig(config);
357
+ // Only `live` advancement is the published-event trigger. Draft
358
+ // repointing is silent — it's the editor's working pointer, not a
359
+ // publication signal.
360
+ if (channel === 'live') {
361
+ await this.events.emit({
362
+ type: 'definition.published',
363
+ definitionId,
364
+ versionId,
365
+ actorId: actorFrom(ctx)
366
+ });
367
+ }
368
+ }
369
+ }
@@ -0,0 +1,117 @@
1
+ import * as path from 'node:path';
2
+ import type {
3
+ IInviteStore,
4
+ IEventSink,
5
+ Invite,
6
+ RequestContext,
7
+ ListOptions,
8
+ Page
9
+ } from '@selvajs/platform';
10
+ import { NoopEventSink, actorFrom } from '@selvajs/platform';
11
+ import { paginate, applyOrder } from './pagination.js';
12
+ import { readJsonFile, writeJsonFile } from './fsJson.js';
13
+
14
+ interface InvitesFile {
15
+ invites: Invite[];
16
+ }
17
+ const EMPTY: InvitesFile = { invites: [] };
18
+
19
+ /**
20
+ * Filesystem-backed invite store. No per-call scoping by ctx.userId — the
21
+ * route layer gates admin actions. `getByTokenHash` is the sole unauthenticated
22
+ * read and is scoped by the hashed token (caller hashes the raw URL token
23
+ * before lookup); it hides expired and already-accepted invites so a reused
24
+ * link surfaces a clean error.
25
+ */
26
+ export class LocalInviteStore implements IInviteStore {
27
+ private readonly filePath: string;
28
+
29
+ static fromEnv(
30
+ env: Record<string, string | undefined>,
31
+ events: IEventSink = new NoopEventSink()
32
+ ): LocalInviteStore {
33
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
34
+ return new LocalInviteStore(env.DATA_PATH, events);
35
+ }
36
+
37
+ constructor(
38
+ dataPath: string,
39
+ private readonly events: IEventSink = new NoopEventSink()
40
+ ) {
41
+ this.filePath = path.join(dataPath, 'invites.json');
42
+ }
43
+
44
+ private async load(): Promise<InvitesFile> {
45
+ return readJsonFile<InvitesFile>(this.filePath, EMPTY);
46
+ }
47
+
48
+ private async save(file: InvitesFile): Promise<void> {
49
+ await writeJsonFile(this.filePath, file);
50
+ }
51
+
52
+ async create(ctx: RequestContext, invite: Invite): Promise<void> {
53
+ const file = await this.load();
54
+ file.invites.push(invite);
55
+ await this.save(file);
56
+ await this.events.emit({
57
+ type: 'invite.created',
58
+ inviteId: invite.id,
59
+ orgId: invite.orgId,
60
+ email: invite.email,
61
+ actorId: actorFrom(ctx)
62
+ });
63
+ }
64
+
65
+ async getByTokenHash(_ctx: RequestContext, tokenHash: string): Promise<Invite | null> {
66
+ const { invites } = await this.load();
67
+ const invite = invites.find((i) => i.tokenHash === tokenHash);
68
+ if (!invite) return null;
69
+ if (invite.acceptedAt) return null;
70
+ if (Date.parse(invite.expiresAt) <= Date.now()) return null;
71
+ return invite;
72
+ }
73
+
74
+ async listByOrg(_ctx: RequestContext, orgId: string, opts?: ListOptions): Promise<Page<Invite>> {
75
+ const { invites } = await this.load();
76
+ const filtered = invites.filter((i) => i.orgId === orgId);
77
+ return paginate(applyOrder(filtered, opts), opts);
78
+ }
79
+
80
+ async markAccepted(ctx: RequestContext, id: string, userId: string): Promise<void> {
81
+ const file = await this.load();
82
+ const invite = file.invites.find((i) => i.id === id);
83
+ if (!invite || invite.acceptedAt) return;
84
+ invite.acceptedAt = new Date().toISOString();
85
+ invite.acceptedByUserId = userId;
86
+ await this.save(file);
87
+ await this.events.emit({
88
+ type: 'invite.accepted',
89
+ inviteId: invite.id,
90
+ orgId: invite.orgId,
91
+ userId,
92
+ actorId: actorFrom(ctx)
93
+ });
94
+ }
95
+
96
+ async revoke(ctx: RequestContext, id: string): Promise<void> {
97
+ const file = await this.load();
98
+ const target = file.invites.find((i) => i.id === id && !i.acceptedAt);
99
+ if (!target) return;
100
+ file.invites = file.invites.filter((i) => i.id !== id || i.acceptedAt);
101
+ await this.save(file);
102
+ await this.events.emit({
103
+ type: 'invite.revoked',
104
+ inviteId: id,
105
+ orgId: target.orgId,
106
+ actorId: actorFrom(ctx)
107
+ });
108
+ }
109
+
110
+ async deleteByOrg(_ctx: RequestContext, orgId: string): Promise<void> {
111
+ const file = await this.load();
112
+ const before = file.invites.length;
113
+ file.invites = file.invites.filter((i) => i.orgId !== orgId);
114
+ if (file.invites.length === before) return;
115
+ await this.save(file);
116
+ }
117
+ }