@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,682 @@
1
+ /**
2
+ * Pure-rule tests for @selvajs/platform/access — exercised directly with
3
+ * constructed inputs. Lives here because the platform package doesn't run
4
+ * its own vitest.
5
+ */
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ canView,
9
+ canSolve,
10
+ canEdit,
11
+ canManage,
12
+ canEditProjectSettings,
13
+ canEditDefinition,
14
+ canChangeVisibilityToPublic,
15
+ checkOwnerRemoval,
16
+ withAdminBypass,
17
+ type DefinitionAccessInput,
18
+ type OrgMember,
19
+ type Project,
20
+ type ProjectAccessInput,
21
+ type ProjectMember
22
+ } from '@selvajs/platform';
23
+
24
+ // ============================================================================
25
+ // helpers
26
+ // ============================================================================
27
+ function project(overrides: Partial<Project> = {}): Project {
28
+ const now = new Date().toISOString();
29
+ return {
30
+ id: 'p-1',
31
+ orgId: 'o-1',
32
+ name: 'P',
33
+ slug: 'p',
34
+ visibility: 'private',
35
+ ownerId: 'u-1',
36
+ createdBy: 'u-1',
37
+ updatedBy: 'u-1',
38
+ autoJoinOnUpload: false,
39
+ createdAt: now,
40
+ updatedAt: now,
41
+ deletedAt: null,
42
+ ...overrides
43
+ };
44
+ }
45
+
46
+ function member(role: ProjectMember['role'], userId = 'u-2'): ProjectMember {
47
+ const now = new Date().toISOString();
48
+ return {
49
+ projectId: 'p-1',
50
+ userId,
51
+ role,
52
+ joinedAt: now,
53
+ updatedAt: now,
54
+ updatedBy: userId,
55
+ deletedAt: null
56
+ };
57
+ }
58
+
59
+ function orgMember(role: OrgMember['role'], userId = 'u-2'): OrgMember {
60
+ const now = new Date().toISOString();
61
+ return {
62
+ orgId: 'o-1',
63
+ userId,
64
+ role,
65
+ permissions: [],
66
+ joinedAt: now,
67
+ updatedAt: now,
68
+ updatedBy: userId,
69
+ deletedAt: null
70
+ };
71
+ }
72
+
73
+ /** Default non-platform fields for ProjectAccessInput. */
74
+ function accessInput(
75
+ overrides: Partial<ProjectAccessInput> & Pick<ProjectAccessInput, 'project'>
76
+ ): ProjectAccessInput {
77
+ return {
78
+ orgPermissions: [],
79
+ platformPermissions: [],
80
+ member: null,
81
+ orgMember: null,
82
+ allowCrossOrgPublic: false,
83
+ enablePlatformProjects: true,
84
+ platformGrants: [],
85
+ actingOrgId: 'o-1',
86
+ userId: 'u-2',
87
+ ...overrides
88
+ };
89
+ }
90
+
91
+ function def(
92
+ overrides: Partial<import('@selvajs/platform').DefinitionRecord> = {}
93
+ ): import('@selvajs/platform').DefinitionRecord {
94
+ const now = new Date().toISOString();
95
+ return {
96
+ guid: 'd-1',
97
+ projectId: 'p-1',
98
+ ownerId: 'u-alice',
99
+ createdBy: 'u-alice',
100
+ updatedBy: 'u-alice',
101
+ displayName: 'D',
102
+ status: 'published',
103
+ solveCount: 0,
104
+ liveVersionId: null,
105
+ draftVersionId: null,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ deletedAt: null,
109
+ ...overrides
110
+ };
111
+ }
112
+
113
+ /** Default non-platform fields for DefinitionAccessInput. */
114
+ function defAccessInput(
115
+ overrides: Partial<DefinitionAccessInput> &
116
+ Pick<DefinitionAccessInput, 'project' | 'definition' | 'userId'>
117
+ ): DefinitionAccessInput {
118
+ return {
119
+ member: null,
120
+ platformPermissions: [],
121
+ enablePlatformProjects: true,
122
+ ...overrides
123
+ };
124
+ }
125
+
126
+ // ============================================================================
127
+ // canView
128
+ // ============================================================================
129
+ describe('canView', () => {
130
+ it('private: returns false without a project member', () => {
131
+ expect(
132
+ canView(
133
+ accessInput({
134
+ project: project({ visibility: 'private' }),
135
+ allowCrossOrgPublic: true
136
+ })
137
+ )
138
+ ).toBe(false);
139
+ });
140
+
141
+ it('private: viewer role is sufficient', () => {
142
+ expect(
143
+ canView(
144
+ accessInput({
145
+ project: project({ visibility: 'private' }),
146
+ member: member('viewer'),
147
+ allowCrossOrgPublic: true
148
+ })
149
+ )
150
+ ).toBe(true);
151
+ });
152
+
153
+ it('org: org member passes, non-member fails', () => {
154
+ const pub = project({ visibility: 'org' });
155
+ expect(
156
+ canView(
157
+ accessInput({ project: pub, orgMember: orgMember('member'), allowCrossOrgPublic: true })
158
+ )
159
+ ).toBe(true);
160
+ expect(canView(accessInput({ project: pub, allowCrossOrgPublic: true }))).toBe(false);
161
+ });
162
+
163
+ it('public + cross-org flag on: any authenticated user passes', () => {
164
+ expect(
165
+ canView(
166
+ accessInput({
167
+ project: project({ visibility: 'public' }),
168
+ allowCrossOrgPublic: true
169
+ })
170
+ )
171
+ ).toBe(true);
172
+ });
173
+
174
+ it('public + cross-org flag off: requires org membership (within-org public)', () => {
175
+ const pub = project({ visibility: 'public' });
176
+ expect(
177
+ canView(
178
+ accessInput({ project: pub, orgMember: orgMember('member'), allowCrossOrgPublic: false })
179
+ )
180
+ ).toBe(true);
181
+ expect(canView(accessInput({ project: pub, allowCrossOrgPublic: false }))).toBe(false);
182
+ });
183
+
184
+ it('null project denies', () => {
185
+ expect(
186
+ canView(accessInput({ project: null as unknown as Project, allowCrossOrgPublic: true }))
187
+ ).toBe(false);
188
+ });
189
+
190
+ // `private` means private from everyone without project membership —
191
+ // including org leadership. Reclaim is the explicit escalation path.
192
+ it('private + org owner (no project membership): still denied', () => {
193
+ expect(
194
+ canView(
195
+ accessInput({
196
+ project: project({ visibility: 'private' }),
197
+ orgMember: orgMember('owner')
198
+ })
199
+ )
200
+ ).toBe(false);
201
+ });
202
+
203
+ it('private + org admin (no project membership): still denied', () => {
204
+ expect(
205
+ canView(
206
+ accessInput({
207
+ project: project({ visibility: 'private' }),
208
+ orgMember: orgMember('admin')
209
+ })
210
+ )
211
+ ).toBe(false);
212
+ });
213
+
214
+ it('private + org member (no project membership): denied', () => {
215
+ expect(
216
+ canView(
217
+ accessInput({
218
+ project: project({ visibility: 'private' }),
219
+ orgMember: orgMember('member')
220
+ })
221
+ )
222
+ ).toBe(false);
223
+ });
224
+
225
+ it('platform: instance_admin passes', () => {
226
+ expect(
227
+ canView(
228
+ accessInput({
229
+ project: project({ visibility: 'platform' }),
230
+ platformPermissions: ['instance_admin']
231
+ })
232
+ )
233
+ ).toBe(true);
234
+ });
235
+
236
+ it('platform: user grant passes regardless of canSolve value', () => {
237
+ expect(
238
+ canView(
239
+ accessInput({
240
+ project: project({ visibility: 'platform' }),
241
+ userId: 'u-grantee',
242
+ platformGrants: [
243
+ {
244
+ id: 'g-1',
245
+ projectId: 'p-1',
246
+ granteeType: 'user',
247
+ granteeId: 'u-grantee',
248
+ canSolve: false,
249
+ createdBy: 'u-admin',
250
+ createdAt: new Date().toISOString()
251
+ }
252
+ ]
253
+ })
254
+ )
255
+ ).toBe(true);
256
+ });
257
+
258
+ it('platform: org grant passes for acting org', () => {
259
+ expect(
260
+ canView(
261
+ accessInput({
262
+ project: project({ visibility: 'platform' }),
263
+ actingOrgId: 'o-grantee',
264
+ platformGrants: [
265
+ {
266
+ id: 'g-1',
267
+ projectId: 'p-1',
268
+ granteeType: 'org',
269
+ granteeId: 'o-grantee',
270
+ canSolve: false,
271
+ createdBy: 'u-admin',
272
+ createdAt: new Date().toISOString()
273
+ }
274
+ ]
275
+ })
276
+ )
277
+ ).toBe(true);
278
+ });
279
+
280
+ it('platform: no grant, no admin → denied', () => {
281
+ expect(canView(accessInput({ project: project({ visibility: 'platform' }) }))).toBe(false);
282
+ });
283
+ });
284
+
285
+ // ============================================================================
286
+ // canSolve
287
+ // ============================================================================
288
+ describe('canSolve', () => {
289
+ it('matches canView for non-platform visibility', () => {
290
+ for (const v of ['public', 'org', 'private'] as const) {
291
+ const input = accessInput({
292
+ project: project({ visibility: v }),
293
+ member: v === 'private' ? member('viewer') : null,
294
+ orgMember: v === 'org' ? orgMember('member') : null,
295
+ allowCrossOrgPublic: true
296
+ });
297
+ expect(canSolve(input)).toBe(canView(input));
298
+ }
299
+ });
300
+
301
+ it('platform: instance_admin passes', () => {
302
+ expect(
303
+ canSolve(
304
+ accessInput({
305
+ project: project({ visibility: 'platform' }),
306
+ platformPermissions: ['instance_admin']
307
+ })
308
+ )
309
+ ).toBe(true);
310
+ });
311
+
312
+ it('platform: canSolve=true grant passes', () => {
313
+ expect(
314
+ canSolve(
315
+ accessInput({
316
+ project: project({ visibility: 'platform' }),
317
+ userId: 'u-grantee',
318
+ platformGrants: [
319
+ {
320
+ id: 'g-1',
321
+ projectId: 'p-1',
322
+ granteeType: 'user',
323
+ granteeId: 'u-grantee',
324
+ canSolve: true,
325
+ createdBy: 'u-admin',
326
+ createdAt: new Date().toISOString()
327
+ }
328
+ ]
329
+ })
330
+ )
331
+ ).toBe(true);
332
+ });
333
+
334
+ it('platform: view-only grant (canSolve=false) is denied for solve', () => {
335
+ expect(
336
+ canSolve(
337
+ accessInput({
338
+ project: project({ visibility: 'platform' }),
339
+ userId: 'u-grantee',
340
+ platformGrants: [
341
+ {
342
+ id: 'g-1',
343
+ projectId: 'p-1',
344
+ granteeType: 'user',
345
+ granteeId: 'u-grantee',
346
+ canSolve: false,
347
+ createdBy: 'u-admin',
348
+ createdAt: new Date().toISOString()
349
+ }
350
+ ]
351
+ })
352
+ )
353
+ ).toBe(false);
354
+ });
355
+ });
356
+
357
+ // ============================================================================
358
+ // canEdit / canManage / canEditProjectSettings
359
+ // ============================================================================
360
+ describe('canEdit', () => {
361
+ it('project owner/editor yes; viewer no', () => {
362
+ const base = accessInput({ project: project(), allowCrossOrgPublic: true });
363
+ expect(canEdit({ ...base, member: member('owner') })).toBe(true);
364
+ expect(canEdit({ ...base, member: member('editor') })).toBe(true);
365
+ expect(canEdit({ ...base, member: member('viewer') })).toBe(false);
366
+ expect(canEdit({ ...base, member: null })).toBe(false);
367
+ });
368
+
369
+ it('manage_definitions org-perm does not grant edit', () => {
370
+ expect(
371
+ canEdit(
372
+ accessInput({
373
+ orgPermissions: ['manage_definitions'],
374
+ project: project({ visibility: 'public' }),
375
+ orgMember: orgMember('member'),
376
+ allowCrossOrgPublic: true
377
+ })
378
+ )
379
+ ).toBe(false);
380
+ });
381
+
382
+ it('platform project: instance_admin passes; project member role is ignored', () => {
383
+ const platformProject = project({ visibility: 'platform' });
384
+ expect(
385
+ canEdit(
386
+ accessInput({
387
+ project: platformProject,
388
+ platformPermissions: ['instance_admin']
389
+ })
390
+ )
391
+ ).toBe(true);
392
+ // A project member role on a platform project is meaningless — only admin grants edit.
393
+ expect(
394
+ canEdit(
395
+ accessInput({
396
+ project: platformProject,
397
+ member: member('owner')
398
+ })
399
+ )
400
+ ).toBe(false);
401
+ });
402
+ });
403
+
404
+ describe('canEditProjectSettings', () => {
405
+ it('project owner yes; editor no', () => {
406
+ const base = accessInput({ project: project(), allowCrossOrgPublic: true });
407
+ expect(canEditProjectSettings({ ...base, member: member('owner') })).toBe(true);
408
+ expect(
409
+ canEditProjectSettings({
410
+ ...base,
411
+ orgPermissions: ['manage_definitions', 'manage_projects'],
412
+ member: member('editor')
413
+ })
414
+ ).toBe(false);
415
+ });
416
+
417
+ it('platform project: instance_admin passes; non-admin denied', () => {
418
+ const platformProject = project({ visibility: 'platform' });
419
+ expect(
420
+ canEditProjectSettings(
421
+ accessInput({
422
+ project: platformProject,
423
+ platformPermissions: ['instance_admin']
424
+ })
425
+ )
426
+ ).toBe(true);
427
+ expect(
428
+ canEditProjectSettings(
429
+ accessInput({
430
+ project: platformProject,
431
+ member: member('owner')
432
+ })
433
+ )
434
+ ).toBe(false);
435
+ });
436
+ });
437
+
438
+ describe('canManage', () => {
439
+ it('only project owner passes', () => {
440
+ const base = accessInput({ project: project(), allowCrossOrgPublic: true });
441
+ expect(canManage({ ...base, member: member('owner') })).toBe(true);
442
+ expect(canManage({ ...base, member: member('editor') })).toBe(false);
443
+ expect(canManage({ ...base, member: null })).toBe(false);
444
+ });
445
+
446
+ it('platform project: instance_admin passes; non-admin denied', () => {
447
+ const platformProject = project({ visibility: 'platform' });
448
+ expect(
449
+ canManage(
450
+ accessInput({
451
+ project: platformProject,
452
+ platformPermissions: ['instance_admin']
453
+ })
454
+ )
455
+ ).toBe(true);
456
+ expect(
457
+ canManage(
458
+ accessInput({
459
+ project: platformProject,
460
+ member: member('owner')
461
+ })
462
+ )
463
+ ).toBe(false);
464
+ });
465
+ });
466
+
467
+ // ============================================================================
468
+ // canChangeVisibilityToPublic
469
+ // ============================================================================
470
+ describe('canChangeVisibilityToPublic', () => {
471
+ it('org owner/admin can flip', () => {
472
+ expect(canChangeVisibilityToPublic({ orgMember: orgMember('owner') })).toBe(true);
473
+ expect(canChangeVisibilityToPublic({ orgMember: orgMember('admin') })).toBe(true);
474
+ });
475
+
476
+ it('member cannot flip', () => {
477
+ expect(canChangeVisibilityToPublic({ orgMember: orgMember('member') })).toBe(false);
478
+ });
479
+
480
+ it('cross-org-public flag does NOT gate the flip — only canView post-flip', () => {
481
+ // Permissions.md §5 (canChangeVisibilityToPublic): with the flag off,
482
+ // public still flips; the meaning of public narrows to within-org. The
483
+ // flag belongs in canView, not here.
484
+ expect(canChangeVisibilityToPublic({ orgMember: orgMember('owner') })).toBe(true);
485
+ });
486
+ });
487
+
488
+ // ============================================================================
489
+ // canEditDefinition
490
+ // ============================================================================
491
+ describe('canEditDefinition', () => {
492
+ it('container project: only project editors/owners', () => {
493
+ const p = project({ visibility: 'public', autoJoinOnUpload: false });
494
+ expect(
495
+ canEditDefinition(
496
+ defAccessInput({
497
+ project: p,
498
+ definition: def({ ownerId: 'u-alice' }),
499
+ member: member('editor', 'u-editor'),
500
+ userId: 'u-editor'
501
+ })
502
+ )
503
+ ).toBe(true);
504
+ // Uploader who isn't a project member: denied on a container project.
505
+ expect(
506
+ canEditDefinition(
507
+ defAccessInput({
508
+ project: p,
509
+ definition: def({ ownerId: 'u-alice' }),
510
+ userId: 'u-alice'
511
+ })
512
+ )
513
+ ).toBe(false);
514
+ });
515
+
516
+ it('commons project: definition owner edits their own', () => {
517
+ const p = project({ visibility: 'public', autoJoinOnUpload: true });
518
+ expect(
519
+ canEditDefinition(
520
+ defAccessInput({
521
+ project: p,
522
+ definition: def({ ownerId: 'u-alice' }),
523
+ userId: 'u-alice'
524
+ })
525
+ )
526
+ ).toBe(true);
527
+ });
528
+
529
+ it('commons project: random user cannot edit someone else’s definition (Alice/Peter)', () => {
530
+ const p = project({ visibility: 'public', autoJoinOnUpload: true });
531
+ expect(
532
+ canEditDefinition(
533
+ defAccessInput({
534
+ project: p,
535
+ definition: def({ ownerId: 'u-alice' }),
536
+ userId: 'u-peter'
537
+ })
538
+ )
539
+ ).toBe(false);
540
+ });
541
+
542
+ it('commons project: project editor still moderates any definition', () => {
543
+ const p = project({ visibility: 'public', autoJoinOnUpload: true });
544
+ expect(
545
+ canEditDefinition(
546
+ defAccessInput({
547
+ project: p,
548
+ definition: def({ ownerId: 'u-alice' }),
549
+ member: member('editor', 'u-mod'),
550
+ userId: 'u-mod'
551
+ })
552
+ )
553
+ ).toBe(true);
554
+ });
555
+
556
+ it('platform project: instance_admin passes', () => {
557
+ const p = project({ visibility: 'platform' });
558
+ expect(
559
+ canEditDefinition(
560
+ defAccessInput({
561
+ project: p,
562
+ definition: def(),
563
+ userId: 'u-admin',
564
+ platformPermissions: ['instance_admin']
565
+ })
566
+ )
567
+ ).toBe(true);
568
+ });
569
+
570
+ it('platform project: grant holder cannot edit definitions', () => {
571
+ const p = project({ visibility: 'platform' });
572
+ expect(
573
+ canEditDefinition(
574
+ defAccessInput({
575
+ project: p,
576
+ definition: def(),
577
+ userId: 'u-grantee'
578
+ })
579
+ )
580
+ ).toBe(false);
581
+ });
582
+ });
583
+
584
+ // ============================================================================
585
+ // withAdminBypass wrapper
586
+ // ============================================================================
587
+ describe('withAdminBypass', () => {
588
+ it('short-circuits true for instance_admin', () => {
589
+ let invoked = false;
590
+ const result = withAdminBypass(['instance_admin'], () => {
591
+ invoked = true;
592
+ return false; // would normally deny
593
+ });
594
+ expect(result).toBe(true);
595
+ expect(invoked).toBe(false);
596
+ });
597
+
598
+ it('calls the rule for non-admin', () => {
599
+ let invoked = false;
600
+ const result = withAdminBypass([], () => {
601
+ invoked = true;
602
+ return true;
603
+ });
604
+ expect(result).toBe(true);
605
+ expect(invoked).toBe(true);
606
+ });
607
+ });
608
+
609
+ // ============================================================================
610
+ // checkOwnerRemoval (Permissions.md §5, §10)
611
+ // ============================================================================
612
+ describe('checkOwnerRemoval', () => {
613
+ it('returns ok for non-owner targets regardless of confirmation', () => {
614
+ expect(
615
+ checkOwnerRemoval({
616
+ target: { role: 'editor' },
617
+ allMembers: [{ role: 'owner' }],
618
+ confirmed: false
619
+ })
620
+ ).toBe('ok');
621
+ expect(
622
+ checkOwnerRemoval({
623
+ target: { role: 'viewer' },
624
+ allMembers: [{ role: 'owner' }],
625
+ confirmed: false
626
+ })
627
+ ).toBe('ok');
628
+ });
629
+
630
+ it('blocks sole-owner removal even with confirmation', () => {
631
+ expect(
632
+ checkOwnerRemoval({
633
+ target: { role: 'owner' },
634
+ allMembers: [{ role: 'owner' }, { role: 'editor' }],
635
+ confirmed: true
636
+ })
637
+ ).toBe('sole_owner');
638
+ });
639
+
640
+ it('owner-on-owner without confirmation needs_confirm', () => {
641
+ expect(
642
+ checkOwnerRemoval({
643
+ target: { role: 'owner' },
644
+ allMembers: [{ role: 'owner' }, { role: 'owner' }],
645
+ confirmed: false
646
+ })
647
+ ).toBe('needs_confirm');
648
+ });
649
+
650
+ it('owner-on-owner with confirmation succeeds', () => {
651
+ expect(
652
+ checkOwnerRemoval({
653
+ target: { role: 'owner' },
654
+ allMembers: [{ role: 'owner' }, { role: 'owner' }],
655
+ confirmed: true
656
+ })
657
+ ).toBe('ok');
658
+ });
659
+
660
+ it('three owners + confirmation succeeds (still leaves two)', () => {
661
+ expect(
662
+ checkOwnerRemoval({
663
+ target: { role: 'owner' },
664
+ allMembers: [{ role: 'owner' }, { role: 'owner' }, { role: 'owner' }],
665
+ confirmed: true
666
+ })
667
+ ).toBe('ok');
668
+ });
669
+
670
+ it('zero owners (corrupt state) reports sole_owner', () => {
671
+ // Defensive: if the caller somehow asks to remove an owner from a project
672
+ // with no owner rows in the page, treat it as sole_owner so we never let
673
+ // the project end up with zero owners.
674
+ expect(
675
+ checkOwnerRemoval({
676
+ target: { role: 'owner' },
677
+ allMembers: [{ role: 'editor' }],
678
+ confirmed: true
679
+ })
680
+ ).toBe('sole_owner');
681
+ });
682
+ });
@@ -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 { runShareLinkStoreConformance } from '@selvajs/platform/testing';
6
+ import { LocalShareLinkStore } from '../LocalShareLinkStore.js';
7
+
8
+ describe('LocalShareLinkStore', () => {
9
+ let tempDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'selva-share-'));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tempDir, { recursive: true, force: true });
17
+ });
18
+
19
+ runShareLinkStoreConformance({
20
+ name: 'LocalShareLinkStore',
21
+ createStore: () => new LocalShareLinkStore({ filePath: path.join(tempDir, 'share-links.json') })
22
+ });
23
+ });