@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,290 @@
1
+ import * as path from 'node:path';
2
+ import {
3
+ isOrgServer,
4
+ isPlatformServer,
5
+ type IComputeServerStore,
6
+ type ComputeConfig,
7
+ type ComputeServerConfig,
8
+ type PlatformComputeServer,
9
+ type RequestContext
10
+ } from '@selvajs/platform';
11
+ import { readJsonFile, writeJsonFile } from './fsJson.js';
12
+ import {
13
+ decodeSecretKey,
14
+ decryptSecret,
15
+ encryptSecret,
16
+ isEncryptedSecret
17
+ } from './secretCrypto.js';
18
+
19
+ /**
20
+ * On-disk file shape. Single document holding *all* servers (platform +
21
+ * org-private), the global `defaultServerId`, and the per-org
22
+ * `orgDefaults` map. Spec §3.
23
+ *
24
+ * `apiKey` on disk is always an `enc:v1:<…>` envelope (AES-256-GCM); the
25
+ * store decrypts on read so callers see plaintext.
26
+ */
27
+ interface OnDiskShape {
28
+ servers: ComputeServerConfig[];
29
+ defaultServerId?: string;
30
+ orgDefaults?: Record<string, string>;
31
+ }
32
+
33
+ const EMPTY: OnDiskShape = { servers: [], orgDefaults: {} };
34
+
35
+ /**
36
+ * Result of {@link LocalComputeServerStore.verifySecrets}. One entry per
37
+ * server whose `apiKey` couldn't be loaded:
38
+ * - `plaintext_on_disk` — the field exists but isn't an `enc:v1:` envelope.
39
+ * Either a hand-edit or a migration regression. Security-relevant.
40
+ * - `key_mismatch` — envelope is valid but GCM auth tag verification
41
+ * fails under the current `SELVA_AT_REST_KEY`. The key was rotated or the
42
+ * data came from another deployment.
43
+ */
44
+ export type SecretVerificationFailureReason = 'key_mismatch' | 'plaintext_on_disk';
45
+
46
+ export interface SecretVerificationFailure {
47
+ serverId: string;
48
+ serverLabel: string;
49
+ reason: SecretVerificationFailureReason;
50
+ /** Underlying error message for `key_mismatch`. Absent for plaintext. */
51
+ cause?: string;
52
+ }
53
+
54
+ export interface SecretVerificationReport {
55
+ ok: boolean;
56
+ failures: SecretVerificationFailure[];
57
+ /** True if at least one row holds an unencrypted apiKey on disk. */
58
+ plaintextFound: boolean;
59
+ }
60
+
61
+ /**
62
+ * Reads/writes compute.config.json. The file is re-read on every read call
63
+ * so changes take effect without a restart.
64
+ *
65
+ * Mutation methods are scope-targeted (`savePlatformServers`,
66
+ * `saveOrgServers`, `setOrgDefault`) — each preserves rows in the other
67
+ * scopes untouched.
68
+ */
69
+ export class LocalComputeServerStore implements IComputeServerStore {
70
+ static fromEnv(env: Record<string, string | undefined>): LocalComputeServerStore {
71
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
72
+ if (!env.SELVA_AT_REST_KEY) {
73
+ throw new Error(
74
+ 'Missing required env var: SELVA_AT_REST_KEY (32-byte hex or base64). ' +
75
+ "Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
76
+ );
77
+ }
78
+ return new LocalComputeServerStore(
79
+ path.join(env.DATA_PATH, 'compute.config.json'),
80
+ decodeSecretKey(env.SELVA_AT_REST_KEY)
81
+ );
82
+ }
83
+
84
+ constructor(
85
+ private readonly configFilePath: string,
86
+ private readonly secretKey: Buffer
87
+ ) {}
88
+
89
+ private async readAll(): Promise<OnDiskShape> {
90
+ const raw = await readJsonFile<OnDiskShape>(this.configFilePath, EMPTY);
91
+ return {
92
+ servers: raw.servers ?? [],
93
+ defaultServerId: raw.defaultServerId,
94
+ orgDefaults: raw.orgDefaults ?? {}
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Per-row tolerant decrypt. A row whose ciphertext can't be authenticated
100
+ * under the current `SELVA_AT_REST_KEY` is returned with `apiKey: undefined`
101
+ * and a warning logged once. The page that loaded the config keeps
102
+ * rendering; solves against that server will fail later when Rhino.Compute
103
+ * rejects the missing key.
104
+ *
105
+ * Boot-time `verifySecrets()` is the strict counterpart — call that from
106
+ * the app entrypoint to refuse to start when this state is detected.
107
+ *
108
+ * Plaintext-on-disk is still hard-fail. That state is never produced by
109
+ * the store itself (every write goes through `encryptApiKeys`), so seeing
110
+ * it means someone hand-edited the file with a real secret in plaintext —
111
+ * which is a security issue we should surface loudly, not paper over.
112
+ */
113
+ private decryptApiKeys(servers: ComputeServerConfig[]): ComputeServerConfig[] {
114
+ return servers.map((s) => {
115
+ if (!s.apiKey) return s;
116
+ if (!isEncryptedSecret(s.apiKey)) {
117
+ throw new Error(
118
+ `compute.config.json contains an unencrypted apiKey for server "${s.label}" (${s.id}). ` +
119
+ 'Re-enter the key via /admin/compute so it is stored encrypted.'
120
+ );
121
+ }
122
+ try {
123
+ return { ...s, apiKey: decryptSecret(s.apiKey, this.secretKey) };
124
+ } catch (cause) {
125
+ console.warn(
126
+ `[selva] Could not decrypt apiKey for compute server "${s.label}" (${s.id}). ` +
127
+ 'The stored ciphertext does not match the current SELVA_AT_REST_KEY. ' +
128
+ 'This server will be returned without an apiKey; solves against it will fail. ' +
129
+ 'Re-enter the key via /admin/compute, or restore the original SELVA_AT_REST_KEY. ' +
130
+ 'See docs/Troubleshooting.md.',
131
+ cause
132
+ );
133
+ return { ...s, apiKey: undefined };
134
+ }
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Boot-time integrity check. Reads every server row and attempts to
140
+ * decrypt each encrypted `apiKey`. Returns a structured report — does NOT
141
+ * throw. The caller decides what to do (refuse boot, log + degrade, etc.).
142
+ *
143
+ * Use this from app startup (`hooks.server.ts`) so a key mismatch fails
144
+ * loudly at deploy time instead of as a blank page when a user first hits
145
+ * a route that loads compute config.
146
+ */
147
+ async verifySecrets(): Promise<SecretVerificationReport> {
148
+ const all = await this.readAll();
149
+ const failures: SecretVerificationFailure[] = [];
150
+ let plaintextFound = false;
151
+
152
+ for (const s of all.servers) {
153
+ if (!s.apiKey) continue;
154
+ if (!isEncryptedSecret(s.apiKey)) {
155
+ plaintextFound = true;
156
+ failures.push({
157
+ serverId: s.id,
158
+ serverLabel: s.label,
159
+ reason: 'plaintext_on_disk'
160
+ });
161
+ continue;
162
+ }
163
+ try {
164
+ decryptSecret(s.apiKey, this.secretKey);
165
+ } catch (cause) {
166
+ failures.push({
167
+ serverId: s.id,
168
+ serverLabel: s.label,
169
+ reason: 'key_mismatch',
170
+ cause: cause instanceof Error ? cause.message : String(cause)
171
+ });
172
+ }
173
+ }
174
+
175
+ return { ok: failures.length === 0, failures, plaintextFound };
176
+ }
177
+
178
+ private encryptApiKeys(servers: ComputeServerConfig[]): ComputeServerConfig[] {
179
+ return servers.map((s) => {
180
+ if (!s.apiKey) return s;
181
+ if (isEncryptedSecret(s.apiKey)) return s;
182
+ return { ...s, apiKey: encryptSecret(s.apiKey, this.secretKey) };
183
+ });
184
+ }
185
+
186
+ async getConfig(_ctx: RequestContext): Promise<ComputeConfig> {
187
+ const all = await this.readAll();
188
+ return {
189
+ servers: this.decryptApiKeys(all.servers),
190
+ defaultServerId: all.defaultServerId,
191
+ orgDefaults: all.orgDefaults
192
+ };
193
+ }
194
+
195
+ async savePlatformServers(
196
+ _ctx: RequestContext,
197
+ servers: ComputeServerConfig[],
198
+ defaultServerId: string | undefined
199
+ ): Promise<void> {
200
+ const all = await this.readAll();
201
+ const orgRows = all.servers.filter(isOrgServer);
202
+ const platformRows = this.encryptApiKeys(servers.filter(isPlatformServer));
203
+
204
+ await writeJsonFile<OnDiskShape>(this.configFilePath, {
205
+ servers: [...platformRows, ...orgRows],
206
+ defaultServerId,
207
+ orgDefaults: all.orgDefaults
208
+ });
209
+ }
210
+
211
+ async saveOrgServers(
212
+ _ctx: RequestContext,
213
+ orgId: string,
214
+ servers: ComputeServerConfig[],
215
+ defaultServerId?: string | null
216
+ ): Promise<void> {
217
+ const all = await this.readAll();
218
+ const platformRows = all.servers.filter(isPlatformServer);
219
+ const otherOrgRows = all.servers.filter((s) => isOrgServer(s) && s.ownerOrgId !== orgId);
220
+ const thisOrgRows = this.encryptApiKeys(
221
+ servers
222
+ .filter((_s): _s is ComputeServerConfig => true)
223
+ .map((s) =>
224
+ isOrgServer(s)
225
+ ? { ...s, ownerOrgId: orgId }
226
+ : // Coerce — caller passed something with the wrong/missing scope.
227
+ ({ ...s, scope: 'org', ownerOrgId: orgId } as ComputeServerConfig)
228
+ )
229
+ );
230
+
231
+ const orgDefaults = { ...(all.orgDefaults ?? {}) };
232
+ if (defaultServerId === null) {
233
+ delete orgDefaults[orgId];
234
+ } else if (typeof defaultServerId === 'string') {
235
+ orgDefaults[orgId] = defaultServerId;
236
+ }
237
+
238
+ await writeJsonFile<OnDiskShape>(this.configFilePath, {
239
+ servers: [...platformRows, ...otherOrgRows, ...thisOrgRows],
240
+ defaultServerId: all.defaultServerId,
241
+ orgDefaults
242
+ });
243
+ }
244
+
245
+ async setOrgDefault(_ctx: RequestContext, orgId: string, serverId: string | null): Promise<void> {
246
+ const all = await this.readAll();
247
+ const orgDefaults = { ...(all.orgDefaults ?? {}) };
248
+ if (serverId === null) {
249
+ delete orgDefaults[orgId];
250
+ } else {
251
+ orgDefaults[orgId] = serverId;
252
+ }
253
+ await writeJsonFile<OnDiskShape>(this.configFilePath, { ...all, orgDefaults });
254
+ }
255
+
256
+ async deleteByOrg(_ctx: RequestContext, orgId: string): Promise<void> {
257
+ const all = await this.readAll();
258
+
259
+ // Drop org-private rows owned by this org.
260
+ const remaining = all.servers.filter((s) => !(isOrgServer(s) && s.ownerOrgId === orgId));
261
+
262
+ // Strip this org from any platform server's `sharedWith` allowlist.
263
+ const cleaned: ComputeServerConfig[] = remaining.map((s) => {
264
+ if (!isPlatformServer(s)) return s;
265
+ if (s.sharedWith === 'all') return s;
266
+ if (!s.sharedWith.includes(orgId)) return s;
267
+ const next: PlatformComputeServer = {
268
+ ...s,
269
+ sharedWith: s.sharedWith.filter((id) => id !== orgId)
270
+ };
271
+ return next;
272
+ });
273
+
274
+ const orgDefaults = { ...(all.orgDefaults ?? {}) };
275
+ const hadDefault = orgId in orgDefaults;
276
+ delete orgDefaults[orgId];
277
+
278
+ const changed =
279
+ cleaned.length !== all.servers.length ||
280
+ hadDefault ||
281
+ cleaned.some((c, i) => c !== all.servers[i]);
282
+ if (!changed) return;
283
+
284
+ await writeJsonFile<OnDiskShape>(this.configFilePath, {
285
+ servers: cleaned,
286
+ defaultServerId: all.defaultServerId,
287
+ orgDefaults
288
+ });
289
+ }
290
+ }
@@ -0,0 +1,148 @@
1
+ import type {
2
+ IDataProvider,
3
+ IOrgStore,
4
+ IProjectStore,
5
+ IDefinitionStore,
6
+ IComputeServerStore,
7
+ IInviteStore,
8
+ IShareLinkStore,
9
+ IUserProfileStore,
10
+ IPlatformPermissionStore,
11
+ IPlatformProjectGrantStore,
12
+ IEventSink,
13
+ RequestContext
14
+ } from '@selvajs/platform';
15
+ import { NoopEventSink } from '@selvajs/platform';
16
+ import * as path from 'node:path';
17
+ import { LocalOrgStore, LocalOrgStoreLoader } from './LocalOrgStore.js';
18
+ import { LocalProjectStore } from './LocalProjectStore.js';
19
+ import { LocalDefinitionStore } from './LocalDefinitionStore.js';
20
+ import { LocalComputeServerStore } from './LocalComputeServerStore.js';
21
+ import { LocalInviteStore } from './LocalInviteStore.js';
22
+ import { LocalShareLinkStore } from './LocalShareLinkStore.js';
23
+ import { LocalPlatformProjectGrantStore } from './LocalPlatformProjectGrantStore.js';
24
+ import { LocalUserProfileProvider } from '../userProfile/LocalUserProfileProvider.js';
25
+ import { LocalPlatformPermissionStore } from '../permissions/LocalPlatformPermissionStore.js';
26
+ import { createLocalUserDataStore, type LocalUserDataStore } from './userData.js';
27
+
28
+ /**
29
+ * Composition of every local-provider data store. One `LocalOrgStoreLoader`
30
+ * is shared across org + project stores so they see the same cache and
31
+ * atomic write path.
32
+ *
33
+ * The `LocalUserDataStore` is similarly shared across the permissions and
34
+ * profile stores: both write to `user-data.json`, and `ensureUser` seeds
35
+ * exactly the same row both stores read from. This is the local equivalent
36
+ * of Supabase's `handle_new_auth_user` trigger.
37
+ *
38
+ * Stores are passed as a record so adding a new store doesn't ripple through
39
+ * test fixtures and call sites.
40
+ */
41
+ export class LocalDataProvider implements IDataProvider {
42
+ readonly orgs: IOrgStore;
43
+ readonly projects: IProjectStore;
44
+ readonly definitions: IDefinitionStore;
45
+ readonly computeServer: IComputeServerStore;
46
+ readonly invites: IInviteStore;
47
+ readonly shareLinks: IShareLinkStore;
48
+ readonly userProfile: IUserProfileStore;
49
+ readonly permissions: IPlatformPermissionStore;
50
+ readonly platformProjectGrants: IPlatformProjectGrantStore;
51
+
52
+ private readonly userData: LocalUserDataStore;
53
+
54
+ private constructor(
55
+ stores: Omit<IDataProvider, 'ensureUser' | 'onUserDeleted'>,
56
+ userData: LocalUserDataStore
57
+ ) {
58
+ this.orgs = stores.orgs;
59
+ this.projects = stores.projects;
60
+ this.definitions = stores.definitions;
61
+ this.computeServer = stores.computeServer;
62
+ this.invites = stores.invites;
63
+ this.shareLinks = stores.shareLinks;
64
+ this.userProfile = stores.userProfile;
65
+ this.permissions = stores.permissions;
66
+ this.platformProjectGrants = stores.platformProjectGrants;
67
+ this.userData = userData;
68
+ }
69
+
70
+ /**
71
+ * Idempotently register a user in the data layer. Called from
72
+ * `hooks.server.ts` on every authed request — the local equivalent of
73
+ * Supabase's `handle_new_auth_user` trigger. After this completes the
74
+ * user has an empty row in `user-data.json` that the permissions and
75
+ * profile stores can read and update.
76
+ *
77
+ * `ctx` is unused — registration runs as a system operation regardless of
78
+ * the calling user. Argument is kept for interface symmetry with adapters
79
+ * that need it.
80
+ */
81
+ async ensureUser(_ctx: RequestContext, userId: string): Promise<void> {
82
+ await this.userData.ensure(userId);
83
+ }
84
+
85
+ /**
86
+ * Cascade hook called after the auth provider deletes a user. Removes the
87
+ * matching `user-data.json` row so the data layer doesn't accumulate
88
+ * orphans. Tolerates missing rows.
89
+ */
90
+ async onUserDeleted(_ctx: RequestContext, userId: string): Promise<void> {
91
+ try {
92
+ await this.userData.deleteUser(userId);
93
+ } catch {
94
+ // Already absent — nothing to clean up.
95
+ }
96
+ }
97
+
98
+ static fromEnv(
99
+ env: Record<string, string | undefined>,
100
+ events: IEventSink = new NoopEventSink()
101
+ ): LocalDataProvider {
102
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
103
+ const dataPath = env.DATA_PATH;
104
+ const userDataFilePath = path.join(dataPath, 'user-data.json');
105
+
106
+ const loader = new LocalOrgStoreLoader(dataPath);
107
+ const platformProjectGrants = LocalPlatformProjectGrantStore.fromEnv(env);
108
+ const invites = LocalInviteStore.fromEnv(env, events);
109
+ const computeServer = LocalComputeServerStore.fromEnv(env);
110
+ const projects = new LocalProjectStore({ loader, grants: platformProjectGrants, events });
111
+ const definitions = new LocalDefinitionStore(dataPath, undefined, events);
112
+ const shareLinks = new LocalShareLinkStore({
113
+ filePath: path.join(dataPath, 'share-links.json'),
114
+ events
115
+ });
116
+ const orgs = new LocalOrgStore({
117
+ loader,
118
+ invites,
119
+ computeServer,
120
+ grants: platformProjectGrants,
121
+ events
122
+ });
123
+
124
+ // Wire cross-store deps that aren't constructor-injected:
125
+ // - canEditDefinition needs the project store for `listPublic`
126
+ // - share-link resolution needs the definition store to enforce the §7
127
+ // soft-delete cascade (Supabase does the equivalent via JOIN)
128
+ definitions.setProjectProvider(projects);
129
+ shareLinks.setDefinitionProvider(definitions);
130
+
131
+ const userData = createLocalUserDataStore(userDataFilePath);
132
+
133
+ return new LocalDataProvider(
134
+ {
135
+ orgs,
136
+ projects,
137
+ definitions,
138
+ computeServer,
139
+ invites,
140
+ shareLinks,
141
+ userProfile: new LocalUserProfileProvider(userDataFilePath),
142
+ permissions: new LocalPlatformPermissionStore(userDataFilePath),
143
+ platformProjectGrants
144
+ },
145
+ userData
146
+ );
147
+ }
148
+ }