@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,28 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ /**
5
+ * Read a JSON file, returning a fallback if the file does not exist.
6
+ * Any other error propagates.
7
+ */
8
+ export async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
9
+ try {
10
+ const raw = await fs.readFile(filePath, 'utf-8');
11
+ return JSON.parse(raw) as T;
12
+ } catch (err) {
13
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return fallback;
14
+ throw err;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Atomically write JSON: ensure the parent directory exists, write to a
20
+ * sibling .tmp file, then rename into place. Prevents partial writes from
21
+ * corrupting the target on crash or interrupt.
22
+ */
23
+ export async function writeJsonFile<T>(filePath: string, data: T): Promise<void> {
24
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
25
+ const tmp = `${filePath}.tmp`;
26
+ await fs.writeFile(tmp, JSON.stringify(data, null, '\t'), 'utf-8');
27
+ await fs.rename(tmp, filePath);
28
+ }
@@ -0,0 +1,16 @@
1
+ export { LocalOrgStore, LocalOrgStoreLoader } from './LocalOrgStore.js';
2
+ export type { LocalOrgStoreData, LocalOrgStoreOptions } from './LocalOrgStore.js';
3
+ export { LocalProjectStore } from './LocalProjectStore.js';
4
+ export type { LocalProjectStoreOptions } from './LocalProjectStore.js';
5
+ export { LocalDefinitionStore } from './LocalDefinitionStore.js';
6
+ export { LocalInviteStore } from './LocalInviteStore.js';
7
+ export { LocalComputeServerStore } from './LocalComputeServerStore.js';
8
+ export type {
9
+ SecretVerificationFailure,
10
+ SecretVerificationFailureReason,
11
+ SecretVerificationReport
12
+ } from './LocalComputeServerStore.js';
13
+ export { LocalShareLinkStore } from './LocalShareLinkStore.js';
14
+ export type { LocalShareLinkStoreOptions } from './LocalShareLinkStore.js';
15
+ export { LocalPlatformProjectGrantStore } from './LocalPlatformProjectGrantStore.js';
16
+ export { LocalDataProvider } from './LocalDataProvider.js';
@@ -0,0 +1,48 @@
1
+ import {
2
+ DEFAULT_PAGE_LIMIT,
3
+ MAX_PAGE_LIMIT,
4
+ type ListOptions,
5
+ type DefinitionListOptions,
6
+ type Page
7
+ } from '@selvajs/platform';
8
+
9
+ type AnyListOptions = ListOptions | DefinitionListOptions;
10
+
11
+ /**
12
+ * In-memory pagination for filesystem-backed adapters.
13
+ * Cursor is an offset encoded as a string. Good enough for single-node local use.
14
+ */
15
+ export function paginate<T>(items: T[], opts?: AnyListOptions): Page<T> {
16
+ const limit = Math.min(Math.max(1, opts?.limit ?? DEFAULT_PAGE_LIMIT), MAX_PAGE_LIMIT);
17
+ const offset = opts?.cursor ? parseInt(opts.cursor, 10) || 0 : 0;
18
+ const slice = items.slice(offset, offset + limit);
19
+ const nextOffset = offset + slice.length;
20
+ return {
21
+ items: slice,
22
+ nextCursor: nextOffset < items.length ? String(nextOffset) : undefined
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Sort a list in place per ListOptions. Mutates the input.
28
+ * Pass `keyFn` to customize how the comparison value is derived (e.g. nested
29
+ * fields, case-folded strings).
30
+ */
31
+ export function applyOrder<T>(
32
+ items: T[],
33
+ opts?: AnyListOptions,
34
+ keyFn: (item: T, field: string) => unknown = (item, field) =>
35
+ (item as Record<string, unknown>)[field]
36
+ ): T[] {
37
+ const field = opts?.orderBy ?? 'createdAt';
38
+ const dir = opts?.orderDir ?? 'desc';
39
+ const mul = dir === 'asc' ? 1 : -1;
40
+ items.sort((a, b) => {
41
+ const av = (keyFn(a, field) ?? '') as string | number;
42
+ const bv = (keyFn(b, field) ?? '') as string | number;
43
+ if (av < bv) return -1 * mul;
44
+ if (av > bv) return 1 * mul;
45
+ return 0;
46
+ });
47
+ return items;
48
+ }
@@ -0,0 +1,69 @@
1
+ import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
2
+
3
+ const ALGO = 'aes-256-gcm';
4
+ const IV_BYTES = 12;
5
+ const TAG_BYTES = 16;
6
+ const PREFIX = 'enc:v1:';
7
+
8
+ /**
9
+ * AES-256-GCM envelope for at-rest secrets (e.g. compute API keys).
10
+ *
11
+ * On-disk format: `enc:v1:<base64(iv|tag|ciphertext)>`. The version prefix
12
+ * lets us migrate to a new algorithm later without ambiguity. Plaintext
13
+ * never sits on disk.
14
+ *
15
+ * Security model: defends against backup leaks, accidental file sharing,
16
+ * and read-only disk access. An attacker with both the data file *and*
17
+ * the master key (env var, process memory) can still decrypt — this is
18
+ * encryption at rest, not a secret manager.
19
+ */
20
+
21
+ export function isEncryptedSecret(value: string): boolean {
22
+ return value.startsWith(PREFIX);
23
+ }
24
+
25
+ export function encryptSecret(plaintext: string, key: Buffer): string {
26
+ if (key.length !== 32) {
27
+ throw new Error(`Secret key must be 32 bytes; got ${key.length}`);
28
+ }
29
+ if (isEncryptedSecret(plaintext)) {
30
+ throw new Error('Refusing to encrypt a value that is already encrypted');
31
+ }
32
+ const iv = randomBytes(IV_BYTES);
33
+ const cipher = createCipheriv(ALGO, key, iv);
34
+ const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
35
+ const tag = cipher.getAuthTag();
36
+ return PREFIX + Buffer.concat([iv, tag, ciphertext]).toString('base64');
37
+ }
38
+
39
+ export function decryptSecret(envelope: string, key: Buffer): string {
40
+ if (key.length !== 32) {
41
+ throw new Error(`Secret key must be 32 bytes; got ${key.length}`);
42
+ }
43
+ if (!isEncryptedSecret(envelope)) {
44
+ throw new Error('Value is not an encrypted secret envelope');
45
+ }
46
+ const buf = Buffer.from(envelope.slice(PREFIX.length), 'base64');
47
+ const iv = buf.subarray(0, IV_BYTES);
48
+ const tag = buf.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
49
+ const ciphertext = buf.subarray(IV_BYTES + TAG_BYTES);
50
+ const decipher = createDecipheriv(ALGO, key, iv);
51
+ decipher.setAuthTag(tag);
52
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
53
+ }
54
+
55
+ /**
56
+ * Decode a `SELVA_AT_REST_KEY` env var into a 32-byte buffer. Accepts either
57
+ * 64-char hex or base64 (with or without padding). Throws on anything else
58
+ * so misconfiguration fails loudly at boot rather than silently producing
59
+ * an undecryptable file.
60
+ */
61
+ export function decodeSecretKey(raw: string): Buffer {
62
+ if (/^[0-9a-fA-F]{64}$/.test(raw)) return Buffer.from(raw, 'hex');
63
+ const buf = Buffer.from(raw, 'base64');
64
+ if (buf.length === 32) return buf;
65
+ throw new Error(
66
+ 'SELVA_AT_REST_KEY must be 32 bytes encoded as 64-char hex or base64. ' +
67
+ "Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
68
+ );
69
+ }
@@ -0,0 +1,134 @@
1
+ import { ProviderError } from '@selvajs/platform';
2
+ import type { PlatformPermission, RecentRun } from '@selvajs/platform';
3
+ import { readJsonFile, writeJsonFile } from './fsJson.js';
4
+
5
+ /**
6
+ * Per-user state owned by the data layer, keyed by the auth provider's user
7
+ * ID. Mirrors `public.user_profiles` in the Supabase schema. **Never holds
8
+ * identity** (email, password hash, createdAt) — those live with the auth
9
+ * provider, which may be anything from `LocalAuthProvider` to an external
10
+ * IdP like Eterna ID.
11
+ *
12
+ * The local equivalent of Supabase's `handle_new_auth_user` trigger lives in
13
+ * `hooks.server.ts`, which calls `IDataProvider.ensureUser` once per
14
+ * authenticated request. After that call, every read here is guaranteed to
15
+ * find a row.
16
+ */
17
+ export interface StoredUserData {
18
+ userId: string;
19
+ displayName?: string;
20
+ platformPermissions: PlatformPermission[];
21
+ starredDefinitions: string[];
22
+ recentRuns: RecentRun[];
23
+ }
24
+
25
+ export interface UserDataFile {
26
+ users: StoredUserData[];
27
+ }
28
+
29
+ const MAX_RECENT_RUNS = 20;
30
+
31
+ const empty = (): UserDataFile => ({ users: [] });
32
+
33
+ function emptyRow(userId: string): StoredUserData {
34
+ return {
35
+ userId,
36
+ platformPermissions: [],
37
+ starredDefinitions: [],
38
+ recentRuns: []
39
+ };
40
+ }
41
+
42
+ export interface LocalUserDataStore {
43
+ /**
44
+ * Idempotent. Adds an empty row if missing; no-op if present. Safe to call
45
+ * on every authed request.
46
+ */
47
+ ensure(userId: string): Promise<void>;
48
+ findById(userId: string): Promise<StoredUserData | null>;
49
+ listAll(): Promise<StoredUserData[]>;
50
+ updatePermissions(userId: string, permissions: PlatformPermission[]): Promise<void>;
51
+ updateDisplayName(userId: string, displayName: string | undefined): Promise<void>;
52
+ starDefinition(userId: string, definitionId: string): Promise<void>;
53
+ unstarDefinition(userId: string, definitionId: string): Promise<void>;
54
+ recordRun(userId: string, run: RecentRun): Promise<void>;
55
+ deleteUser(userId: string): Promise<void>;
56
+ }
57
+
58
+ export function createLocalUserDataStore(filePath: string): LocalUserDataStore {
59
+ async function findOrThrow(userId: string): Promise<{ file: UserDataFile; row: StoredUserData }> {
60
+ const file = await readJsonFile<UserDataFile>(filePath, empty());
61
+ const row = file.users.find((u) => u.userId === userId);
62
+ if (!row) throw new ProviderError(`User-data row "${userId}" not found`, 404);
63
+ return { file, row };
64
+ }
65
+
66
+ return {
67
+ async ensure(userId) {
68
+ const file = await readJsonFile<UserDataFile>(filePath, empty());
69
+ if (file.users.some((u) => u.userId === userId)) return;
70
+ file.users.push(emptyRow(userId));
71
+ await writeJsonFile(filePath, file);
72
+ },
73
+
74
+ async findById(userId) {
75
+ const { users } = await readJsonFile<UserDataFile>(filePath, empty());
76
+ return users.find((u) => u.userId === userId) ?? null;
77
+ },
78
+
79
+ async listAll() {
80
+ const { users } = await readJsonFile<UserDataFile>(filePath, empty());
81
+ return users;
82
+ },
83
+
84
+ async updatePermissions(userId, permissions) {
85
+ const { file, row } = await findOrThrow(userId);
86
+ row.platformPermissions = permissions;
87
+ await writeJsonFile(filePath, file);
88
+ },
89
+
90
+ async updateDisplayName(userId, displayName) {
91
+ const { file, row } = await findOrThrow(userId);
92
+ if (displayName === undefined) {
93
+ delete row.displayName;
94
+ } else {
95
+ row.displayName = displayName;
96
+ }
97
+ await writeJsonFile(filePath, file);
98
+ },
99
+
100
+ async starDefinition(userId, definitionId) {
101
+ const { file, row } = await findOrThrow(userId);
102
+ if (!row.starredDefinitions.includes(definitionId)) {
103
+ row.starredDefinitions.push(definitionId);
104
+ await writeJsonFile(filePath, file);
105
+ }
106
+ },
107
+
108
+ async unstarDefinition(userId, definitionId) {
109
+ const { file, row } = await findOrThrow(userId);
110
+ row.starredDefinitions = row.starredDefinitions.filter((d) => d !== definitionId);
111
+ await writeJsonFile(filePath, file);
112
+ },
113
+
114
+ async recordRun(userId, run) {
115
+ const { file, row } = await findOrThrow(userId);
116
+ // Remove older entry for same definition, then prepend newest.
117
+ row.recentRuns = [
118
+ run,
119
+ ...row.recentRuns.filter((r) => r.definitionId !== run.definitionId)
120
+ ].slice(0, MAX_RECENT_RUNS);
121
+ await writeJsonFile(filePath, file);
122
+ },
123
+
124
+ async deleteUser(userId) {
125
+ const file = await readJsonFile<UserDataFile>(filePath, empty());
126
+ const before = file.users.length;
127
+ file.users = file.users.filter((u) => u.userId !== userId);
128
+ if (file.users.length === before) {
129
+ throw new ProviderError(`User-data row "${userId}" not found`, 404);
130
+ }
131
+ await writeJsonFile(filePath, file);
132
+ }
133
+ };
134
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ // Auth — identity-only (auth-users.json)
2
+ export { LocalAuthProvider } from './auth/LocalAuthProvider.js';
3
+ export type { LocalAuthProviderConfig } from './auth/LocalAuthProvider.js';
4
+ export { signHmacToken, verifyHmacToken } from './auth/hmac.js';
5
+ export { hashPassword, verifyPasswordHash, createLocalAuthUserStore } from './auth/users.js';
6
+ export type { StoredAuthUser, AuthUsersFile, LocalAuthUserStore } from './auth/users.js';
7
+
8
+ // Data — class names use the `*Store` suffix to match the Supabase provider.
9
+ export {
10
+ LocalDataProvider,
11
+ LocalOrgStore,
12
+ LocalOrgStoreLoader,
13
+ LocalProjectStore,
14
+ LocalDefinitionStore,
15
+ LocalInviteStore,
16
+ LocalComputeServerStore,
17
+ LocalShareLinkStore,
18
+ LocalPlatformProjectGrantStore
19
+ } from './data/index.js';
20
+ export type {
21
+ LocalOrgStoreData,
22
+ LocalOrgStoreOptions,
23
+ LocalProjectStoreOptions,
24
+ LocalShareLinkStoreOptions,
25
+ SecretVerificationFailure,
26
+ SecretVerificationFailureReason,
27
+ SecretVerificationReport
28
+ } from './data/index.js';
29
+
30
+ // Per-user data layer (user-data.json) — keyed by user ID, paired with any
31
+ // auth provider. The permission and profile stores both read/write here.
32
+ export { createLocalUserDataStore } from './data/userData.js';
33
+ export type { LocalUserDataStore, StoredUserData, UserDataFile } from './data/userData.js';
34
+
35
+ // Storage
36
+ export { LocalStorageProvider } from './storage/LocalStorageProvider.js';
37
+
38
+ // User profile
39
+ export { LocalUserProfileProvider } from './userProfile/LocalUserProfileProvider.js';
40
+
41
+ // Platform permissions (data-layer authorization store)
42
+ export { LocalPlatformPermissionStore } from './permissions/LocalPlatformPermissionStore.js';
@@ -0,0 +1,129 @@
1
+ import * as path from 'node:path';
2
+ import type {
3
+ IPlatformPermissionStore,
4
+ PlatformPermission,
5
+ RequestContext,
6
+ UserManagementResult
7
+ } from '@selvajs/platform';
8
+ import { ProviderError, hasPermission } from '@selvajs/platform';
9
+ import { createLocalUserDataStore, type LocalUserDataStore } from '../data/userData.js';
10
+
11
+ /**
12
+ * Filesystem-backed platform-permission store. Reads and writes the
13
+ * `platformPermissions` field on `user-data.json` — a data-layer file owned
14
+ * by `LocalDataProvider`, distinct from `auth-users.json` which the
15
+ * `LocalAuthProvider` owns. This split lets the local data layer pair with
16
+ * any auth provider (local, Supabase, Entra, Eterna, …): the data layer only
17
+ * cares about the user ID, never about how identity is stored.
18
+ *
19
+ * The "user is known to the data layer" precondition is established by
20
+ * `IDataProvider.ensureUser`, called from `hooks.server.ts` on every authed
21
+ * request — the local equivalent of the Supabase `handle_new_auth_user`
22
+ * trigger. After that call, `set` finds a row and `getFor` reads it; before
23
+ * it, `set` returns `not_found` (matching the conformance contract).
24
+ *
25
+ * Enforces:
26
+ * - The §2 sole-`instance_admin` invariant on `set` (refuses to drop the
27
+ * last admin)
28
+ * - Authorization on read/write (`assertCanRead` / `assertCanWrite`)
29
+ *
30
+ * "Disabled" state lives on the auth provider's user record. The local
31
+ * provider's permission store can't see across that boundary, so the
32
+ * invariant counters here treat every row in `user-data.json` as enabled.
33
+ * Code that disables a user is expected to also drop their `instance_admin`
34
+ * grant via `set` (the §2 invariant kicks in there) before the auth-side
35
+ * disable lands — see `LocalAuthProvider.disableUser`.
36
+ */
37
+ export class LocalPlatformPermissionStore implements IPlatformPermissionStore {
38
+ private readonly data: LocalUserDataStore;
39
+
40
+ static fromEnv(env: Record<string, string | undefined>): LocalPlatformPermissionStore {
41
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
42
+ return new LocalPlatformPermissionStore(path.join(env.DATA_PATH, 'user-data.json'));
43
+ }
44
+
45
+ constructor(userDataFilePath: string) {
46
+ this.data = createLocalUserDataStore(userDataFilePath);
47
+ }
48
+
49
+ async getFor(ctx: RequestContext, userId: string): Promise<PlatformPermission[]> {
50
+ assertCanRead(ctx, userId);
51
+ const row = await this.data.findById(userId);
52
+ return row?.platformPermissions ?? [];
53
+ }
54
+
55
+ async getForBatch(
56
+ ctx: RequestContext,
57
+ userIds: readonly string[]
58
+ ): Promise<Map<string, PlatformPermission[]>> {
59
+ assertCanReadBatch(ctx);
60
+ const all = await this.data.listAll();
61
+ const wanted = new Set(userIds);
62
+ const out = new Map<string, PlatformPermission[]>();
63
+ for (const u of all) {
64
+ if (wanted.has(u.userId)) out.set(u.userId, u.platformPermissions ?? []);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ async set(
70
+ ctx: RequestContext,
71
+ userId: string,
72
+ permissions: readonly PlatformPermission[]
73
+ ): Promise<UserManagementResult> {
74
+ assertAdmin(ctx);
75
+ const target = await this.data.findById(userId);
76
+ if (!target) return 'not_found';
77
+ const wasAdmin = target.platformPermissions.includes('instance_admin');
78
+ const willBeAdmin = permissions.includes('instance_admin');
79
+ if (wasAdmin && !willBeAdmin) {
80
+ const others = await this.countOtherAdmins(userId);
81
+ if (others === 0) return 'last_admin';
82
+ }
83
+ try {
84
+ await this.data.updatePermissions(userId, [...permissions]);
85
+ return 'ok';
86
+ } catch (err) {
87
+ if (err instanceof ProviderError && err.statusCode === 404) return 'not_found';
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ async hasInstanceAdmin(_ctx: RequestContext): Promise<boolean> {
93
+ // First-run + invariant check — always allowed (read-only existence
94
+ // probe). The route layer decides what to do with the answer.
95
+ const all = await this.data.listAll();
96
+ return all.some((u) => u.platformPermissions.includes('instance_admin'));
97
+ }
98
+
99
+ async countInstanceAdminsExcluding(_ctx: RequestContext, excludeUserId: string): Promise<number> {
100
+ return this.countOtherAdmins(excludeUserId);
101
+ }
102
+
103
+ private async countOtherAdmins(excludeUserId: string): Promise<number> {
104
+ const all = await this.data.listAll();
105
+ return all.filter(
106
+ (u) => u.userId !== excludeUserId && u.platformPermissions.includes('instance_admin')
107
+ ).length;
108
+ }
109
+ }
110
+
111
+ function assertCanRead(ctx: RequestContext, userId: string): void {
112
+ if (ctx.system) return;
113
+ if (ctx.userId === userId) return;
114
+ if (hasPermission(ctx, 'instance_admin')) return;
115
+ throw new ProviderError('Forbidden: cannot read another user’s permissions', 403);
116
+ }
117
+
118
+ function assertAdmin(ctx: RequestContext): void {
119
+ if (ctx.system) return;
120
+ if (hasPermission(ctx, 'instance_admin')) return;
121
+ throw new ProviderError('Forbidden: instance admin required', 403);
122
+ }
123
+
124
+ function assertCanReadBatch(ctx: RequestContext): void {
125
+ if (ctx.system) return;
126
+ if (hasPermission(ctx, 'instance_admin')) return;
127
+ if (hasPermission(ctx, 'manage_instance_users')) return;
128
+ throw new ProviderError('Forbidden: instance admin or manage_instance_users required', 403);
129
+ }
@@ -0,0 +1,40 @@
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 { runPlatformPermissionStoreConformance } from '@selvajs/platform/testing';
6
+ import { LocalPlatformPermissionStore } from '../LocalPlatformPermissionStore.js';
7
+ import { createLocalUserDataStore } from '../../data/userData.js';
8
+ import { randomUUID } from 'node:crypto';
9
+
10
+ describe('LocalPlatformPermissionStore', () => {
11
+ let tempDir: string;
12
+
13
+ beforeEach(async () => {
14
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-perm-test-'));
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await fs.rm(tempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ runPlatformPermissionStoreConformance({
22
+ name: 'LocalPlatformPermissionStore',
23
+ createStore: async () => {
24
+ const userDataPath = path.join(tempDir, 'user-data.json');
25
+ const userData = createLocalUserDataStore(userDataPath);
26
+ const store = new LocalPlatformPermissionStore(userDataPath);
27
+ return {
28
+ store,
29
+ seedUser: async () => {
30
+ // Mirrors `IDataProvider.ensureUser` — produces a user the data
31
+ // layer knows about, regardless of which auth provider would
32
+ // hand out the ID in production.
33
+ const id = randomUUID();
34
+ await userData.ensure(id);
35
+ return id;
36
+ }
37
+ };
38
+ }
39
+ });
40
+ });
@@ -0,0 +1 @@
1
+ export { LocalPlatformPermissionStore } from './LocalPlatformPermissionStore.js';
@@ -0,0 +1,78 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { transcodeImageIfNeeded } from '@selvajs/platform/storage';
4
+ import type { IStorageProvider } from '@selvajs/platform/storage';
5
+
6
+ /**
7
+ * Filesystem implementation of IStorageProvider.
8
+ * Files are stored under DATA_PATH/{path}.
9
+ * Images are auto-transcoded to WebP on put() via the shared platform helper
10
+ * (`transcodeImageIfNeeded`) — the same helper the Supabase adapter uses,
11
+ * so both providers produce identical bytes for the same upload.
12
+ */
13
+ export class LocalStorageProvider implements IStorageProvider {
14
+ private readonly basePath: string;
15
+ private readonly publicUrlBase: string;
16
+
17
+ static fromEnv(env: Record<string, string | undefined>): LocalStorageProvider {
18
+ if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
19
+ return new LocalStorageProvider(env.DATA_PATH, '/api/files');
20
+ }
21
+
22
+ constructor(basePath: string, publicUrlBase = '/api/files') {
23
+ this.basePath = basePath;
24
+ this.publicUrlBase = publicUrlBase;
25
+ }
26
+
27
+ /**
28
+ * Resolve a caller-provided path under basePath, rejecting anything that
29
+ * would escape the root. Last line of defense against traversal — while
30
+ * platform-side helpers (definitionPaths) also assert safe keys, this
31
+ * adapter is reached by any IStorageProvider caller.
32
+ */
33
+ private resolvePath(storagePath: string): string {
34
+ const base = path.resolve(this.basePath);
35
+ const full = path.resolve(base, storagePath);
36
+ if (full !== base && !full.startsWith(base + path.sep)) {
37
+ throw new Error(`Path escapes base: ${storagePath}`);
38
+ }
39
+ return full;
40
+ }
41
+
42
+ async get(storagePath: string): Promise<Uint8Array | null> {
43
+ try {
44
+ const buffer = await fs.readFile(this.resolvePath(storagePath));
45
+ return new Uint8Array(buffer);
46
+ } catch (err) {
47
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
48
+ throw err;
49
+ }
50
+ }
51
+
52
+ async put(storagePath: string, data: Uint8Array, contentType?: string): Promise<void> {
53
+ // Normalize images through the shared transcoder — rewrites `.png` → `.webp`,
54
+ // caps dimensions, and re-encodes at the platform's canonical quality.
55
+ // Non-images pass through untouched.
56
+ const transcoded = await transcodeImageIfNeeded(data, contentType, storagePath);
57
+ const fullPath = this.resolvePath(transcoded.path);
58
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
59
+ await fs.writeFile(fullPath, Buffer.from(transcoded.data));
60
+ }
61
+
62
+ async delete(storagePath: string): Promise<void> {
63
+ try {
64
+ await fs.unlink(this.resolvePath(storagePath));
65
+ } catch (err) {
66
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
67
+ }
68
+ }
69
+
70
+ async deletePrefix(prefix: string): Promise<void> {
71
+ const fullPath = this.resolvePath(prefix);
72
+ await fs.rm(fullPath, { recursive: true, force: true });
73
+ }
74
+
75
+ getPublicUrl(storagePath: string): string {
76
+ return `${this.publicUrlBase}/${storagePath}`;
77
+ }
78
+ }
@@ -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 { runStorageProviderConformance } from '@selvajs/platform/testing';
6
+ import { LocalStorageProvider } from '../LocalStorageProvider.js';
7
+
8
+ describe('LocalStorageProvider', () => {
9
+ let tempDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-storage-test-'));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tempDir, { recursive: true, force: true });
17
+ });
18
+
19
+ runStorageProviderConformance({
20
+ name: 'LocalStorageProvider',
21
+ createStorage: () => new LocalStorageProvider(tempDir)
22
+ });
23
+ });
@@ -0,0 +1 @@
1
+ export { LocalStorageProvider } from './LocalStorageProvider.js';