@astrale-domains/shell 0.1.6 → 0.1.8

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 (309) hide show
  1. package/README.md +73 -173
  2. package/dist/domain.d.ts +499 -31
  3. package/dist/domain.js +99 -78
  4. package/dist/functions/application/index.js +2 -1
  5. package/dist/functions/application/legacy/index.js +1 -0
  6. package/dist/functions/application/legacy/list-entrypoints.js +8 -0
  7. package/dist/functions/authority/capabilities.js +1 -0
  8. package/dist/functions/authority/composition.js +47 -0
  9. package/dist/functions/authority/index.js +2 -0
  10. package/dist/functions/authority/roles.js +113 -0
  11. package/dist/functions/directory/disconnect-directory.js +35 -0
  12. package/dist/functions/directory/index.js +1 -0
  13. package/dist/functions/group/assign-member.js +7 -0
  14. package/dist/functions/group/change-group-membership.js +101 -20
  15. package/dist/functions/group/create-group.js +23 -0
  16. package/dist/functions/group/delete-group.js +33 -0
  17. package/dist/functions/group/deprovision-group.js +27 -0
  18. package/dist/functions/group/group-errors.js +43 -0
  19. package/dist/functions/group/group-membership-errors.js +3 -1
  20. package/dist/functions/group/group.js +51 -0
  21. package/dist/functions/group/index.js +8 -2
  22. package/dist/functions/group/legacy/index.js +1 -0
  23. package/dist/functions/group/legacy/membership.js +64 -0
  24. package/dist/functions/group/membership-laws.js +37 -0
  25. package/dist/functions/group/provision-group.js +33 -0
  26. package/dist/functions/group/unassign-member.js +7 -0
  27. package/dist/functions/group/update-group.js +55 -0
  28. package/dist/functions/invitation/index.js +2 -0
  29. package/dist/functions/invitation/invitation-errors.js +13 -0
  30. package/dist/functions/invitation/invitation-methods.js +41 -0
  31. package/dist/functions/invitation/send-invitation.js +131 -0
  32. package/dist/functions/user/account.js +35 -0
  33. package/dist/functions/user/create-user.js +45 -0
  34. package/dist/functions/user/deactivate-user.js +19 -0
  35. package/dist/functions/user/delete-user.js +34 -0
  36. package/dist/functions/user/deprovision-user.js +21 -0
  37. package/dist/functions/user/index.js +15 -1
  38. package/dist/functions/user/invite-errors.js +18 -0
  39. package/dist/functions/user/invite-user.js +42 -34
  40. package/dist/functions/user/legacy/claimed-account.js +11 -0
  41. package/dist/functions/user/legacy/index.js +4 -0
  42. package/dist/functions/user/legacy/invite.js +20 -0
  43. package/dist/functions/user/legacy/migrate-accounts.js +98 -0
  44. package/dist/functions/user/legacy/record-profile.js +19 -0
  45. package/dist/functions/user/protect-administrators.js +16 -0
  46. package/dist/functions/user/provision-user.js +70 -0
  47. package/dist/functions/user/reactivate-user.js +19 -0
  48. package/dist/functions/user/read-account.js +12 -0
  49. package/dist/functions/user/request-invitation.js +17 -0
  50. package/dist/functions/user/update-profile.js +22 -0
  51. package/dist/functions/user/update-user.js +68 -0
  52. package/dist/functions/user/user-errors.js +69 -0
  53. package/dist/functions/user/write-account.js +57 -0
  54. package/dist/functions/view-access/index.js +2 -1
  55. package/dist/functions/view-access/{assign-views.js → legacy/assign-views.js} +11 -7
  56. package/dist/functions/view-access/legacy/index.js +1 -0
  57. package/dist/functions/view-access/{view-access-errors.js → legacy/view-access-errors.js} +5 -1
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.js +3 -1
  60. package/dist/integrations/user-invitation/invite-user-request.d.ts +6 -0
  61. package/dist/legacy/index.js +1 -0
  62. package/dist/legacy/release.js +33 -0
  63. package/dist/mutations/composition/grant-role.js +13 -0
  64. package/dist/mutations/composition/index.js +1 -0
  65. package/dist/mutations/group/index.js +1 -1
  66. package/dist/mutations/group/legacy/index.js +1 -0
  67. package/dist/mutations/group/legacy/membership-class.js +7 -0
  68. package/dist/mutations/group/set-group-membership.js +60 -19
  69. package/dist/mutations/identity/create-profile.js +37 -0
  70. package/dist/mutations/identity/delete-identity.js +28 -0
  71. package/dist/mutations/identity/index.js +4 -0
  72. package/dist/mutations/identity/write-account.js +109 -0
  73. package/dist/mutations/identity/write-group.js +72 -0
  74. package/dist/mutations/invitation/index.js +1 -0
  75. package/dist/mutations/invitation/write-invitation.js +79 -0
  76. package/dist/mutations/user/legacy/index.js +1 -0
  77. package/dist/mutations/user/{record-user-profile.js → legacy/record-user-profile.js} +5 -2
  78. package/dist/providers/admin-enrollment/create-admin-enrollment-provider.js +6 -3
  79. package/dist/providers/admin-enrollment/legacy/index.js +1 -0
  80. package/dist/providers/admin-enrollment/legacy/send-without-profile.js +16 -0
  81. package/dist/queries/application/legacy/index.js +1 -0
  82. package/dist/queries/application/legacy/read-installed-entrypoints.js +47 -0
  83. package/dist/queries/composition/index.js +2 -0
  84. package/dist/queries/composition/read-extensions.js +34 -0
  85. package/dist/queries/composition/read-role-nodes.js +76 -0
  86. package/dist/queries/group/index.js +2 -1
  87. package/dist/queries/group/legacy/index.js +3 -0
  88. package/dist/queries/group/legacy/read-core-groups.js +46 -0
  89. package/dist/queries/group/legacy/read-member-of-group.js +24 -0
  90. package/dist/queries/group/legacy/read-members-of-group.js +27 -0
  91. package/dist/queries/group/read-group-members.js +73 -0
  92. package/dist/queries/group/read-group-membership-endpoints.js +11 -0
  93. package/dist/queries/group/read-group-membership.js +6 -21
  94. package/dist/queries/identity/index.js +1 -0
  95. package/dist/queries/identity/read-identities.js +29 -0
  96. package/dist/queries/invitation/index.js +1 -0
  97. package/dist/queries/invitation/read-invitation.js +50 -0
  98. package/dist/queries/profile/index.js +1 -0
  99. package/dist/queries/profile/read-profile.js +15 -0
  100. package/dist/queries/role/index.js +1 -0
  101. package/dist/queries/role/read-role-node.js +21 -0
  102. package/dist/queries/user/legacy/index.js +1 -0
  103. package/dist/queries/user/legacy/read-user-profile.js +27 -0
  104. package/dist/runtime.d.ts +20147 -466
  105. package/dist/runtime.js +34 -7
  106. package/dist/schema/index.d.ts +7 -4
  107. package/dist/schema/index.js +8 -4
  108. package/dist/schema/legacy/index.js +1 -0
  109. package/dist/schema/legacy/schema.js +32 -0
  110. package/dist/schema/modules/application/legacy/entrypoint-entry.js +11 -0
  111. package/dist/schema/modules/application/legacy/functions/index.js +1 -0
  112. package/dist/schema/modules/application/legacy/functions/list-entrypoints.js +22 -0
  113. package/dist/schema/modules/application/legacy/index.js +2 -0
  114. package/dist/schema/modules/directory/functions/disconnect-directory.d.ts +13 -0
  115. package/dist/schema/modules/directory/functions/disconnect-directory.js +15 -0
  116. package/dist/schema/modules/directory/functions/index.d.ts +1 -0
  117. package/dist/schema/modules/directory/functions/index.js +1 -0
  118. package/dist/schema/modules/directory/index.d.ts +1 -0
  119. package/dist/schema/modules/directory/index.js +1 -0
  120. package/dist/schema/modules/group/classes/group-profile-link.d.ts +34 -0
  121. package/dist/schema/modules/group/classes/group-profile-link.js +21 -0
  122. package/dist/schema/modules/group/classes/group-profile.d.ts +15 -0
  123. package/dist/schema/modules/group/classes/group-profile.js +20 -0
  124. package/dist/schema/modules/group/classes/group.d.ts +87 -1
  125. package/dist/schema/modules/group/classes/group.js +38 -7
  126. package/dist/schema/modules/group/classes/index.d.ts +2 -1
  127. package/dist/schema/modules/group/classes/index.js +3 -1
  128. package/dist/schema/modules/group/functions/assign-member.js +17 -0
  129. package/dist/schema/modules/group/functions/assign-members.js +17 -0
  130. package/dist/schema/modules/group/functions/create-group.js +20 -0
  131. package/dist/schema/modules/group/functions/delete-group.js +12 -0
  132. package/dist/schema/modules/group/functions/deprovision-group.js +14 -0
  133. package/dist/schema/modules/group/functions/index.js +9 -2
  134. package/dist/schema/modules/group/functions/provision-group.js +14 -0
  135. package/dist/schema/modules/group/functions/unassign-member.js +17 -0
  136. package/dist/schema/modules/group/functions/unassign-members.js +17 -0
  137. package/dist/schema/modules/group/functions/update-group.js +20 -0
  138. package/dist/schema/modules/group/index.d.ts +3 -3
  139. package/dist/schema/modules/group/index.js +4 -3
  140. package/dist/schema/modules/group/legacy/admission.d.ts +9 -0
  141. package/dist/schema/modules/group/legacy/admission.js +9 -0
  142. package/dist/schema/modules/group/legacy/classes/index.d.ts +1 -0
  143. package/dist/schema/modules/group/legacy/classes/index.js +1 -0
  144. package/dist/schema/modules/group/{classes → legacy/classes}/member-of-group.d.ts +10 -1
  145. package/dist/schema/modules/group/legacy/classes/member-of-group.js +32 -0
  146. package/dist/schema/modules/group/legacy/core-groups.d.ts +22 -0
  147. package/dist/schema/modules/group/legacy/core-groups.js +28 -0
  148. package/dist/schema/modules/group/legacy/index.d.ts +5 -0
  149. package/dist/schema/modules/group/legacy/index.js +6 -0
  150. package/dist/schema/modules/group/legacy/policies/belongs-to-group.d.ts +5 -0
  151. package/dist/schema/modules/group/legacy/policies/belongs-to-group.js +9 -0
  152. package/dist/schema/modules/group/legacy/policies/index.d.ts +2 -0
  153. package/dist/schema/modules/group/legacy/policies/index.js +2 -0
  154. package/dist/schema/modules/group/legacy/policies/owns-membership-group.d.ts +6 -0
  155. package/dist/schema/modules/group/legacy/policies/owns-membership-group.js +12 -0
  156. package/dist/schema/modules/group/legacy/relaxations.d.ts +21 -0
  157. package/dist/schema/modules/group/legacy/relaxations.js +27 -0
  158. package/dist/schema/modules/group/policies/describes-group-of-subject.d.ts +2 -0
  159. package/dist/schema/modules/group/policies/describes-group-of-subject.js +7 -0
  160. package/dist/schema/modules/group/policies/index.d.ts +2 -1
  161. package/dist/schema/modules/group/policies/index.js +2 -1
  162. package/dist/schema/modules/group/policies/is-group-profile-source.d.ts +2 -0
  163. package/dist/schema/modules/group/policies/is-group-profile-source.js +6 -0
  164. package/dist/schema/modules/group/types/group-membership-input.js +8 -0
  165. package/dist/schema/modules/group/types/group.js +10 -0
  166. package/dist/schema/modules/group/types/index.js +2 -1
  167. package/dist/schema/modules/invitation/classes/index.js +2 -0
  168. package/dist/schema/modules/invitation/classes/invitation-user.js +17 -0
  169. package/dist/schema/modules/invitation/classes/invitation.js +32 -0
  170. package/dist/schema/modules/invitation/functions/accept.js +16 -0
  171. package/dist/schema/modules/invitation/functions/cancel.js +12 -0
  172. package/dist/schema/modules/invitation/functions/index.js +4 -0
  173. package/dist/schema/modules/invitation/functions/resend.js +12 -0
  174. package/dist/schema/modules/invitation/functions/send.js +20 -0
  175. package/dist/schema/modules/invitation/index.js +2 -0
  176. package/dist/schema/modules/invitation/types/index.js +1 -0
  177. package/dist/schema/modules/invitation/types/invitation-status.js +5 -0
  178. package/dist/schema/modules/namespace/classes/alias.d.ts +2 -1
  179. package/dist/schema/modules/namespace/classes/alias.js +11 -2
  180. package/dist/schema/modules/namespace/functions/link.js +3 -2
  181. package/dist/schema/modules/namespace/functions/rename.js +3 -2
  182. package/dist/schema/modules/namespace/functions/unlink.js +3 -2
  183. package/dist/schema/modules/role/classes/index.d.ts +1 -0
  184. package/dist/schema/modules/role/classes/index.js +1 -0
  185. package/dist/schema/modules/role/classes/role.d.ts +16 -0
  186. package/dist/schema/modules/role/classes/role.js +19 -0
  187. package/dist/schema/modules/role/core/index.d.ts +1 -0
  188. package/dist/schema/modules/role/core/index.js +1 -0
  189. package/dist/schema/modules/role/core/roles.d.ts +39 -0
  190. package/dist/schema/modules/role/core/roles.js +31 -0
  191. package/dist/schema/modules/role/index.d.ts +2 -0
  192. package/dist/schema/modules/role/index.js +2 -0
  193. package/dist/schema/modules/user/classes/index.d.ts +2 -0
  194. package/dist/schema/modules/user/classes/index.js +3 -0
  195. package/dist/schema/modules/user/classes/user-profile-link.d.ts +35 -0
  196. package/dist/schema/modules/user/classes/user-profile-link.js +22 -0
  197. package/dist/schema/modules/user/classes/user-profile.d.ts +41 -0
  198. package/dist/schema/modules/user/classes/user-profile.js +30 -0
  199. package/dist/schema/modules/user/classes/user.d.ts +133 -8
  200. package/dist/schema/modules/user/classes/user.js +46 -15
  201. package/dist/schema/modules/user/functions/create-user.js +21 -0
  202. package/dist/schema/modules/user/functions/deactivate-user.js +12 -0
  203. package/dist/schema/modules/user/functions/delete-user.js +12 -0
  204. package/dist/schema/modules/user/functions/deprovision-user.js +14 -0
  205. package/dist/schema/modules/user/functions/index.js +8 -1
  206. package/dist/schema/modules/user/functions/invite-user.js +23 -4
  207. package/dist/schema/modules/user/functions/provision-user.js +25 -0
  208. package/dist/schema/modules/user/functions/reactivate-user.js +12 -0
  209. package/dist/schema/modules/user/functions/update-profile.js +16 -0
  210. package/dist/schema/modules/user/functions/update-user.js +22 -0
  211. package/dist/schema/modules/user/index.d.ts +2 -1
  212. package/dist/schema/modules/user/index.js +2 -1
  213. package/dist/schema/modules/user/legacy/account-properties.js +24 -0
  214. package/dist/schema/modules/user/legacy/functions/index.js +2 -0
  215. package/dist/schema/modules/user/legacy/functions/migrate-accounts.js +23 -0
  216. package/dist/schema/modules/user/legacy/functions/record-profile.js +18 -0
  217. package/dist/schema/modules/user/legacy/index.js +5 -0
  218. package/dist/schema/modules/user/legacy/invite.js +15 -0
  219. package/dist/schema/modules/user/legacy/relaxations.js +37 -0
  220. package/dist/schema/modules/user/policies/describes-subject.d.ts +2 -0
  221. package/dist/schema/modules/user/policies/describes-subject.js +7 -0
  222. package/dist/schema/modules/user/policies/index.d.ts +2 -0
  223. package/dist/schema/modules/user/policies/index.js +2 -0
  224. package/dist/schema/modules/user/policies/is-profile-source.d.ts +2 -0
  225. package/dist/schema/modules/user/policies/is-profile-source.js +6 -0
  226. package/dist/schema/modules/user/types/account.d.ts +12 -0
  227. package/dist/schema/modules/user/types/account.js +12 -0
  228. package/dist/schema/modules/user/types/index.d.ts +1 -1
  229. package/dist/schema/modules/user/types/index.js +1 -1
  230. package/dist/schema/modules/view-access/classes/view-access.d.ts +7 -11
  231. package/dist/schema/modules/view-access/classes/view-access.js +17 -12
  232. package/dist/schema/modules/view-access/index.d.ts +1 -2
  233. package/dist/schema/modules/view-access/index.js +1 -2
  234. package/dist/schema/modules/view-access/legacy/functions/assign-views.js +22 -0
  235. package/dist/schema/modules/view-access/legacy/functions/index.js +1 -0
  236. package/dist/schema/modules/view-access/legacy/index.js +2 -0
  237. package/dist/schema/modules/view-access/legacy/view-assignment.js +24 -0
  238. package/dist/schema/modules/view-access/policies/holds-view-access.d.ts +2 -0
  239. package/dist/schema/modules/view-access/policies/holds-view-access.js +5 -0
  240. package/dist/schema/modules/view-access/policies/index.d.ts +1 -0
  241. package/dist/schema/modules/view-access/policies/index.js +1 -0
  242. package/dist/schema/policies/has-role.d.ts +6 -0
  243. package/dist/schema/policies/has-role.js +9 -0
  244. package/dist/schema/policies/index.d.ts +1 -0
  245. package/dist/schema/policies/index.js +1 -4
  246. package/dist/schema/policies/legacy/administers-shell-class.js +21 -0
  247. package/dist/schema/policies/legacy/index.js +4 -0
  248. package/dist/schema/policies/legacy/is-self.js +9 -0
  249. package/dist/schema/policies/legacy/owns-class.js +12 -0
  250. package/dist/schema/policies/legacy/uses-function.js +9 -0
  251. package/dist/schema/schema.d.ts +1937 -119
  252. package/dist/schema/schema.js +35 -15
  253. package/dist/schema/types/attributes.js +15 -0
  254. package/dist/schema/types/index.js +1 -0
  255. package/dist/utils/attributes/attributes.js +12 -0
  256. package/dist/utils/attributes/index.js +1 -0
  257. package/dist/utils/index.js +3 -0
  258. package/dist/utils/same-json/index.js +1 -0
  259. package/dist/utils/same-json/same-json.js +9 -0
  260. package/package.json +3 -3
  261. package/dist/functions/application/list-entrypoints.js +0 -6
  262. package/dist/functions/group/assign-member-to-group.js +0 -4
  263. package/dist/functions/group/unassign-member-from-group.js +0 -4
  264. package/dist/functions/user/record-profile.js +0 -18
  265. package/dist/mutations/user/index.js +0 -1
  266. package/dist/queries/application/index.js +0 -1
  267. package/dist/queries/application/read-installed-entrypoints.js +0 -28
  268. package/dist/queries/user/index.js +0 -1
  269. package/dist/queries/user/read-user-profile.js +0 -9
  270. package/dist/schema/modules/application/functions/index.d.ts +0 -1
  271. package/dist/schema/modules/application/functions/index.js +0 -1
  272. package/dist/schema/modules/application/functions/list-entrypoints.d.ts +0 -15
  273. package/dist/schema/modules/application/functions/list-entrypoints.js +0 -16
  274. package/dist/schema/modules/application/index.d.ts +0 -2
  275. package/dist/schema/modules/application/index.js +0 -2
  276. package/dist/schema/modules/application/types/entrypoint-entry.d.ts +0 -8
  277. package/dist/schema/modules/application/types/entrypoint-entry.js +0 -7
  278. package/dist/schema/modules/application/types/index.d.ts +0 -1
  279. package/dist/schema/modules/application/types/index.js +0 -1
  280. package/dist/schema/modules/group/classes/member-of-group.js +0 -12
  281. package/dist/schema/modules/group/core/administrators-group.d.ts +0 -1
  282. package/dist/schema/modules/group/core/administrators-group.js +0 -6
  283. package/dist/schema/modules/group/core/applications-group.d.ts +0 -1
  284. package/dist/schema/modules/group/core/applications-group.js +0 -6
  285. package/dist/schema/modules/group/core/index.d.ts +0 -3
  286. package/dist/schema/modules/group/core/index.js +0 -3
  287. package/dist/schema/modules/group/core/members-group.d.ts +0 -1
  288. package/dist/schema/modules/group/core/members-group.js +0 -6
  289. package/dist/schema/modules/group/functions/assign-member-to-group.js +0 -11
  290. package/dist/schema/modules/group/functions/unassign-member-from-group.js +0 -11
  291. package/dist/schema/modules/group/policies/belongs-to-group.d.ts +0 -1
  292. package/dist/schema/modules/group/policies/belongs-to-group.js +0 -5
  293. package/dist/schema/modules/user/functions/record-profile.js +0 -14
  294. package/dist/schema/modules/user/types/user-profile.d.ts +0 -7
  295. package/dist/schema/modules/user/types/user-profile.js +0 -7
  296. package/dist/schema/modules/view-access/core/index.d.ts +0 -1
  297. package/dist/schema/modules/view-access/core/index.js +0 -1
  298. package/dist/schema/modules/view-access/core/view-only-group.d.ts +0 -5
  299. package/dist/schema/modules/view-access/core/view-only-group.js +0 -10
  300. package/dist/schema/modules/view-access/functions/assign-views.d.ts +0 -16
  301. package/dist/schema/modules/view-access/functions/assign-views.js +0 -17
  302. package/dist/schema/modules/view-access/functions/index.d.ts +0 -1
  303. package/dist/schema/modules/view-access/functions/index.js +0 -1
  304. package/dist/schema/modules/view-access/types/index.js +0 -1
  305. package/dist/schema/modules/view-access/types/view-assignment.js +0 -16
  306. package/dist/schema/policies/administers-shell-class.js +0 -11
  307. package/dist/schema/policies/is-self.js +0 -5
  308. package/dist/schema/policies/owns-class.js +0 -7
  309. package/dist/schema/policies/uses-function.js +0 -5
@@ -0,0 +1,27 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { readMembers } from '../authority/index.js';
3
+ import { deleteIdentity } from '../../mutations/identity/index.js';
4
+ import { readProfile } from '../../queries/profile/index.js';
5
+ import { groupErrors } from './group-errors.js';
6
+ import { findGroup } from './group.js';
7
+ /**
8
+ * The directory deleted the group (`dsync.group.deleted`): its memberships leave and it is deleted
9
+ * (G4). A group the directory no longer has is left as it is (A3).
10
+ */
11
+ export const deprovision = defineAction()('Group.deprovision', { errors: groupErrors }, async (context) => {
12
+ const { input, error } = context;
13
+ const group = await findGroup(context, input);
14
+ if (group === undefined)
15
+ return {};
16
+ const members = await readMembers(context, group.id);
17
+ const profile = await context.query(readProfile, { kind: 'Group', node: group.id });
18
+ const result = await context.mutate(deleteIdentity, {
19
+ kind: 'Group',
20
+ node: group.id,
21
+ members,
22
+ ...(profile === undefined ? {} : { profile }),
23
+ });
24
+ if (!result.ok)
25
+ throw error('GROUP_CHANGED', {});
26
+ return {};
27
+ });
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ const none = z.strictObject({});
3
+ /**
4
+ * The refusals of the group methods. The SDK declares no FORBIDDEN business family, so a law that
5
+ * forbids a change for the state of the group or its caller answers CONFLICT.
6
+ */
7
+ export const groupErrors = {
8
+ GROUP_NOT_FOUND: {
9
+ family: 'NOT_FOUND',
10
+ message: 'No group is at this path.',
11
+ details: none,
12
+ },
13
+ GROUP_ATTRIBUTES_TOO_MANY: {
14
+ family: 'VALIDATION_ERROR',
15
+ message: 'A group carries at most 64 attributes.',
16
+ details: z.strictObject({ max: z.number() }),
17
+ },
18
+ GROUP_DIRECTORY_MANAGED: {
19
+ family: 'CONFLICT',
20
+ message: "This group is managed by your organization's directory. Change it in the directory.",
21
+ details: none,
22
+ },
23
+ GROUP_NOT_DIRECTORY_MANAGED: {
24
+ family: 'CONFLICT',
25
+ message: 'This group is local: only group.writer changes its members.',
26
+ details: none,
27
+ },
28
+ GROUP_ROLE_PROTECTED: {
29
+ family: 'CONFLICT',
30
+ message: 'This group grants a role. Only an administrator may change its members.',
31
+ details: z.strictObject({ role: z.string() }),
32
+ },
33
+ GROUP_MEMBERSHIP_CYCLE: {
34
+ family: 'CONFLICT',
35
+ message: 'This member already holds the group: the membership would close a cycle.',
36
+ details: none,
37
+ },
38
+ GROUP_CHANGED: {
39
+ family: 'CONFLICT',
40
+ message: 'The group changed concurrently; retry the operation.',
41
+ details: none,
42
+ },
43
+ };
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { groupErrors } from './group-errors.js';
2
3
  export const groupMembershipErrors = {
4
+ ...groupErrors,
3
5
  GROUP_MEMBERSHIP_ENDPOINTS_UNAVAILABLE: {
4
6
  family: 'CONFLICT',
5
7
  message: 'Shell must be able to traverse both membership endpoints before confirming membership.',
@@ -7,7 +9,7 @@ export const groupMembershipErrors = {
7
9
  },
8
10
  INVALID_GROUP_MEMBERSHIP_REFERENCE: {
9
11
  family: 'VALIDATION_ERROR',
10
- message: 'Member and group must be exact canonical @NodeId paths.',
12
+ message: 'Members and group must be exact canonical @NodeId paths of accounts or groups.',
11
13
  details: z.strictObject({}),
12
14
  },
13
15
  GROUP_MEMBERSHIP_CONFLICT: {
@@ -0,0 +1,51 @@
1
+ import { GROUP_PROFILE_PROPERTIES, groupProfileValues, } from '../../mutations/identity/index.js';
2
+ import { readIdentities } from '../../queries/identity/index.js';
3
+ import { readProfile } from '../../queries/profile/index.js';
4
+ import { toAttributes } from '../../utils/index.js';
5
+ const text = (value) => (typeof value === 'string' ? value : undefined);
6
+ /** One group Node as Shell's laws read it, by the Property keys of the resolved Domain. */
7
+ export function toGroup(domain, node) {
8
+ const properties = domain.classes.Group.properties;
9
+ const value = (name) => node.props[properties[name].key];
10
+ const read = (name) => text(value(name));
11
+ const values = {
12
+ name: read('name'),
13
+ description: read('description'),
14
+ icon: read('icon'),
15
+ directory: read('directory'),
16
+ externalId: read('externalId'),
17
+ attributes: toAttributes(value('attributes')),
18
+ };
19
+ return {
20
+ id: node.id,
21
+ class: node.class,
22
+ ...Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined)),
23
+ };
24
+ }
25
+ export async function readGroup({ query, domain }, group) {
26
+ const [node] = await query(readIdentities, { kind: 'Group', ids: [group] });
27
+ return node === undefined ? undefined : toGroup(domain, node);
28
+ }
29
+ export async function findGroup({ query, domain }, where) {
30
+ const [node] = await query(readIdentities, { kind: 'Group', where });
31
+ return node === undefined ? undefined : toGroup(domain, node);
32
+ }
33
+ /** A group is provided while it carries its directory (G2). */
34
+ export const isProvidedGroup = (group) => group.directory !== undefined;
35
+ /**
36
+ * The profile a group change writes: its profile Node when the change touches a shared value, or,
37
+ * for a group stored before profiles, the whole profile to create; undefined when nothing shared
38
+ * changes.
39
+ */
40
+ export async function groupProfileTarget({ query }, group, set, unset) {
41
+ const shares = GROUP_PROFILE_PROPERTIES.some((key) => key in set || unset.includes(key));
42
+ if (!shares)
43
+ return undefined;
44
+ const profile = await query(readProfile, { kind: 'Group', node: group.id });
45
+ if (profile !== undefined)
46
+ return { id: profile };
47
+ const merged = { ...group, ...set };
48
+ for (const key of unset)
49
+ delete merged[key];
50
+ return { create: groupProfileValues(merged) };
51
+ }
@@ -1,2 +1,8 @@
1
- export { assignMember } from './assign-member-to-group.js';
2
- export { unassignMember } from './unassign-member-from-group.js';
1
+ export { create } from './create-group.js';
2
+ export { update } from './update-group.js';
3
+ export { remove } from './delete-group.js';
4
+ export { assignMember, assignMembers } from './assign-member.js';
5
+ export { unassignMember, unassignMembers } from './unassign-member.js';
6
+ export { provision } from './provision-group.js';
7
+ export { deprovision } from './deprovision-group.js';
8
+ export { toGroup } from './group.js';
@@ -0,0 +1 @@
1
+ export { legacyChangeForeignMembership, legacyMembersEdges } from './membership.js';
@@ -0,0 +1,64 @@
1
+ import { setGroupMembership } from '../../../mutations/group/index.js';
2
+ import { readGroupMembership, readGroupMembershipEndpoints, } from '../../../queries/group/index.js';
3
+ import { legacyReadCoreGroups, legacyReadMemberOfGroup, legacyReadMembersOfGroup, } from '../../../queries/group/legacy/index.js';
4
+ /**
5
+ * @deprecated The legacy `member_of_group` Edges a change of a Shell group's members also converges
6
+ * (L-SHELL-8), read in one go for all the members: joining one of the legacy Core groups writes it
7
+ * beside `extends_with`, for the readers that still check it; leaving any group removes it.
8
+ * Replaced by `extends_with` alone. Removal: Shell major (delivery step 5) after 1Pact step 4.
9
+ */
10
+ export async function legacyMembersEdges(context, selection, present) {
11
+ const [observed, legacyGroups] = await Promise.all([
12
+ context.query(legacyReadMembersOfGroup, selection),
13
+ present ? context.query(legacyReadCoreGroups, {}) : undefined,
14
+ ]);
15
+ const core = legacyGroups !== undefined && Object.values(legacyGroups).includes(selection.group);
16
+ return {
17
+ present: observed.present,
18
+ guard: observed.guard,
19
+ /** Whether a member is to keep `member_of_group`: never when it leaves the group. */
20
+ wanted: (member) => present && (core || observed.present.has(member)),
21
+ };
22
+ }
23
+ /**
24
+ * @deprecated A Domain that extends Group changes the members of its own groups (L-SHELL-9): Shell
25
+ * writes both legacy Edges as before, but under that Domain's authority, since the release no
26
+ * longer grants Shell any grant authority over foreign Identities (AM5). Shell's group laws
27
+ * (G2, G3) do not apply to another Domain's Identity. Replaced by the Domain's own Identity and
28
+ * Roles (P5, P6). Removal: Shell major (delivery step 5) after 1Pact step 4.
29
+ */
30
+ export async function legacyChangeForeignMembership(context, membership, present) {
31
+ const caller = context.graph.caller;
32
+ if (caller === undefined)
33
+ throw context.error('GROUP_MEMBERSHIP_ENDPOINTS_UNAVAILABLE', {});
34
+ const observe = async () => {
35
+ const endpoints = await caller.query(readGroupMembershipEndpoints, membership);
36
+ if (!endpoints.visible)
37
+ throw context.error('GROUP_MEMBERSHIP_ENDPOINTS_UNAVAILABLE', {});
38
+ const [authority, legacy] = await Promise.all([
39
+ caller.query(readGroupMembership, membership),
40
+ caller.query(legacyReadMemberOfGroup, membership),
41
+ ]);
42
+ const edges = [
43
+ { edge: 'authority', current: authority.present, present, guard: authority.guard },
44
+ { edge: 'legacy', current: legacy.present, present, guard: legacy.guard },
45
+ ];
46
+ return { endpoints: endpoints.guard, edges };
47
+ };
48
+ const matches = (edges) => edges.every(({ current, present: wanted }) => current === wanted);
49
+ const current = await observe();
50
+ if (matches(current.edges))
51
+ return {};
52
+ const result = await caller.mutate(setGroupMembership, { ...membership, ...current });
53
+ if (result.ok)
54
+ return {};
55
+ if (result.outcomeKnown) {
56
+ if (result.rule === 'cycle')
57
+ throw context.error('GROUP_MEMBERSHIP_CYCLE', {});
58
+ throw context.error('GROUP_MEMBERSHIP_CONFLICT', { cause: result.code });
59
+ }
60
+ const recovered = await observe();
61
+ if (matches(recovered.edges))
62
+ return {};
63
+ throw context.error('GROUP_MEMBERSHIP_UNCONFIRMED', { cause: result.code, outcomeKnown: false });
64
+ }
@@ -0,0 +1,37 @@
1
+ import { authority } from '../authority/index.js';
2
+ import { isProvidedGroup } from './group.js';
3
+ /**
4
+ * G2 and G3 for one change of a Shell group's members. A provided group changes its members only
5
+ * through its directory, a local one through `group.writer`; a group that holds a Role, directly or
6
+ * through what it extends, only by an admin or its directory. A member that would join a group it
7
+ * already holds is refused by the Kernel, which keeps `extends_with` acyclic.
8
+ */
9
+ export async function membershipRefusal(capabilities, group) {
10
+ const authorities = authority(capabilities);
11
+ const provided = isProvidedGroup(group);
12
+ if (provided) {
13
+ if (!(await authorities.callerHolds('directory.provisioner'))) {
14
+ return { code: 'GROUP_DIRECTORY_MANAGED' };
15
+ }
16
+ }
17
+ else if (!(await authorities.callerHolds('group.writer'))) {
18
+ return { code: 'GROUP_NOT_DIRECTORY_MANAGED' };
19
+ }
20
+ if (!provided) {
21
+ const role = await roleProtected(capabilities, group);
22
+ if (role !== undefined)
23
+ return { code: 'GROUP_ROLE_PROTECTED', role };
24
+ }
25
+ return undefined;
26
+ }
27
+ /**
28
+ * G3: the Role a group holds, when the caller is no admin and so may not change who holds it,
29
+ * through its members or by deleting it.
30
+ */
31
+ export async function roleProtected(capabilities, group) {
32
+ const authorities = authority(capabilities);
33
+ const role = await authorities.heldRole(group.id);
34
+ if (role === undefined || (await authorities.callerHolds('workspace.admin')))
35
+ return undefined;
36
+ return role;
37
+ }
@@ -0,0 +1,33 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { Path } from '@astrale-os/sdk/graph/path';
3
+ import { createGroup, updateGroup } from '../../mutations/identity/index.js';
4
+ import { groupErrors } from './group-errors.js';
5
+ import { findGroup, groupProfileTarget } from './group.js';
6
+ /**
7
+ * Create or rename the group a directory provides (`dsync.group.created`, `dsync.group.updated`).
8
+ * Replaying an event writes nothing (A3).
9
+ */
10
+ export const provision = defineAction()('Group.provision', { errors: groupErrors }, async (context) => {
11
+ const { input, error } = context;
12
+ const { directory, externalId, name } = input;
13
+ const existing = await findGroup(context, { directory, externalId });
14
+ if (existing !== undefined) {
15
+ if (existing.name !== name) {
16
+ const profile = await groupProfileTarget(context, existing, { name }, []);
17
+ const result = await context.mutate(updateGroup, {
18
+ group: existing.id,
19
+ set: { name },
20
+ ...(profile === undefined ? {} : { profile }),
21
+ });
22
+ if (!result.ok)
23
+ throw error('GROUP_CHANGED', {});
24
+ }
25
+ return { group: Path.id(existing.id).raw, created: false };
26
+ }
27
+ const result = await context.mutate(createGroup, {
28
+ values: { name, directory, externalId },
29
+ });
30
+ if (!result.ok)
31
+ throw error('GROUP_CHANGED', {});
32
+ return { group: Path.id(result.group).raw, created: true };
33
+ });
@@ -0,0 +1,7 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { changeGroupMembers, changeGroupMembership } from './change-group-membership.js';
3
+ import { groupMembershipErrors } from './group-membership-errors.js';
4
+ /** Withdraw one account or group from a group, in one read and one write. */
5
+ export const unassignMember = defineAction()('Group.unassignMember', { errors: groupMembershipErrors }, (context) => changeGroupMembership(context, false));
6
+ /** Withdraw several accounts or groups from a group, in one commit. */
7
+ export const unassignMembers = defineAction()('Group.unassignMembers', { errors: groupMembershipErrors }, (context) => changeGroupMembers(context, context.input, false));
@@ -0,0 +1,55 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { updateGroup } from '../../mutations/identity/index.js';
3
+ import { MAX_ATTRIBUTES, boundedAttributes } from '../../utils/index.js';
4
+ import { sameJson } from '../../utils/index.js';
5
+ import { groupErrors } from './group-errors.js';
6
+ import { groupProfileTarget, isProvidedGroup, readGroup } from './group.js';
7
+ /**
8
+ * Rename, describe, or change the icon or the attributes of a group; a provided group's name is
9
+ * its directory's (G2), its attributes the instance's own (AM11).
10
+ */
11
+ export const update = defineAction()('Group.update', { errors: groupErrors }, async (context) => {
12
+ const { input, error, self } = context;
13
+ const group = await readGroup(context, self);
14
+ if (group === undefined)
15
+ throw error('GROUP_NOT_FOUND', {});
16
+ if (isProvidedGroup(group) && input.name !== undefined && input.name !== group.name) {
17
+ throw error('GROUP_DIRECTORY_MANAGED', {});
18
+ }
19
+ const set = {};
20
+ const unset = [];
21
+ for (const key of ['name', 'description', 'icon']) {
22
+ const value = input[key];
23
+ if (value === undefined)
24
+ continue;
25
+ if (value === null) {
26
+ if (group[key] !== undefined)
27
+ unset.push(key);
28
+ }
29
+ else if (value !== group[key])
30
+ set[key] = value;
31
+ }
32
+ if (input.attributes === null) {
33
+ if (group.attributes !== undefined)
34
+ unset.push('attributes');
35
+ }
36
+ else if (input.attributes !== undefined) {
37
+ const attributes = boundedAttributes(input.attributes);
38
+ if (attributes === undefined)
39
+ throw error('GROUP_ATTRIBUTES_TOO_MANY', { max: MAX_ATTRIBUTES });
40
+ if (!sameJson(attributes, group.attributes))
41
+ set.attributes = attributes;
42
+ }
43
+ if (Object.keys(set).length === 0 && unset.length === 0)
44
+ return {};
45
+ const profile = await groupProfileTarget(context, group, set, unset);
46
+ const result = await context.mutate(updateGroup, {
47
+ group: self,
48
+ set: set,
49
+ unset,
50
+ ...(profile === undefined ? {} : { profile }),
51
+ });
52
+ if (!result.ok)
53
+ throw error('GROUP_CHANGED', {});
54
+ return {};
55
+ });
@@ -0,0 +1,2 @@
1
+ export { send } from './send-invitation.js';
2
+ export { accept, cancel, resend } from './invitation-methods.js';
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export const invitationErrors = {
3
+ INVITATION_NOT_FOUND: {
4
+ family: 'NOT_FOUND',
5
+ message: 'No invitation the caller reads.',
6
+ details: z.strictObject({}),
7
+ },
8
+ INVITATION_NOT_PENDING: {
9
+ family: 'CONFLICT',
10
+ message: 'The invitation is no longer pending: invite the account again.',
11
+ details: z.strictObject({}),
12
+ },
13
+ };
@@ -0,0 +1,41 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { readInvitation } from '../../queries/invitation/index.js';
3
+ import { invitationErrors } from './invitation-errors.js';
4
+ import { invitationSignal } from './send-invitation.js';
5
+ /** The pending invitation a method acts on, or its refusal. */
6
+ function pending(invitation, error) {
7
+ if (invitation === undefined)
8
+ throw error('INVITATION_NOT_FOUND', {});
9
+ if (invitation.status !== 'pending')
10
+ throw error('INVITATION_NOT_PENDING', {});
11
+ return invitation;
12
+ }
13
+ /**
14
+ * Send the run of a pending invitation its next event. The Shell signals under its own Domain
15
+ * authority: the Kernel lets only the Domain that owns a run signal it.
16
+ */
17
+ async function signal(kernel, invitation, value) {
18
+ await kernel.execution.signal(invitation.execution, {
19
+ name: invitationSignal(invitation.round),
20
+ value,
21
+ });
22
+ }
23
+ /** Send the email of a pending invitation again: same invitation, same run. */
24
+ export const resend = defineAction()('Invitation.resend', { errors: invitationErrors }, async (context) => {
25
+ const invitation = pending(await context.query(readInvitation, { invitation: context.self }), context.error);
26
+ await signal(context.kernel, invitation, { kind: 'resend' });
27
+ return {};
28
+ });
29
+ /** Accept a pending invitation at the first sign-in: the run binds the account (U8). */
30
+ export const accept = defineAction()('Invitation.accept', { errors: invitationErrors }, async (context) => {
31
+ const invitation = pending(await context.query(readInvitation, { invitation: context.self }), context.error);
32
+ const { iss, sub } = context.input;
33
+ await signal(context.kernel, invitation, { kind: 'accepted', iss, sub });
34
+ return {};
35
+ });
36
+ /** Cancel a pending invitation: its run marks it cancelled. */
37
+ export const cancel = defineAction()('Invitation.cancel', { errors: invitationErrors }, async (context) => {
38
+ const invitation = pending(await context.query(readInvitation, { invitation: context.self }), context.error);
39
+ await context.kernel.execution.cancel(invitation.execution);
40
+ return {};
41
+ });
@@ -0,0 +1,131 @@
1
+ import { defineWorkflow } from '@astrale-os/sdk/workflow';
2
+ import { inviteErrors, readAccount, userErrors } from '../user/index.js';
3
+ import { InvitationRequestFailure } from '../../integrations/user-invitation/index.js';
4
+ import { bindAccount, createInvitation, updateInvitation } from '../../mutations/invitation/index.js';
5
+ import { readInvitation, readPendingInvitations } from '../../queries/invitation/index.js';
6
+ import { canonicalNodeId } from '../../utils/canonical-node-path/index.js';
7
+ const errors = { ...inviteErrors, ...userErrors };
8
+ /** How long an invitation waits for its first sign-in. */
9
+ export const INVITATION_TTL_MS = 7 * 24 * 3_600_000;
10
+ /** The signal name of one round: a name is delivered once, so each event moves to the next. */
11
+ export const invitationSignal = (round) => `event-${round}`;
12
+ /**
13
+ * Invite an account and keep its invitation until it ends: record it, have Admin send the email,
14
+ * then wait for the first sign-in (`accept`), a resend, a cancel or the expiry. It grants nothing:
15
+ * the account holds what `User.create` or an admin gave it.
16
+ *
17
+ * An account has at most one pending invitation. When one is pending for the same email, this run
18
+ * has it resent, with a full time to live again, and ends. For another email, it cancels it and
19
+ * records a new one; Admin replaces its own link when it receives the new request.
20
+ */
21
+ export const send = defineWorkflow()('Invitation.send', { errors }, async (context) => {
22
+ const { step, execution, awaitSignal, cancelled, error } = context;
23
+ const user = canonicalNodeId(context.input.user, () => error('INVALID_INVITATION_REFERENCE', {}));
24
+ const account = await step.run('read-account', ({ query }) => readAccount({ query, domain: context.domain }, user));
25
+ if (account === undefined)
26
+ throw error('USER_NOT_FOUND', {});
27
+ if (account.frozen)
28
+ throw error('USER_SUSPENDED', {});
29
+ if (account.email === undefined)
30
+ throw error('USER_EMAIL_INVALID', {});
31
+ const email = account.email;
32
+ // Stop the runs of replaced invitations: each finds its invitation closed and ends.
33
+ const stop = (runs) => step.run('stop-replaced', async ({ kernel }) => {
34
+ for (const { execution: ref } of runs) {
35
+ await kernel.execution.cancel(ref).catch(() => undefined);
36
+ }
37
+ });
38
+ const retire = async (invitations) => {
39
+ for (const [index, { id }] of invitations.entries()) {
40
+ await step.mutate(`retire-${index}`, updateInvitation, {
41
+ invitation: id,
42
+ status: 'cancelled',
43
+ });
44
+ }
45
+ await stop(invitations);
46
+ };
47
+ // Close the invitation, unless another run already closed it (a replacement).
48
+ const close = async (name, status) => {
49
+ const current = await step.run(`read-${name}`, ({ query }) => query(readInvitation, { invitation }));
50
+ if (current?.status !== 'pending')
51
+ return { status: 'cancelled' };
52
+ await step.mutate(name, updateInvitation, { invitation, status });
53
+ return { status };
54
+ };
55
+ const pending = await step.run('read-pending', ({ query }) => query(readPendingInvitations, { user }));
56
+ const same = pending.find((previous) => previous.email === email);
57
+ // Its run sends the email again and restarts the time to live. A run that no longer answers
58
+ // has its invitation replaced instead.
59
+ const resent = same !== undefined &&
60
+ (await step.run('resend-pending', ({ kernel }) => kernel.execution
61
+ .signal(same.execution, {
62
+ name: invitationSignal(same.round),
63
+ value: { kind: 'resend' },
64
+ })
65
+ .then(() => true, () => false)));
66
+ if (resent) {
67
+ const others = pending.filter((previous) => previous !== same);
68
+ if (others.length > 0)
69
+ await retire(others);
70
+ return { status: 'pending' };
71
+ }
72
+ const startedAt = await step.run('clock', () => Date.now());
73
+ let expiresAt = startedAt + INVITATION_TTL_MS;
74
+ const { invitation } = await step.mutate('record-invitation', createInvitation, {
75
+ user,
76
+ execution: execution.id,
77
+ expiresAt: new Date(expiresAt).toISOString(),
78
+ email,
79
+ replaces: pending.map((previous) => previous.id),
80
+ });
81
+ if (pending.length > 0)
82
+ await stop(pending);
83
+ const requestedBy = execution.initiator.caller ?? execution.initiator.principal;
84
+ const sendEmail = async (round) => {
85
+ try {
86
+ await step.run(`send-email-${round}`, ({ integrations: providers }) => providers.enrollment.invite({
87
+ requestId: `${execution.id}:${round}`,
88
+ user,
89
+ email,
90
+ requestedBy: String(requestedBy),
91
+ }));
92
+ }
93
+ catch (cause) {
94
+ if (cause instanceof InvitationRequestFailure) {
95
+ throw error(cause.code, { requestId: `${execution.id}:${round}` });
96
+ }
97
+ throw cause;
98
+ }
99
+ };
100
+ try {
101
+ await sendEmail(0);
102
+ for (let round = 0;; round += 1) {
103
+ const now = await step.run(`clock-${round}`, () => Date.now());
104
+ const event = await awaitSignal(invitationSignal(round), {
105
+ timeoutMs: Math.max(expiresAt - now, 1),
106
+ });
107
+ if (event.kind === 'timeout')
108
+ return await close('expire', 'expired');
109
+ if (event.value.kind === 'accepted') {
110
+ const { iss, sub } = event.value;
111
+ await step.mutate('bind-account', bindAccount, { user, iss, sub });
112
+ await step.mutate('accept', updateInvitation, { invitation, status: 'accepted' });
113
+ return { status: 'accepted' };
114
+ }
115
+ // A resend is a new link from Admin, with a full time to live again.
116
+ await sendEmail(round + 1);
117
+ const resentAt = await step.run(`resent-at-${round + 1}`, () => Date.now());
118
+ expiresAt = resentAt + INVITATION_TTL_MS;
119
+ await step.mutate(`next-round-${round + 1}`, updateInvitation, {
120
+ invitation,
121
+ round: round + 1,
122
+ expiresAt: new Date(expiresAt).toISOString(),
123
+ });
124
+ }
125
+ }
126
+ catch (cause) {
127
+ if (!cancelled(cause))
128
+ throw cause;
129
+ return await close('cancel', 'cancelled');
130
+ }
131
+ });
@@ -0,0 +1,35 @@
1
+ import { Path } from '@astrale-os/sdk/graph/path';
2
+ import { toAttributes } from '../../utils/index.js';
3
+ const text = (value) => (typeof value === 'string' ? value : undefined);
4
+ /** One account Node as Shell's laws read it, by the Property keys of the resolved Domain. */
5
+ export function toAccount(domain, node) {
6
+ const properties = domain.classes.User.properties;
7
+ const read = (name) => node.props[properties[name].key];
8
+ const values = {
9
+ name: text(read('name')),
10
+ email: text(read('email')),
11
+ givenName: text(read('givenName')),
12
+ familyName: text(read('familyName')),
13
+ icon: text(read('icon')),
14
+ directory: text(read('directory')),
15
+ externalId: text(read('externalId')),
16
+ attributes: toAttributes(read('attributes')),
17
+ frozen: typeof read('frozen') === 'boolean' ? read('frozen') : undefined,
18
+ };
19
+ return {
20
+ id: node.id,
21
+ class: node.class,
22
+ ...Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined)),
23
+ };
24
+ }
25
+ /** An account is provided while it carries its directory (U3). */
26
+ export const isProvided = (account) => account.directory !== undefined;
27
+ /** An email in the form Shell keeps it, lower case; undefined when it is not one. */
28
+ export function normalizeEmail(input) {
29
+ const email = input.trim().toLowerCase();
30
+ return email.length >= 3 && email.length <= 320 && /^[^\s@]+@[^\s@]+$/u.test(email)
31
+ ? email
32
+ : undefined;
33
+ }
34
+ /** A Node as a method returns it. */
35
+ export const nodeReference = (id) => Path.id(id).raw;
@@ -0,0 +1,45 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { createAccount } from '../../mutations/identity/index.js';
3
+ import { MAX_ATTRIBUTES, boundedAttributes } from '../../utils/index.js';
4
+ import { nodeReference, normalizeEmail } from './account.js';
5
+ // Seam L-SHELL-13: an email an account claimed before `email` existed is taken (U1).
6
+ import { legacyClaimedAccount } from './legacy/index.js';
7
+ import { findAccount } from './read-account.js';
8
+ import { userErrors } from './user-errors.js';
9
+ /**
10
+ * Idempotent on the email (U1): the account of the email comes back, `created: false`, whoever
11
+ * created it, invited or provided. A Domain needs no directory to know it (A5).
12
+ */
13
+ export const create = defineAction()('User.create', { errors: userErrors }, async (context) => {
14
+ const { input, error } = context;
15
+ const email = normalizeEmail(input.email);
16
+ if (email === undefined)
17
+ throw error('USER_EMAIL_INVALID', {});
18
+ const attributes = input.attributes === undefined ? undefined : boundedAttributes(input.attributes);
19
+ if (input.attributes !== undefined && attributes === undefined) {
20
+ throw error('USER_ATTRIBUTES_TOO_MANY', { max: MAX_ATTRIBUTES });
21
+ }
22
+ const existing = await findAccount(context, { email });
23
+ if (existing !== undefined)
24
+ return { user: nodeReference(existing.id), created: false };
25
+ const claimed = await legacyClaimedAccount(context, email);
26
+ if (claimed !== undefined)
27
+ throw error('USER_EMAIL_TAKEN', { user: nodeReference(claimed) });
28
+ const result = await context.mutate(createAccount, {
29
+ values: {
30
+ email,
31
+ name: input.name,
32
+ ...(input.givenName === undefined ? {} : { givenName: input.givenName }),
33
+ ...(input.familyName === undefined ? {} : { familyName: input.familyName }),
34
+ ...(attributes === undefined ? {} : { attributes }),
35
+ frozen: false,
36
+ },
37
+ });
38
+ if (result.ok)
39
+ return { user: nodeReference(result.user), created: true };
40
+ // Another call created the account of this email meanwhile: it is the one.
41
+ const winner = await findAccount(context, { email });
42
+ if (winner !== undefined)
43
+ return { user: nodeReference(winner.id), created: false };
44
+ throw error('ACCOUNT_CHANGED', {});
45
+ });
@@ -0,0 +1,19 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { protectAdministrators } from './protect-administrators.js';
3
+ import { readAccount } from './read-account.js';
4
+ import { userErrors } from './user-errors.js';
5
+ import { writeAccount } from './write-account.js';
6
+ /** `frozen: true`: the Kernel refuses the account's next sign-in (U4, U5). */
7
+ export const deactivate = defineAction()('User.deactivate', { errors: userErrors }, async (context) => {
8
+ const { error, self } = context;
9
+ const account = await readAccount(context, self);
10
+ if (account === undefined)
11
+ throw error('USER_NOT_FOUND', {});
12
+ const refusal = await protectAdministrators(context, self, { keepsLastAdministrator: true });
13
+ if (refusal !== undefined)
14
+ throw error(refusal, {});
15
+ const written = await writeAccount(context, account, { frozen: true });
16
+ if (written === 'CHANGED' || written === 'DUPLICATE_KEY')
17
+ throw error('ACCOUNT_CHANGED', {});
18
+ return {};
19
+ });