@selvajs/platform 0.21.0-beta.3 → 1.0.0-beta.4

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 (362) hide show
  1. package/README.md +10 -7
  2. package/dist/access/index.d.ts +0 -1
  3. package/dist/access/index.js +0 -1
  4. package/dist/access/rules.d.ts +7 -14
  5. package/dist/access/rules.js +22 -26
  6. package/dist/apiTokens/index.d.ts +1 -2
  7. package/dist/apiTokens/index.js +1 -2
  8. package/dist/apiTokens/interface.d.ts +0 -1
  9. package/dist/apiTokens/interface.js +0 -1
  10. package/dist/apiTokens/scope.d.ts +0 -1
  11. package/dist/apiTokens/scope.js +0 -1
  12. package/dist/apiTokens/types.d.ts +0 -1
  13. package/dist/apiTokens/types.js +0 -1
  14. package/dist/auth/index.d.ts +0 -1
  15. package/dist/auth/index.js +0 -1
  16. package/dist/auth/interface.d.ts +4 -23
  17. package/dist/auth/interface.js +0 -1
  18. package/dist/auth/types.d.ts +4 -2
  19. package/dist/auth/types.js +0 -1
  20. package/dist/bindings/index.d.ts +0 -1
  21. package/dist/bindings/index.js +0 -1
  22. package/dist/bindings/interface.d.ts +0 -1
  23. package/dist/bindings/interface.js +0 -1
  24. package/dist/computeServer/index.d.ts +1 -7
  25. package/dist/computeServer/index.js +2 -8
  26. package/dist/computeServer/interface.d.ts +9 -6
  27. package/dist/computeServer/interface.js +0 -1
  28. package/dist/computeServer/public.d.ts +6 -0
  29. package/dist/computeServer/public.js +2 -0
  30. package/dist/computeServer/secrets.d.ts +12 -6
  31. package/dist/computeServer/secrets.js +35 -15
  32. package/dist/computeServer/types.d.ts +5 -2
  33. package/dist/computeServer/types.js +4 -1
  34. package/dist/computeServer/utils.d.ts +17 -6
  35. package/dist/computeServer/utils.js +31 -21
  36. package/dist/config.d.ts +1 -2
  37. package/dist/config.js +0 -1
  38. package/dist/context.d.ts +11 -1
  39. package/dist/context.js +51 -2
  40. package/dist/data/index.d.ts +0 -1
  41. package/dist/data/index.js +0 -1
  42. package/dist/data/interface.d.ts +0 -1
  43. package/dist/data/interface.js +0 -1
  44. package/dist/definitions/index.d.ts +2 -3
  45. package/dist/definitions/index.js +2 -3
  46. package/dist/definitions/interface.d.ts +0 -1
  47. package/dist/definitions/interface.js +0 -1
  48. package/dist/definitions/paths.d.ts +0 -1
  49. package/dist/definitions/paths.js +1 -12
  50. package/dist/definitions/schemas.d.ts +0 -21
  51. package/dist/definitions/schemas.js +6 -19
  52. package/dist/definitions/types.d.ts +3 -4
  53. package/dist/definitions/types.js +0 -3
  54. package/dist/errors/interface.d.ts +1 -2
  55. package/dist/errors/interface.js +0 -1
  56. package/dist/errors.d.ts +8 -1
  57. package/dist/errors.js +11 -1
  58. package/dist/events/audit.d.ts +1 -2
  59. package/dist/events/audit.js +0 -1
  60. package/dist/events/index.d.ts +0 -1
  61. package/dist/events/index.js +0 -1
  62. package/dist/events/interface.d.ts +0 -1
  63. package/dist/events/interface.js +0 -1
  64. package/dist/index.d.ts +18 -61
  65. package/dist/index.js +18 -24
  66. package/dist/invites/index.d.ts +0 -1
  67. package/dist/invites/index.js +0 -1
  68. package/dist/invites/interface.d.ts +5 -6
  69. package/dist/invites/interface.js +0 -1
  70. package/dist/invites/types.d.ts +0 -1
  71. package/dist/invites/types.js +0 -1
  72. package/dist/logging/interface.d.ts +0 -1
  73. package/dist/logging/interface.js +0 -1
  74. package/dist/metrics/interface.d.ts +0 -1
  75. package/dist/metrics/interface.js +0 -1
  76. package/dist/notifications/index.d.ts +0 -1
  77. package/dist/notifications/index.js +0 -1
  78. package/dist/notifications/interface.d.ts +1 -3
  79. package/dist/notifications/interface.js +1 -3
  80. package/dist/notifications/types.d.ts +0 -1
  81. package/dist/notifications/types.js +0 -1
  82. package/dist/organizations/index.d.ts +2 -3
  83. package/dist/organizations/index.js +1 -2
  84. package/dist/organizations/interface.d.ts +3 -5
  85. package/dist/organizations/interface.js +0 -1
  86. package/dist/organizations/paths.d.ts +1 -6
  87. package/dist/organizations/paths.js +2 -20
  88. package/dist/organizations/schemas.d.ts +5 -7
  89. package/dist/organizations/schemas.js +20 -13
  90. package/dist/organizations/types.d.ts +0 -1
  91. package/dist/organizations/types.js +0 -1
  92. package/dist/pagination.d.ts +9 -3
  93. package/dist/pagination.js +15 -1
  94. package/dist/permissions/index.d.ts +2 -2
  95. package/dist/permissions/index.js +2 -2
  96. package/dist/permissions/interface.d.ts +4 -4
  97. package/dist/permissions/interface.js +0 -1
  98. package/dist/permissions/types.d.ts +0 -1
  99. package/dist/permissions/types.js +0 -1
  100. package/dist/platformProjects/index.d.ts +1 -3
  101. package/dist/platformProjects/index.js +1 -2
  102. package/dist/platformProjects/interface.d.ts +0 -1
  103. package/dist/platformProjects/interface.js +0 -1
  104. package/dist/platformProjects/schemas.d.ts +0 -1
  105. package/dist/platformProjects/schemas.js +0 -1
  106. package/dist/platformProjects/types.d.ts +3 -2
  107. package/dist/platformProjects/types.js +0 -1
  108. package/dist/projects/index.d.ts +0 -1
  109. package/dist/projects/index.js +0 -1
  110. package/dist/projects/interface.d.ts +0 -1
  111. package/dist/projects/interface.js +0 -1
  112. package/dist/projects/schemas.d.ts +0 -1
  113. package/dist/projects/schemas.js +0 -1
  114. package/dist/projects/types.d.ts +0 -1
  115. package/dist/projects/types.js +0 -1
  116. package/dist/shareLinks/index.d.ts +0 -1
  117. package/dist/shareLinks/index.js +0 -1
  118. package/dist/shareLinks/interface.d.ts +6 -3
  119. package/dist/shareLinks/interface.js +0 -1
  120. package/dist/shareLinks/schemas.d.ts +0 -1
  121. package/dist/shareLinks/schemas.js +4 -2
  122. package/dist/shareLinks/types.d.ts +0 -1
  123. package/dist/shareLinks/types.js +0 -1
  124. package/dist/solveCache/interface.d.ts +2 -3
  125. package/dist/solveCache/interface.js +0 -1
  126. package/dist/storage/assetClasses.d.ts +4 -8
  127. package/dist/storage/assetClasses.js +6 -21
  128. package/dist/storage/cacheBust.d.ts +0 -1
  129. package/dist/storage/cacheBust.js +5 -3
  130. package/dist/storage/image.d.ts +8 -5
  131. package/dist/storage/image.js +74 -12
  132. package/dist/storage/index.d.ts +2 -4
  133. package/dist/storage/index.js +2 -3
  134. package/dist/storage/interface.d.ts +11 -3
  135. package/dist/storage/interface.js +0 -1
  136. package/dist/storage/safeKey.d.ts +7 -0
  137. package/dist/storage/safeKey.js +12 -0
  138. package/dist/testing/index.d.ts +5 -2
  139. package/dist/testing/index.js +3 -2
  140. package/dist/testing/suites/apiTokenStoreSuite.d.ts +0 -1
  141. package/dist/testing/suites/apiTokenStoreSuite.js +0 -1
  142. package/dist/testing/suites/auditQuerySuite.d.ts +18 -0
  143. package/dist/testing/suites/auditQuerySuite.js +73 -0
  144. package/dist/testing/suites/authProviderSuite.d.ts +14 -3
  145. package/dist/testing/suites/authProviderSuite.js +57 -32
  146. package/dist/testing/suites/computeServerStoreSuite.d.ts +1 -2
  147. package/dist/testing/suites/computeServerStoreSuite.js +68 -4
  148. package/dist/testing/suites/definitionStoreSuite.d.ts +2 -1
  149. package/dist/testing/suites/definitionStoreSuite.js +19 -16
  150. package/dist/testing/suites/emailLinkAuthSuite.d.ts +2 -5
  151. package/dist/testing/suites/emailLinkAuthSuite.js +1 -9
  152. package/dist/testing/suites/eventSinkSuite.d.ts +0 -1
  153. package/dist/testing/suites/eventSinkSuite.js +9 -1
  154. package/dist/testing/suites/helpers.d.ts +5 -1
  155. package/dist/testing/suites/helpers.js +7 -1
  156. package/dist/testing/suites/inviteStoreSuite.d.ts +0 -1
  157. package/dist/testing/suites/inviteStoreSuite.js +17 -7
  158. package/dist/testing/suites/oauthAuthSuite.d.ts +18 -0
  159. package/dist/testing/suites/oauthAuthSuite.js +26 -0
  160. package/dist/testing/suites/orgStoreSuite.d.ts +5 -1
  161. package/dist/testing/suites/orgStoreSuite.js +26 -7
  162. package/dist/testing/suites/platformPermissionStoreSuite.d.ts +0 -5
  163. package/dist/testing/suites/platformPermissionStoreSuite.js +12 -63
  164. package/dist/testing/suites/platformProjectGrantStoreSuite.d.ts +0 -1
  165. package/dist/testing/suites/platformProjectGrantStoreSuite.js +0 -5
  166. package/dist/testing/suites/projectStoreSuite.d.ts +5 -1
  167. package/dist/testing/suites/projectStoreSuite.js +6 -6
  168. package/dist/testing/suites/shareLinkStoreSuite.d.ts +2 -1
  169. package/dist/testing/suites/shareLinkStoreSuite.js +22 -25
  170. package/dist/testing/suites/solveMetricSinkSuite.d.ts +0 -1
  171. package/dist/testing/suites/solveMetricSinkSuite.js +2 -2
  172. package/dist/testing/suites/storageProviderSuite.d.ts +0 -1
  173. package/dist/testing/suites/storageProviderSuite.js +15 -10
  174. package/dist/testing/suites/userProfileStoreSuite.d.ts +1 -2
  175. package/dist/testing/suites/userProfileStoreSuite.js +42 -7
  176. package/dist/userProfile/index.d.ts +2 -2
  177. package/dist/userProfile/index.js +2 -2
  178. package/dist/userProfile/interface.d.ts +5 -6
  179. package/dist/userProfile/interface.js +0 -1
  180. package/dist/userProfile/types.d.ts +2 -1
  181. package/dist/userProfile/types.js +2 -1
  182. package/dist/utils/audit.d.ts +0 -1
  183. package/dist/utils/audit.js +0 -1
  184. package/dist/utils/index.d.ts +0 -2
  185. package/dist/utils/index.js +0 -1
  186. package/package.json +7 -6
  187. package/dist/access/index.d.ts.map +0 -1
  188. package/dist/access/index.js.map +0 -1
  189. package/dist/access/rules.d.ts.map +0 -1
  190. package/dist/access/rules.js.map +0 -1
  191. package/dist/apiTokens/index.d.ts.map +0 -1
  192. package/dist/apiTokens/index.js.map +0 -1
  193. package/dist/apiTokens/interface.d.ts.map +0 -1
  194. package/dist/apiTokens/interface.js.map +0 -1
  195. package/dist/apiTokens/scope.d.ts.map +0 -1
  196. package/dist/apiTokens/scope.js.map +0 -1
  197. package/dist/apiTokens/types.d.ts.map +0 -1
  198. package/dist/apiTokens/types.js.map +0 -1
  199. package/dist/auth/index.d.ts.map +0 -1
  200. package/dist/auth/index.js.map +0 -1
  201. package/dist/auth/interface.d.ts.map +0 -1
  202. package/dist/auth/interface.js.map +0 -1
  203. package/dist/auth/types.d.ts.map +0 -1
  204. package/dist/auth/types.js.map +0 -1
  205. package/dist/bindings/index.d.ts.map +0 -1
  206. package/dist/bindings/index.js.map +0 -1
  207. package/dist/bindings/interface.d.ts.map +0 -1
  208. package/dist/bindings/interface.js.map +0 -1
  209. package/dist/computeServer/index.d.ts.map +0 -1
  210. package/dist/computeServer/index.js.map +0 -1
  211. package/dist/computeServer/interface.d.ts.map +0 -1
  212. package/dist/computeServer/interface.js.map +0 -1
  213. package/dist/computeServer/secrets.d.ts.map +0 -1
  214. package/dist/computeServer/secrets.js.map +0 -1
  215. package/dist/computeServer/types.d.ts.map +0 -1
  216. package/dist/computeServer/types.js.map +0 -1
  217. package/dist/computeServer/utils.d.ts.map +0 -1
  218. package/dist/computeServer/utils.js.map +0 -1
  219. package/dist/config.d.ts.map +0 -1
  220. package/dist/config.js.map +0 -1
  221. package/dist/context.d.ts.map +0 -1
  222. package/dist/context.js.map +0 -1
  223. package/dist/data/index.d.ts.map +0 -1
  224. package/dist/data/index.js.map +0 -1
  225. package/dist/data/interface.d.ts.map +0 -1
  226. package/dist/data/interface.js.map +0 -1
  227. package/dist/definitions/index.d.ts.map +0 -1
  228. package/dist/definitions/index.js.map +0 -1
  229. package/dist/definitions/interface.d.ts.map +0 -1
  230. package/dist/definitions/interface.js.map +0 -1
  231. package/dist/definitions/paths.d.ts.map +0 -1
  232. package/dist/definitions/paths.js.map +0 -1
  233. package/dist/definitions/schemas.d.ts.map +0 -1
  234. package/dist/definitions/schemas.js.map +0 -1
  235. package/dist/definitions/types.d.ts.map +0 -1
  236. package/dist/definitions/types.js.map +0 -1
  237. package/dist/errors/interface.d.ts.map +0 -1
  238. package/dist/errors/interface.js.map +0 -1
  239. package/dist/errors.d.ts.map +0 -1
  240. package/dist/errors.js.map +0 -1
  241. package/dist/events/audit.d.ts.map +0 -1
  242. package/dist/events/audit.js.map +0 -1
  243. package/dist/events/index.d.ts.map +0 -1
  244. package/dist/events/index.js.map +0 -1
  245. package/dist/events/interface.d.ts.map +0 -1
  246. package/dist/events/interface.js.map +0 -1
  247. package/dist/index.d.ts.map +0 -1
  248. package/dist/index.js.map +0 -1
  249. package/dist/invites/index.d.ts.map +0 -1
  250. package/dist/invites/index.js.map +0 -1
  251. package/dist/invites/interface.d.ts.map +0 -1
  252. package/dist/invites/interface.js.map +0 -1
  253. package/dist/invites/types.d.ts.map +0 -1
  254. package/dist/invites/types.js.map +0 -1
  255. package/dist/logging/interface.d.ts.map +0 -1
  256. package/dist/logging/interface.js.map +0 -1
  257. package/dist/metrics/interface.d.ts.map +0 -1
  258. package/dist/metrics/interface.js.map +0 -1
  259. package/dist/notifications/index.d.ts.map +0 -1
  260. package/dist/notifications/index.js.map +0 -1
  261. package/dist/notifications/interface.d.ts.map +0 -1
  262. package/dist/notifications/interface.js.map +0 -1
  263. package/dist/notifications/types.d.ts.map +0 -1
  264. package/dist/notifications/types.js.map +0 -1
  265. package/dist/organizations/index.d.ts.map +0 -1
  266. package/dist/organizations/index.js.map +0 -1
  267. package/dist/organizations/interface.d.ts.map +0 -1
  268. package/dist/organizations/interface.js.map +0 -1
  269. package/dist/organizations/paths.d.ts.map +0 -1
  270. package/dist/organizations/paths.js.map +0 -1
  271. package/dist/organizations/schemas.d.ts.map +0 -1
  272. package/dist/organizations/schemas.js.map +0 -1
  273. package/dist/organizations/types.d.ts.map +0 -1
  274. package/dist/organizations/types.js.map +0 -1
  275. package/dist/pagination.d.ts.map +0 -1
  276. package/dist/pagination.js.map +0 -1
  277. package/dist/permissions/index.d.ts.map +0 -1
  278. package/dist/permissions/index.js.map +0 -1
  279. package/dist/permissions/interface.d.ts.map +0 -1
  280. package/dist/permissions/interface.js.map +0 -1
  281. package/dist/permissions/types.d.ts.map +0 -1
  282. package/dist/permissions/types.js.map +0 -1
  283. package/dist/platformProjects/index.d.ts.map +0 -1
  284. package/dist/platformProjects/index.js.map +0 -1
  285. package/dist/platformProjects/interface.d.ts.map +0 -1
  286. package/dist/platformProjects/interface.js.map +0 -1
  287. package/dist/platformProjects/schemas.d.ts.map +0 -1
  288. package/dist/platformProjects/schemas.js.map +0 -1
  289. package/dist/platformProjects/types.d.ts.map +0 -1
  290. package/dist/platformProjects/types.js.map +0 -1
  291. package/dist/projects/index.d.ts.map +0 -1
  292. package/dist/projects/index.js.map +0 -1
  293. package/dist/projects/interface.d.ts.map +0 -1
  294. package/dist/projects/interface.js.map +0 -1
  295. package/dist/projects/schemas.d.ts.map +0 -1
  296. package/dist/projects/schemas.js.map +0 -1
  297. package/dist/projects/types.d.ts.map +0 -1
  298. package/dist/projects/types.js.map +0 -1
  299. package/dist/shareLinks/index.d.ts.map +0 -1
  300. package/dist/shareLinks/index.js.map +0 -1
  301. package/dist/shareLinks/interface.d.ts.map +0 -1
  302. package/dist/shareLinks/interface.js.map +0 -1
  303. package/dist/shareLinks/schemas.d.ts.map +0 -1
  304. package/dist/shareLinks/schemas.js.map +0 -1
  305. package/dist/shareLinks/types.d.ts.map +0 -1
  306. package/dist/shareLinks/types.js.map +0 -1
  307. package/dist/solveCache/interface.d.ts.map +0 -1
  308. package/dist/solveCache/interface.js.map +0 -1
  309. package/dist/storage/assetClasses.d.ts.map +0 -1
  310. package/dist/storage/assetClasses.js.map +0 -1
  311. package/dist/storage/cacheBust.d.ts.map +0 -1
  312. package/dist/storage/cacheBust.js.map +0 -1
  313. package/dist/storage/image.d.ts.map +0 -1
  314. package/dist/storage/image.js.map +0 -1
  315. package/dist/storage/index.d.ts.map +0 -1
  316. package/dist/storage/index.js.map +0 -1
  317. package/dist/storage/interface.d.ts.map +0 -1
  318. package/dist/storage/interface.js.map +0 -1
  319. package/dist/testing/index.d.ts.map +0 -1
  320. package/dist/testing/index.js.map +0 -1
  321. package/dist/testing/suites/apiTokenStoreSuite.d.ts.map +0 -1
  322. package/dist/testing/suites/apiTokenStoreSuite.js.map +0 -1
  323. package/dist/testing/suites/authProviderSuite.d.ts.map +0 -1
  324. package/dist/testing/suites/authProviderSuite.js.map +0 -1
  325. package/dist/testing/suites/computeServerStoreSuite.d.ts.map +0 -1
  326. package/dist/testing/suites/computeServerStoreSuite.js.map +0 -1
  327. package/dist/testing/suites/definitionStoreSuite.d.ts.map +0 -1
  328. package/dist/testing/suites/definitionStoreSuite.js.map +0 -1
  329. package/dist/testing/suites/emailLinkAuthSuite.d.ts.map +0 -1
  330. package/dist/testing/suites/emailLinkAuthSuite.js.map +0 -1
  331. package/dist/testing/suites/eventSinkSuite.d.ts.map +0 -1
  332. package/dist/testing/suites/eventSinkSuite.js.map +0 -1
  333. package/dist/testing/suites/helpers.d.ts.map +0 -1
  334. package/dist/testing/suites/helpers.js.map +0 -1
  335. package/dist/testing/suites/inviteStoreSuite.d.ts.map +0 -1
  336. package/dist/testing/suites/inviteStoreSuite.js.map +0 -1
  337. package/dist/testing/suites/orgStoreSuite.d.ts.map +0 -1
  338. package/dist/testing/suites/orgStoreSuite.js.map +0 -1
  339. package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +0 -1
  340. package/dist/testing/suites/platformPermissionStoreSuite.js.map +0 -1
  341. package/dist/testing/suites/platformProjectGrantStoreSuite.d.ts.map +0 -1
  342. package/dist/testing/suites/platformProjectGrantStoreSuite.js.map +0 -1
  343. package/dist/testing/suites/projectStoreSuite.d.ts.map +0 -1
  344. package/dist/testing/suites/projectStoreSuite.js.map +0 -1
  345. package/dist/testing/suites/shareLinkStoreSuite.d.ts.map +0 -1
  346. package/dist/testing/suites/shareLinkStoreSuite.js.map +0 -1
  347. package/dist/testing/suites/solveMetricSinkSuite.d.ts.map +0 -1
  348. package/dist/testing/suites/solveMetricSinkSuite.js.map +0 -1
  349. package/dist/testing/suites/storageProviderSuite.d.ts.map +0 -1
  350. package/dist/testing/suites/storageProviderSuite.js.map +0 -1
  351. package/dist/testing/suites/userProfileStoreSuite.d.ts.map +0 -1
  352. package/dist/testing/suites/userProfileStoreSuite.js.map +0 -1
  353. package/dist/userProfile/index.d.ts.map +0 -1
  354. package/dist/userProfile/index.js.map +0 -1
  355. package/dist/userProfile/interface.d.ts.map +0 -1
  356. package/dist/userProfile/interface.js.map +0 -1
  357. package/dist/userProfile/types.d.ts.map +0 -1
  358. package/dist/userProfile/types.js.map +0 -1
  359. package/dist/utils/audit.d.ts.map +0 -1
  360. package/dist/utils/audit.js.map +0 -1
  361. package/dist/utils/index.d.ts.map +0 -1
  362. package/dist/utils/index.js.map +0 -1
@@ -5,12 +5,9 @@
5
5
  * `countInstanceAdminsExcluding`, plus the §2 sole-`instance_admin`
6
6
  * invariant — `set` must return `'last_admin'` when dropping the only admin,
7
7
  * and `countInstanceAdminsExcluding` must exclude the named user.
8
- *
9
- * These tests used to live on the auth-provider conformance suite when the
10
- * invariant was enforced there; the move from `IAuthProvider` to
11
- * `IPlatformPermissionStore` brought them with it.
12
8
  */
13
9
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import { makeSystemCtx } from './helpers.js';
14
11
  import { ALL_ORG_PERMISSIONS, ALL_PLATFORM_PERMISSIONS } from '../../index.js';
15
12
  export function runPlatformPermissionStoreConformance(opts) {
16
13
  const { name, createStore, disableUser, cleanup } = opts;
@@ -89,12 +86,7 @@ export function runPlatformPermissionStoreConformance(opts) {
89
86
  it('hasInstanceAdmin returns false on a fresh instance, true after granting', async () => {
90
87
  const { store, seedUser } = await createStore();
91
88
  const adminId = await seedUser();
92
- const sysCtx = {
93
- userId: '',
94
- platformPermissions: [],
95
- orgPermissions: [],
96
- system: true
97
- };
89
+ const sysCtx = makeSystemCtx();
98
90
  expect(await store.hasInstanceAdmin(sysCtx)).toBe(false);
99
91
  await store.set(adminCtx(adminId), adminId, ['instance_admin']);
100
92
  expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
@@ -121,10 +113,8 @@ export function runPlatformPermissionStoreConformance(opts) {
121
113
  const after = await store.getFor(adminCtx(adminA), adminB);
122
114
  expect(after).toContain('instance_admin');
123
115
  });
124
- // The invariant has to survive concurrency, not just sequence. Two admins
125
- // demoting each other at the same moment used to each read the other as
126
- // "another admin exists" and both commit, bricking the instance. Exactly
127
- // one must win.
116
+ // The invariant has to survive concurrency, not just sequence: two admins
117
+ // demoting each other must not each read the other as the survivor.
128
118
  it('concurrent demotions of two admins leave one standing', async () => {
129
119
  const { store, seedUser } = await createStore();
130
120
  const adminA = await seedUser();
@@ -138,12 +128,7 @@ export function runPlatformPermissionStoreConformance(opts) {
138
128
  // One demotion succeeds, the other is refused — never both.
139
129
  expect([resA, resB].filter((r) => r === 'ok')).toHaveLength(1);
140
130
  expect([resA, resB].filter((r) => r === 'last_admin')).toHaveLength(1);
141
- const sysCtx = {
142
- userId: '',
143
- platformPermissions: [],
144
- orgPermissions: [],
145
- system: true
146
- };
131
+ const sysCtx = makeSystemCtx();
147
132
  expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
148
133
  });
149
134
  it('a burst of concurrent demotions never drops the last admin', async () => {
@@ -159,24 +144,14 @@ export function runPlatformPermissionStoreConformance(opts) {
159
144
  // Whatever the interleaving, exactly one admin survives.
160
145
  expect(results.filter((r) => r === 'ok')).toHaveLength(ids.length - 1);
161
146
  expect(results.filter((r) => r === 'last_admin')).toHaveLength(1);
162
- const sysCtx = {
163
- userId: '',
164
- platformPermissions: [],
165
- orgPermissions: [],
166
- system: true
167
- };
147
+ const sysCtx = makeSystemCtx();
168
148
  expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
169
149
  });
170
150
  it('claimFirstInstanceAdmin grants on a fresh instance and refuses once claimed', async () => {
171
151
  const { store, seedUser } = await createStore();
172
152
  const first = await seedUser();
173
153
  const second = await seedUser();
174
- const sysCtx = {
175
- userId: '',
176
- platformPermissions: [],
177
- orgPermissions: [],
178
- system: true
179
- };
154
+ const sysCtx = makeSystemCtx();
180
155
  expect(await store.claimFirstInstanceAdmin(sysCtx, first, ['instance_admin'])).toBe(true);
181
156
  expect(await store.getFor(adminCtx(first), first)).toContain('instance_admin');
182
157
  // The instance is claimed. A later signer must not become a second
@@ -193,12 +168,7 @@ export function runPlatformPermissionStoreConformance(opts) {
193
168
  const ids = [];
194
169
  for (let i = 0; i < 4; i++)
195
170
  ids.push(await seedUser());
196
- const sysCtx = {
197
- userId: '',
198
- platformPermissions: [],
199
- orgPermissions: [],
200
- system: true
201
- };
171
+ const sysCtx = makeSystemCtx();
202
172
  const results = await Promise.all(ids.map((id) => store.claimFirstInstanceAdmin(sysCtx, id, ['instance_admin'])));
203
173
  expect(results.filter(Boolean)).toHaveLength(1);
204
174
  const admins = await store.getForBatch(adminCtx(ids[0]), ids);
@@ -210,12 +180,7 @@ export function runPlatformPermissionStoreConformance(opts) {
210
180
  const adminB = await seedUser();
211
181
  await store.set(adminCtx(adminA), adminA, ['instance_admin']);
212
182
  await store.set(adminCtx(adminA), adminB, ['instance_admin']);
213
- const sysCtx = {
214
- userId: '',
215
- platformPermissions: [],
216
- orgPermissions: [],
217
- system: true
218
- };
183
+ const sysCtx = makeSystemCtx();
219
184
  // Excluding adminA: one admin (adminB) remains
220
185
  expect(await store.countInstanceAdminsExcluding(sysCtx, adminA)).toBe(1);
221
186
  // Excluding adminB: one admin (adminA) remains
@@ -225,23 +190,13 @@ export function runPlatformPermissionStoreConformance(opts) {
225
190
  const { store, seedUser } = await createStore();
226
191
  const adminId = await seedUser();
227
192
  await store.set(adminCtx(adminId), adminId, ['instance_admin']);
228
- const sysCtx = {
229
- userId: '',
230
- platformPermissions: [],
231
- orgPermissions: [],
232
- system: true
233
- };
193
+ const sysCtx = makeSystemCtx();
234
194
  expect(await store.countInstanceAdminsExcluding(sysCtx, adminId)).toBe(0);
235
195
  });
236
196
  itWithDisable('hasInstanceAdmin ignores disabled admins', async () => {
237
197
  const { store, seedUser } = await createStore();
238
198
  const adminId = await seedUser();
239
- const sysCtx = {
240
- userId: '',
241
- platformPermissions: [],
242
- orgPermissions: [],
243
- system: true
244
- };
199
+ const sysCtx = makeSystemCtx();
245
200
  await store.set(adminCtx(adminId), adminId, ['instance_admin']);
246
201
  expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
247
202
  await disableUser(adminId);
@@ -253,12 +208,7 @@ export function runPlatformPermissionStoreConformance(opts) {
253
208
  const adminB = await seedUser();
254
209
  await store.set(adminCtx(adminA), adminA, ['instance_admin']);
255
210
  await store.set(adminCtx(adminA), adminB, ['instance_admin']);
256
- const sysCtx = {
257
- userId: '',
258
- platformPermissions: [],
259
- orgPermissions: [],
260
- system: true
261
- };
211
+ const sysCtx = makeSystemCtx();
262
212
  await disableUser(adminB);
263
213
  // adminB holds the grant but is disabled — must not count as the
264
214
  // "other admin" that would allow demoting/disabling adminA.
@@ -312,4 +262,3 @@ export function runPlatformPermissionStoreConformance(opts) {
312
262
  });
313
263
  });
314
264
  }
315
- //# sourceMappingURL=platformPermissionStoreSuite.js.map
@@ -17,4 +17,3 @@ export interface PlatformProjectGrantStoreConformanceOptions {
17
17
  seedProject?: (projectId: string) => Promise<void>;
18
18
  }
19
19
  export declare function runPlatformProjectGrantStoreConformance(opts: PlatformProjectGrantStoreConformanceOptions): void;
20
- //# sourceMappingURL=platformProjectGrantStoreSuite.d.ts.map
@@ -92,10 +92,6 @@ export function runPlatformProjectGrantStoreConformance(opts) {
92
92
  const got = await store.listByProject(SYSTEM_CONTEXT, projectId);
93
93
  expect(got).toHaveLength(0);
94
94
  });
95
- it('delete on unknown id throws', async () => {
96
- const store = await createStore();
97
- await expect(store.delete(SYSTEM_CONTEXT, makeUuid())).rejects.toThrow();
98
- });
99
95
  it('deleteByProject removes every grant for that project', async () => {
100
96
  const store = await createStore();
101
97
  const target = await seed();
@@ -131,4 +127,3 @@ export function runPlatformProjectGrantStoreConformance(opts) {
131
127
  });
132
128
  });
133
129
  }
134
- //# sourceMappingURL=platformProjectGrantStoreSuite.js.map
@@ -24,8 +24,12 @@ export interface ProjectStoreConformanceOptions {
24
24
  * actually stored — callers use that, not the suggested one.
25
25
  */
26
26
  seedUser?: SeedUserFn;
27
+ /**
28
+ * Seeds the other party in the ctx-isolation cases. Needed where `seedUser` makes
29
+ * instance admins, who see everything by design.
30
+ */
31
+ seedOutsider?: SeedUserFn;
27
32
  /** If true, run ctx-isolation tests (adapters with row-level security). */
28
33
  ctxIsolation?: boolean;
29
34
  }
30
35
  export declare function runProjectStoreConformance(opts: ProjectStoreConformanceOptions): void;
31
- //# sourceMappingURL=projectStoreSuite.d.ts.map
@@ -32,8 +32,9 @@ function member(projectId, userId, role) {
32
32
  };
33
33
  }
34
34
  export function runProjectStoreConformance(opts) {
35
- const { name, createStore: rawCreateStore, seedUser = noopSeedUser, ctxIsolation = false } = opts;
35
+ const { name, createStore: rawCreateStore, seedUser = noopSeedUser, seedOutsider = seedUser, ctxIsolation = false } = opts;
36
36
  const { seed, ctx, registerToken } = makeSeedHelpers(seedUser);
37
+ const outsider = makeSeedHelpers(seedOutsider);
37
38
  const createStore = async () => {
38
39
  const result = await rawCreateStore();
39
40
  if (result.ownerSessionToken)
@@ -317,22 +318,21 @@ export function runProjectStoreConformance(opts) {
317
318
  it('ctx isolation: projects created by one user are not visible to another', async () => {
318
319
  const { store, orgId } = await createStore();
319
320
  const userA = await seed();
320
- const userB = await seed();
321
+ const userB = await outsider.seed();
321
322
  const pA = project(orgId, userA);
322
323
  await store.createProject(ctx(userA), pA);
323
- const page = await store.listProjects(ctx(userB), orgId);
324
+ const page = await store.listProjects(outsider.ctx(userB), orgId);
324
325
  expect(page.items.map((p) => p.id)).not.toContain(pA.id);
325
326
  });
326
327
  it('ctx isolation: user cannot get a project they do not own', async () => {
327
328
  const { store, orgId } = await createStore();
328
329
  const userA = await seed();
329
- const userB = await seed();
330
+ const userB = await outsider.seed();
330
331
  const pA = project(orgId, userA);
331
332
  await store.createProject(ctx(userA), pA);
332
- const got = await store.getProject(ctx(userB), pA.id);
333
+ const got = await store.getProject(outsider.ctx(userB), pA.id);
333
334
  expect(got).toBeNull();
334
335
  });
335
336
  }
336
337
  });
337
338
  }
338
- //# sourceMappingURL=projectStoreSuite.js.map
@@ -36,6 +36,7 @@ export interface ShareLinkStoreConformanceOptions {
36
36
  * `definitions` row + parent project). Local adapters can return stub ids.
37
37
  */
38
38
  createScope?: () => Promise<ShareLinkTestScope> | ShareLinkTestScope;
39
+ /** The scope seeds `orgId` and `otherOrgId`, so the `listByOrg` cases can run. */
40
+ orgRoster?: boolean;
39
41
  }
40
42
  export declare function runShareLinkStoreConformance(opts: ShareLinkStoreConformanceOptions): void;
41
- //# sourceMappingURL=shareLinkStoreSuite.d.ts.map
@@ -7,13 +7,11 @@
7
7
  * local adapter can stub.
8
8
  */
9
9
  import { describe, it, expect } from 'vitest';
10
- import { makeCtx, makeUuid } from './helpers.js';
11
- // Suite-wide token map. `scopeFor` populates it; `ctx` auto-attaches the
12
- // matching token so DB-side auth (Supabase RLS) sees the right user.
13
- const tokens = new Map();
14
- function ctx(userId, opts = {}) {
15
- return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
16
- }
10
+ import { SYSTEM_CONTEXT } from '../../context.js';
11
+ import { makeSeedHelpers, makeUuid } from './helpers.js';
12
+ // `scopeFor` registers the owner's token, so `ctx` runs as them on an adapter with
13
+ // DB-side auth (Supabase RLS). This suite seeds nobody itself.
14
+ const { ctx, registerToken } = makeSeedHelpers(async (userId) => ({ userId }));
17
15
  const DEFAULT_SCOPE = {
18
16
  ownerId: 'user-1',
19
17
  definitionId: 'def-1',
@@ -37,11 +35,11 @@ function link(scope, overrides = {}) {
37
35
  };
38
36
  }
39
37
  export function runShareLinkStoreConformance(opts) {
40
- const { name, createStore, createScope } = opts;
38
+ const { name, createStore, createScope, orgRoster = false } = opts;
41
39
  const scopeFor = async () => {
42
40
  const s = createScope ? await createScope() : DEFAULT_SCOPE;
43
41
  if (s.ownerSessionToken)
44
- tokens.set(s.ownerId, s.ownerSessionToken);
42
+ registerToken(s.ownerId, s.ownerSessionToken);
45
43
  return s;
46
44
  };
47
45
  describe(`IShareLinkStore conformance: ${name}`, () => {
@@ -132,15 +130,12 @@ export function runShareLinkStoreConformance(opts) {
132
130
  const got = await store.getByTokenHash(ctx(scope.ownerId), 'not-yet-expired');
133
131
  expect(got?.id).toBe(l.id);
134
132
  });
135
- // The org roster is the only read that answers "what reaches this
136
- // tenant's data right now?". `it.skipIf` rather than an omitted block, so
137
- // an adapter that stops seeding an org shows as skipped instead of silently
138
- // covering nothing.
139
- describe('listByOrg', () => {
133
+ // The org roster is the only read that answers "what reaches this tenant's
134
+ // data right now?". `skipIf` rather than an omitted block, so an adapter that
135
+ // doesn't seed an org shows as skipped instead of silently covering nothing.
136
+ describe.skipIf(!orgRoster)('listByOrg', () => {
140
137
  it('returns links across every definition in the org, newest first', async () => {
141
138
  const scope = await scopeFor();
142
- if (!scope.orgId)
143
- return;
144
139
  const store = await createStore();
145
140
  const a = link(scope, { createdAt: '2026-04-01T00:00:00.000Z' });
146
141
  const b = link(scope, {
@@ -154,8 +149,6 @@ export function runShareLinkStoreConformance(opts) {
154
149
  });
155
150
  it('names the parent definition and project on every row', async () => {
156
151
  const scope = await scopeFor();
157
- if (!scope.orgId)
158
- return;
159
152
  const store = await createStore();
160
153
  const l = link(scope);
161
154
  await store.create(ctx(scope.ownerId), l);
@@ -169,8 +162,6 @@ export function runShareLinkStoreConformance(opts) {
169
162
  });
170
163
  it('never carries tokenHash — the roster spans the whole tenant', async () => {
171
164
  const scope = await scopeFor();
172
- if (!scope.orgId)
173
- return;
174
165
  const store = await createStore();
175
166
  await store.create(ctx(scope.ownerId), link(scope));
176
167
  const page = await store.listByOrg(ctx(scope.ownerId), scope.orgId, { limit: 100 });
@@ -181,8 +172,6 @@ export function runShareLinkStoreConformance(opts) {
181
172
  });
182
173
  it('excludes revoked and expired links', async () => {
183
174
  const scope = await scopeFor();
184
- if (!scope.orgId)
185
- return;
186
175
  const store = await createStore();
187
176
  const revoked = link(scope);
188
177
  const expired = link(scope, { expiresAt: new Date(Date.now() - 60_000).toISOString() });
@@ -199,14 +188,23 @@ export function runShareLinkStoreConformance(opts) {
199
188
  });
200
189
  it('does not leak links into another org', async () => {
201
190
  const scope = await scopeFor();
202
- if (!scope.orgId || !scope.otherOrgId)
203
- return;
191
+ if (!scope.otherOrgId)
192
+ throw new Error('orgRoster requires scope.otherOrgId');
204
193
  const store = await createStore();
205
194
  await store.create(ctx(scope.ownerId), link(scope));
206
195
  const page = await store.listByOrg(ctx(scope.ownerId), scope.otherOrgId, { limit: 100 });
207
196
  expect(page.items).toHaveLength(0);
208
197
  });
209
198
  });
199
+ it('tryIncrementSolveCount holds the cap under concurrent solves', async () => {
200
+ const store = await createStore();
201
+ const scope = await scopeFor();
202
+ const l = link(scope, { maxSolves: 5 });
203
+ await store.create(ctx(scope.ownerId), l);
204
+ const results = await Promise.all(Array.from({ length: 20 }, () => store.tryIncrementSolveCount(SYSTEM_CONTEXT, l.id)));
205
+ expect(results.filter((n) => n !== null)).toHaveLength(5);
206
+ expect((await store.getById(ctx(scope.ownerId), l.id))?.solveCount).toBe(5);
207
+ });
210
208
  it('revoke is idempotent (no error on double-revoke)', async () => {
211
209
  const store = await createStore();
212
210
  const scope = await scopeFor();
@@ -251,4 +249,3 @@ export function runShareLinkStoreConformance(opts) {
251
249
  });
252
250
  });
253
251
  }
254
- //# sourceMappingURL=shareLinkStoreSuite.js.map
@@ -41,4 +41,3 @@ export interface SolveMetricSinkConformanceOptions {
41
41
  cleanup?: () => Promise<void> | void;
42
42
  }
43
43
  export declare function runSolveMetricSinkConformance(opts: SolveMetricSinkConformanceOptions): void;
44
- //# sourceMappingURL=solveMetricSinkSuite.d.ts.map
@@ -10,9 +10,10 @@
10
10
  * a backend failure — the compute route depends on that to keep serving the
11
11
  * response after a solve.
12
12
  */
13
+ import { makeSystemCtx } from './helpers.js';
13
14
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
14
15
  function sysCtx(userId) {
15
- return { userId, platformPermissions: [], orgPermissions: [], system: true };
16
+ return makeSystemCtx(userId);
16
17
  }
17
18
  function baseMetric(overrides = {}) {
18
19
  return {
@@ -95,4 +96,3 @@ export function runSolveMetricSinkConformance(opts) {
95
96
  }
96
97
  });
97
98
  }
98
- //# sourceMappingURL=solveMetricSinkSuite.js.map
@@ -12,4 +12,3 @@ export interface StorageProviderConformanceOptions {
12
12
  createStorage: () => Promise<IStorageProvider> | IStorageProvider;
13
13
  }
14
14
  export declare function runStorageProviderConformance(opts: StorageProviderConformanceOptions): void;
15
- //# sourceMappingURL=storageProviderSuite.d.ts.map
@@ -24,6 +24,10 @@ function checksum(data) {
24
24
  export function runStorageProviderConformance(opts) {
25
25
  const { name, createStorage } = opts;
26
26
  describe(`IStorageProvider conformance: ${name}`, () => {
27
+ it('put returns the path it stored at', async () => {
28
+ const storage = await createStorage();
29
+ expect(await storage.put('test/plain.txt', bytes('data'), 'text/plain')).toBe('test/plain.txt');
30
+ });
27
31
  it('put + get returns the stored data', async () => {
28
32
  const storage = await createStorage();
29
33
  const data = bytes('Hello, World!');
@@ -77,6 +81,13 @@ export function runStorageProviderConformance(opts) {
77
81
  expect(meta1).toBeNull();
78
82
  expect(file2).toBeTruthy();
79
83
  });
84
+ it('deletePrefix refuses a prefix that names the whole store', async () => {
85
+ const storage = await createStorage();
86
+ await storage.put('test/file.txt', bytes('keep'));
87
+ await expect(storage.deletePrefix('')).rejects.toThrow();
88
+ await expect(storage.deletePrefix('/')).rejects.toThrow();
89
+ expect(await storage.get('test/file.txt')).toEqual(bytes('keep'));
90
+ });
80
91
  it('deletePrefix with no matches is a no-op', async () => {
81
92
  const storage = await createStorage();
82
93
  await storage.put('other/file.txt', bytes('data'));
@@ -136,18 +147,13 @@ export function runStorageProviderConformance(opts) {
136
147
  // Image transcoding
137
148
  // ============================================================================
138
149
  //
139
- // Every adapter MUST run uploads through the shared `transcodeImageIfNeeded`
140
- // helper so cover images, archive thumbnails, etc. land as WebP regardless
141
- // of what the user uploaded. Otherwise a provider that skipped this would
142
- // produce a pipeline that looks correct until someone uploads a PNG.
150
+ // Part of the `IStorageProvider.put` contract: images land as WebP at a
151
+ // rewritten path, whatever the user uploaded.
143
152
  it('transcodes png input to webp (path rewritten, magic bytes flipped)', async () => {
144
153
  const storage = await createStorage();
145
154
  // Minimal 1×1 transparent PNG (68 bytes): 8-byte signature + IHDR + IDAT + IEND.
146
155
  // The IDAT is a real zlib stream of one scanline (filter 0 + RGBA 0,0,0,0) with a
147
- // correct Adler-32. Don't hand-tweak these bytes — the previous fixture carried a
148
- // truncated IDAT that older libpng tolerated and sharp >=0.35 rejects outright
149
- // ("vipspng: libpng read error"). Regenerate with zlib.deflateSync if it ever needs
150
- // to change.
156
+ // correct Adler-32. Don't hand-tweak these bytes: regenerate with zlib.deflateSync.
151
157
  const pngBytes = Uint8Array.from([
152
158
  0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
153
159
  0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
@@ -155,7 +161,7 @@ export function runStorageProviderConformance(opts) {
155
161
  0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x7a, 0x5e, 0xab, 0x3f, 0x00, 0x00, 0x00, 0x00,
156
162
  0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
157
163
  ]);
158
- await storage.put('covers/tiny.png', pngBytes, 'image/png');
164
+ expect(await storage.put('covers/tiny.png', pngBytes, 'image/png')).toBe('covers/tiny.webp');
159
165
  // Image transcoding rewrites the path to .webp — the original path is empty.
160
166
  const atPng = await storage.get('covers/tiny.png');
161
167
  expect(atPng).toBeNull();
@@ -185,4 +191,3 @@ export function runStorageProviderConformance(opts) {
185
191
  });
186
192
  });
187
193
  }
188
- //# sourceMappingURL=storageProviderSuite.js.map
@@ -2,7 +2,7 @@
2
2
  * Adapter conformance suite for IUserProfileStore.
3
3
  *
4
4
  * Tests the user-profile mutations (display name, starred definitions,
5
- * recent runs) that used to live on IAuthProvider. Adapters must seed
5
+ * recent runs). Adapters must seed
6
6
  * a real user before each test since profile mutations target a userId.
7
7
  */
8
8
  import type { IUserProfileStore } from '../../userProfile/interface.js';
@@ -21,4 +21,3 @@ export interface UserProfileStoreConformanceOptions {
21
21
  }>;
22
22
  }
23
23
  export declare function runUserProfileStoreConformance(opts: UserProfileStoreConformanceOptions): void;
24
- //# sourceMappingURL=userProfileStoreSuite.d.ts.map
@@ -2,20 +2,56 @@
2
2
  * Adapter conformance suite for IUserProfileStore.
3
3
  *
4
4
  * Tests the user-profile mutations (display name, starred definitions,
5
- * recent runs) that used to live on IAuthProvider. Adapters must seed
5
+ * recent runs). Adapters must seed
6
6
  * a real user before each test since profile mutations target a userId.
7
7
  */
8
8
  import { describe, it, expect } from 'vitest';
9
9
  import { SYSTEM_CONTEXT } from '../../context.js';
10
- import { makeUuid } from './helpers.js';
11
- // Profile mutations need a ctx scoped to the target user (docs/contributing/permissions.md §9 +
12
- // the `IUserProfileStore` JSDoc auth-boundary note). The conformance suite
13
- // uses SYSTEM_CONTEXT throughout so it doesn't need to mint per-user ctxs;
14
- // the per-user authorization is exercised in adapter-specific tests.
10
+ import { makeCtx, makeUuid } from './helpers.js';
11
+ // SYSTEM_CONTEXT, so most cases don't need a ctx minted per user. The scoping case
12
+ // below builds its own.
15
13
  const ctx = SYSTEM_CONTEXT;
16
14
  export function runUserProfileStoreConformance(opts) {
17
15
  const { name, createStore } = opts;
18
16
  describe(`IUserProfileStore conformance: ${name}`, () => {
17
+ it('a user cannot read or change someone else through a plain ctx', async () => {
18
+ const { store, seedUser } = await createStore();
19
+ const victim = await seedUser('victim@example.com');
20
+ const other = makeCtx(makeUuid(), { platformPermissions: [] });
21
+ await expect(store.getProfile(other, victim.id)).rejects.toThrow();
22
+ await expect(store.updateProfile(other, victim.id, { displayName: 'Hacked' })).rejects.toThrow();
23
+ });
24
+ it('recordRun keeps one entry per definition, newest first', async () => {
25
+ const { store, seedUser } = await createStore();
26
+ const user = await seedUser('dedupe@example.com');
27
+ const run = (definitionId, timestamp) => ({
28
+ definitionId,
29
+ runId: makeUuid(),
30
+ definitionName: 'D',
31
+ timestamp
32
+ });
33
+ const a = makeUuid();
34
+ const b = makeUuid();
35
+ await store.recordRun(ctx, user.id, run(a, '2026-01-01T00:00:00.000Z'));
36
+ await store.recordRun(ctx, user.id, run(b, '2026-01-02T00:00:00.000Z'));
37
+ await store.recordRun(ctx, user.id, run(a, '2026-01-03T00:00:00.000Z'));
38
+ const profile = await store.getProfile(ctx, user.id);
39
+ expect(profile?.recentRuns.map((r) => r.definitionId)).toEqual([a, b]);
40
+ });
41
+ it('recordRun caps the list', async () => {
42
+ const { store, seedUser } = await createStore();
43
+ const user = await seedUser('cap@example.com');
44
+ for (let i = 0; i < 30; i++) {
45
+ await store.recordRun(ctx, user.id, {
46
+ definitionId: makeUuid(),
47
+ runId: makeUuid(),
48
+ definitionName: 'D',
49
+ timestamp: new Date(i).toISOString()
50
+ });
51
+ }
52
+ const profile = await store.getProfile(ctx, user.id);
53
+ expect(profile?.recentRuns.length).toBeLessThan(30);
54
+ });
19
55
  it('getProfile returns null for unknown user', async () => {
20
56
  const { store } = await createStore();
21
57
  const profile = await store.getProfile(ctx, makeUuid());
@@ -102,4 +138,3 @@ export function runUserProfileStoreConformance(opts) {
102
138
  });
103
139
  });
104
140
  }
105
- //# sourceMappingURL=userProfileStoreSuite.js.map
@@ -1,4 +1,4 @@
1
1
  export type { IUserProfileStore } from './interface.js';
2
2
  export type { UserProfile, RecentRun } from './types.js';
3
- export { emptyProfile } from './types.js';
4
- //# sourceMappingURL=index.d.ts.map
3
+ export { MAX_RECENT_RUNS, emptyProfile } from './types.js';
4
+ export { assertCanAccessProfile } from '../context.js';
@@ -1,2 +1,2 @@
1
- export { emptyProfile } from './types.js';
2
- //# sourceMappingURL=index.js.map
1
+ export { MAX_RECENT_RUNS, emptyProfile } from './types.js';
2
+ export { assertCanAccessProfile } from '../context.js';
@@ -6,11 +6,11 @@ import type { RecentRun, UserProfile } from './types.js';
6
6
  * providers (Entra, Supabase Auth) don't have to model fields they can't
7
7
  * persist.
8
8
  *
9
- * Adapters must scope by `ctx`: a caller cannot read or modify another
10
- * user's profile via the `userId` argument. SQL adapters delegate to RLS;
11
- * document-store adapters check `ctx.userId === userId` (or
12
- * `instance_admin` for admin tooling). Pass `SYSTEM_CONTEXT` for trusted
13
- * server flows (auto-seed on signup, janitors).
9
+ * Adapters must scope `getProfile` and the writes by `ctx`: a caller cannot read
10
+ * or modify another user's profile via the `userId` argument. `getProfiles` is
11
+ * unscoped by design, because rosters show other people's names, so its callers
12
+ * decide who may see the list. Pass `SYSTEM_CONTEXT` for trusted server flows
13
+ * (auto-seed on signup, janitors).
14
14
  */
15
15
  export interface IUserProfileStore {
16
16
  /** Returns null when no profile row exists — treat as `emptyProfile(id)`. */
@@ -30,4 +30,3 @@ export interface IUserProfileStore {
30
30
  */
31
31
  recordRun(ctx: RequestContext, userId: string, run: RecentRun): Promise<UserManagementResult>;
32
32
  }
33
- //# sourceMappingURL=interface.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=interface.js.map
@@ -1,3 +1,5 @@
1
+ /** How many entries `recordRun` keeps per user. */
2
+ export declare const MAX_RECENT_RUNS = 20;
1
3
  export interface RecentRun {
2
4
  definitionId: string;
3
5
  runId: string;
@@ -14,4 +16,3 @@ export interface UserProfile {
14
16
  recentRuns: RecentRun[];
15
17
  }
16
18
  export declare function emptyProfile(userId: string): UserProfile;
17
- //# sourceMappingURL=types.d.ts.map
@@ -1,4 +1,5 @@
1
+ /** How many entries `recordRun` keeps per user. */
2
+ export const MAX_RECENT_RUNS = 20;
1
3
  export function emptyProfile(userId) {
2
4
  return { userId, starredDefinitions: [], recentRuns: [] };
3
5
  }
4
- //# sourceMappingURL=types.js.map
@@ -8,4 +8,3 @@ export interface AuditSoftDelete extends AuditUpdate {
8
8
  deletedAt: string;
9
9
  }
10
10
  export declare function auditSoftDelete(ctx: RequestContext, fallbackActor: string | null | undefined): AuditSoftDelete;
11
- //# sourceMappingURL=audit.d.ts.map
@@ -24,4 +24,3 @@ export function auditSoftDelete(ctx, fallbackActor) {
24
24
  updatedBy: ctx.userId || fallbackActor || ''
25
25
  };
26
26
  }
27
- //# sourceMappingURL=audit.js.map
@@ -1,3 +1 @@
1
- export type { AuditUpdate, AuditSoftDelete } from './audit.js';
2
1
  export { auditUpdate, auditSoftDelete } from './audit.js';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { auditUpdate, auditSoftDelete } from './audit.js';
2
- //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@selvajs/platform",
3
- "version": "0.21.0-beta.3",
4
- "description": "Pure TypeScript interfaces for Selva platform providers (auth, definitions, compute)",
3
+ "version": "1.0.0-beta.4",
4
+ "description": "Interfaces, access rules and conformance suites for Selva platform providers",
5
5
  "license": "MIT",
6
6
  "author": "VektorNode",
7
7
  "homepage": "https://selva.dev",
@@ -25,6 +25,7 @@
25
25
  "node": ">=24.0.0"
26
26
  },
27
27
  "type": "module",
28
+ "sideEffects": false,
28
29
  "main": "./dist/index.js",
29
30
  "types": "./dist/index.d.ts",
30
31
  "exports": {
@@ -131,8 +132,8 @@
131
132
  "!**/*.spec.*"
132
133
  ],
133
134
  "dependencies": {
134
- "zod": "^4.6.5",
135
- "@selvajs/schemas": "^5.3.0-beta.0"
135
+ "zod": "^4.6.7",
136
+ "@selvajs/schemas": "^5.3.0-beta.1"
136
137
  },
137
138
  "peerDependencies": {
138
139
  "sharp": "^0.34.0 || ^0.35.0",
@@ -147,9 +148,9 @@
147
148
  }
148
149
  },
149
150
  "devDependencies": {
150
- "sharp": "^0.35.4",
151
+ "sharp": "^0.35.5",
151
152
  "typescript": "~6.0.3",
152
- "vitest": "^5.0.2",
153
+ "vitest": "^5.0.5",
153
154
  "@selvajs/config": "0.0.4"
154
155
  },
155
156
  "scripts": {