@astrale-domains/shell 0.1.5 → 0.1.7

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 (301) hide show
  1. package/README.md +73 -173
  2. package/dist/domain.d.ts +477 -31
  3. package/dist/domain.js +87 -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 +98 -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/change-group-membership.js +43 -9
  14. package/dist/functions/group/create-group.js +23 -0
  15. package/dist/functions/group/delete-group.js +33 -0
  16. package/dist/functions/group/deprovision-group.js +27 -0
  17. package/dist/functions/group/group-errors.js +43 -0
  18. package/dist/functions/group/group-membership-errors.js +2 -0
  19. package/dist/functions/group/group.js +51 -0
  20. package/dist/functions/group/index.js +8 -2
  21. package/dist/functions/group/legacy/index.js +1 -0
  22. package/dist/functions/group/legacy/membership.js +60 -0
  23. package/dist/functions/group/membership-laws.js +40 -0
  24. package/dist/functions/group/provision-group.js +33 -0
  25. package/dist/functions/group/update-group.js +55 -0
  26. package/dist/functions/invitation/index.js +2 -0
  27. package/dist/functions/invitation/invitation-errors.js +13 -0
  28. package/dist/functions/invitation/invitation-methods.js +41 -0
  29. package/dist/functions/invitation/send-invitation.js +81 -0
  30. package/dist/functions/user/account.js +35 -0
  31. package/dist/functions/user/create-user.js +45 -0
  32. package/dist/functions/user/deactivate-user.js +19 -0
  33. package/dist/functions/user/delete-user.js +34 -0
  34. package/dist/functions/user/deprovision-user.js +21 -0
  35. package/dist/functions/user/index.js +15 -1
  36. package/dist/functions/user/invite-errors.js +18 -0
  37. package/dist/functions/user/invite-user.js +42 -34
  38. package/dist/functions/user/legacy/claimed-account.js +11 -0
  39. package/dist/functions/user/legacy/index.js +4 -0
  40. package/dist/functions/user/legacy/invite.js +20 -0
  41. package/dist/functions/user/legacy/migrate-accounts.js +98 -0
  42. package/dist/functions/user/legacy/record-profile.js +19 -0
  43. package/dist/functions/user/protect-administrators.js +16 -0
  44. package/dist/functions/user/provision-user.js +70 -0
  45. package/dist/functions/user/reactivate-user.js +19 -0
  46. package/dist/functions/user/read-account.js +12 -0
  47. package/dist/functions/user/request-invitation.js +17 -0
  48. package/dist/functions/user/update-profile.js +22 -0
  49. package/dist/functions/user/update-user.js +68 -0
  50. package/dist/functions/user/user-errors.js +69 -0
  51. package/dist/functions/user/write-account.js +57 -0
  52. package/dist/functions/view-access/index.js +2 -1
  53. package/dist/functions/view-access/{assign-views.js → legacy/assign-views.js} +11 -7
  54. package/dist/functions/view-access/legacy/index.js +1 -0
  55. package/dist/functions/view-access/{view-access-errors.js → legacy/view-access-errors.js} +5 -1
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.js +3 -1
  58. package/dist/integrations/user-invitation/invite-user-request.d.ts +6 -0
  59. package/dist/legacy/index.js +1 -0
  60. package/dist/legacy/release.js +33 -0
  61. package/dist/mutations/composition/grant-role.js +13 -0
  62. package/dist/mutations/composition/index.js +1 -0
  63. package/dist/mutations/group/legacy/index.js +1 -0
  64. package/dist/mutations/group/legacy/membership-class.js +7 -0
  65. package/dist/mutations/group/set-group-membership.js +23 -14
  66. package/dist/mutations/identity/create-profile.js +37 -0
  67. package/dist/mutations/identity/delete-identity.js +28 -0
  68. package/dist/mutations/identity/index.js +4 -0
  69. package/dist/mutations/identity/write-account.js +109 -0
  70. package/dist/mutations/identity/write-group.js +72 -0
  71. package/dist/mutations/invitation/index.js +1 -0
  72. package/dist/mutations/invitation/write-invitation.js +62 -0
  73. package/dist/mutations/user/legacy/index.js +1 -0
  74. package/dist/mutations/user/{record-user-profile.js → legacy/record-user-profile.js} +5 -2
  75. package/dist/providers/admin-enrollment/create-admin-enrollment-provider.js +6 -3
  76. package/dist/providers/admin-enrollment/legacy/index.js +1 -0
  77. package/dist/providers/admin-enrollment/legacy/send-without-profile.js +16 -0
  78. package/dist/queries/application/legacy/index.js +1 -0
  79. package/dist/queries/application/legacy/read-installed-entrypoints.js +47 -0
  80. package/dist/queries/composition/index.js +2 -0
  81. package/dist/queries/composition/read-extensions.js +34 -0
  82. package/dist/queries/composition/read-role-nodes.js +41 -0
  83. package/dist/queries/group/index.js +1 -1
  84. package/dist/queries/group/legacy/index.js +2 -0
  85. package/dist/queries/group/legacy/read-core-groups.js +46 -0
  86. package/dist/queries/group/legacy/read-member-of-group.js +24 -0
  87. package/dist/queries/group/read-group-membership-endpoints.js +11 -0
  88. package/dist/queries/group/read-group-membership.js +6 -21
  89. package/dist/queries/identity/index.js +1 -0
  90. package/dist/queries/identity/read-identities.js +29 -0
  91. package/dist/queries/invitation/index.js +1 -0
  92. package/dist/queries/invitation/read-invitation.js +25 -0
  93. package/dist/queries/profile/index.js +1 -0
  94. package/dist/queries/profile/read-profile.js +15 -0
  95. package/dist/queries/role/index.js +1 -0
  96. package/dist/queries/role/read-role-node.js +13 -0
  97. package/dist/queries/user/legacy/index.js +1 -0
  98. package/dist/queries/user/legacy/read-user-profile.js +27 -0
  99. package/dist/runtime.d.ts +18203 -630
  100. package/dist/runtime.js +32 -7
  101. package/dist/schema/index.d.ts +7 -4
  102. package/dist/schema/index.js +8 -4
  103. package/dist/schema/legacy/index.js +1 -0
  104. package/dist/schema/legacy/schema.js +32 -0
  105. package/dist/schema/modules/application/legacy/entrypoint-entry.js +11 -0
  106. package/dist/schema/modules/application/legacy/functions/index.js +1 -0
  107. package/dist/schema/modules/application/legacy/functions/list-entrypoints.js +21 -0
  108. package/dist/schema/modules/application/legacy/index.js +2 -0
  109. package/dist/schema/modules/directory/functions/disconnect-directory.d.ts +13 -0
  110. package/dist/schema/modules/directory/functions/disconnect-directory.js +15 -0
  111. package/dist/schema/modules/directory/functions/index.d.ts +1 -0
  112. package/dist/schema/modules/directory/functions/index.js +1 -0
  113. package/dist/schema/modules/directory/index.d.ts +1 -0
  114. package/dist/schema/modules/directory/index.js +1 -0
  115. package/dist/schema/modules/group/classes/group-profile-link.d.ts +34 -0
  116. package/dist/schema/modules/group/classes/group-profile-link.js +21 -0
  117. package/dist/schema/modules/group/classes/group-profile.d.ts +15 -0
  118. package/dist/schema/modules/group/classes/group-profile.js +20 -0
  119. package/dist/schema/modules/group/classes/group.d.ts +73 -1
  120. package/dist/schema/modules/group/classes/group.js +28 -7
  121. package/dist/schema/modules/group/classes/index.d.ts +2 -1
  122. package/dist/schema/modules/group/classes/index.js +3 -1
  123. package/dist/schema/modules/group/functions/assign-member.js +17 -0
  124. package/dist/schema/modules/group/functions/create-group.js +20 -0
  125. package/dist/schema/modules/group/functions/delete-group.js +12 -0
  126. package/dist/schema/modules/group/functions/deprovision-group.js +14 -0
  127. package/dist/schema/modules/group/functions/index.js +7 -2
  128. package/dist/schema/modules/group/functions/provision-group.js +14 -0
  129. package/dist/schema/modules/group/functions/unassign-member.js +17 -0
  130. package/dist/schema/modules/group/functions/update-group.js +20 -0
  131. package/dist/schema/modules/group/index.d.ts +3 -3
  132. package/dist/schema/modules/group/index.js +4 -3
  133. package/dist/schema/modules/group/legacy/admission.d.ts +9 -0
  134. package/dist/schema/modules/group/legacy/admission.js +9 -0
  135. package/dist/schema/modules/group/legacy/classes/index.d.ts +1 -0
  136. package/dist/schema/modules/group/legacy/classes/index.js +1 -0
  137. package/dist/schema/modules/group/{classes → legacy/classes}/member-of-group.d.ts +10 -1
  138. package/dist/schema/modules/group/legacy/classes/member-of-group.js +32 -0
  139. package/dist/schema/modules/group/legacy/core-groups.d.ts +22 -0
  140. package/dist/schema/modules/group/legacy/core-groups.js +28 -0
  141. package/dist/schema/modules/group/legacy/index.d.ts +5 -0
  142. package/dist/schema/modules/group/legacy/index.js +6 -0
  143. package/dist/schema/modules/group/legacy/policies/belongs-to-group.d.ts +5 -0
  144. package/dist/schema/modules/group/legacy/policies/belongs-to-group.js +9 -0
  145. package/dist/schema/modules/group/legacy/policies/index.d.ts +2 -0
  146. package/dist/schema/modules/group/legacy/policies/index.js +2 -0
  147. package/dist/schema/modules/group/legacy/policies/owns-membership-group.d.ts +6 -0
  148. package/dist/schema/modules/group/legacy/policies/owns-membership-group.js +12 -0
  149. package/dist/schema/modules/group/legacy/relaxations.d.ts +21 -0
  150. package/dist/schema/modules/group/legacy/relaxations.js +27 -0
  151. package/dist/schema/modules/group/policies/describes-group-of-subject.d.ts +2 -0
  152. package/dist/schema/modules/group/policies/describes-group-of-subject.js +7 -0
  153. package/dist/schema/modules/group/policies/index.d.ts +2 -1
  154. package/dist/schema/modules/group/policies/index.js +2 -1
  155. package/dist/schema/modules/group/policies/is-group-profile-source.d.ts +2 -0
  156. package/dist/schema/modules/group/policies/is-group-profile-source.js +6 -0
  157. package/dist/schema/modules/group/types/group.js +10 -0
  158. package/dist/schema/modules/group/types/index.js +1 -0
  159. package/dist/schema/modules/invitation/classes/index.js +2 -0
  160. package/dist/schema/modules/invitation/classes/invitation-user.js +17 -0
  161. package/dist/schema/modules/invitation/classes/invitation.js +30 -0
  162. package/dist/schema/modules/invitation/functions/accept.js +16 -0
  163. package/dist/schema/modules/invitation/functions/cancel.js +12 -0
  164. package/dist/schema/modules/invitation/functions/index.js +4 -0
  165. package/dist/schema/modules/invitation/functions/resend.js +12 -0
  166. package/dist/schema/modules/invitation/functions/send.js +20 -0
  167. package/dist/schema/modules/invitation/index.js +2 -0
  168. package/dist/schema/modules/invitation/types/index.js +1 -0
  169. package/dist/schema/modules/invitation/types/invitation-status.js +5 -0
  170. package/dist/schema/modules/namespace/classes/alias.d.ts +2 -1
  171. package/dist/schema/modules/namespace/classes/alias.js +11 -2
  172. package/dist/schema/modules/namespace/functions/link.js +3 -2
  173. package/dist/schema/modules/namespace/functions/rename.js +3 -2
  174. package/dist/schema/modules/namespace/functions/unlink.js +3 -2
  175. package/dist/schema/modules/role/classes/index.d.ts +1 -0
  176. package/dist/schema/modules/role/classes/index.js +1 -0
  177. package/dist/schema/modules/role/classes/role.d.ts +16 -0
  178. package/dist/schema/modules/role/classes/role.js +19 -0
  179. package/dist/schema/modules/role/core/index.d.ts +1 -0
  180. package/dist/schema/modules/role/core/index.js +1 -0
  181. package/dist/schema/modules/role/core/roles.d.ts +37 -0
  182. package/dist/schema/modules/role/core/roles.js +30 -0
  183. package/dist/schema/modules/role/index.d.ts +2 -0
  184. package/dist/schema/modules/role/index.js +2 -0
  185. package/dist/schema/modules/user/classes/index.d.ts +2 -0
  186. package/dist/schema/modules/user/classes/index.js +3 -0
  187. package/dist/schema/modules/user/classes/user-profile-link.d.ts +35 -0
  188. package/dist/schema/modules/user/classes/user-profile-link.js +22 -0
  189. package/dist/schema/modules/user/classes/user-profile.d.ts +41 -0
  190. package/dist/schema/modules/user/classes/user-profile.js +30 -0
  191. package/dist/schema/modules/user/classes/user.d.ts +133 -8
  192. package/dist/schema/modules/user/classes/user.js +46 -15
  193. package/dist/schema/modules/user/functions/create-user.js +21 -0
  194. package/dist/schema/modules/user/functions/deactivate-user.js +12 -0
  195. package/dist/schema/modules/user/functions/delete-user.js +12 -0
  196. package/dist/schema/modules/user/functions/deprovision-user.js +14 -0
  197. package/dist/schema/modules/user/functions/index.js +8 -1
  198. package/dist/schema/modules/user/functions/invite-user.js +23 -4
  199. package/dist/schema/modules/user/functions/provision-user.js +25 -0
  200. package/dist/schema/modules/user/functions/reactivate-user.js +12 -0
  201. package/dist/schema/modules/user/functions/update-profile.js +16 -0
  202. package/dist/schema/modules/user/functions/update-user.js +22 -0
  203. package/dist/schema/modules/user/index.d.ts +2 -1
  204. package/dist/schema/modules/user/index.js +2 -1
  205. package/dist/schema/modules/user/legacy/account-properties.js +24 -0
  206. package/dist/schema/modules/user/legacy/functions/index.js +2 -0
  207. package/dist/schema/modules/user/legacy/functions/migrate-accounts.js +23 -0
  208. package/dist/schema/modules/user/legacy/functions/record-profile.js +18 -0
  209. package/dist/schema/modules/user/legacy/index.js +5 -0
  210. package/dist/schema/modules/user/legacy/invite.js +15 -0
  211. package/dist/schema/modules/user/legacy/relaxations.js +37 -0
  212. package/dist/schema/modules/user/policies/describes-subject.d.ts +2 -0
  213. package/dist/schema/modules/user/policies/describes-subject.js +7 -0
  214. package/dist/schema/modules/user/policies/index.d.ts +2 -0
  215. package/dist/schema/modules/user/policies/index.js +2 -0
  216. package/dist/schema/modules/user/policies/is-profile-source.d.ts +2 -0
  217. package/dist/schema/modules/user/policies/is-profile-source.js +6 -0
  218. package/dist/schema/modules/user/types/account.d.ts +12 -0
  219. package/dist/schema/modules/user/types/account.js +12 -0
  220. package/dist/schema/modules/user/types/index.d.ts +1 -1
  221. package/dist/schema/modules/user/types/index.js +1 -1
  222. package/dist/schema/modules/view-access/classes/view-access.d.ts +7 -11
  223. package/dist/schema/modules/view-access/classes/view-access.js +17 -12
  224. package/dist/schema/modules/view-access/index.d.ts +1 -2
  225. package/dist/schema/modules/view-access/index.js +1 -2
  226. package/dist/schema/modules/view-access/legacy/functions/assign-views.js +22 -0
  227. package/dist/schema/modules/view-access/legacy/functions/index.js +1 -0
  228. package/dist/schema/modules/view-access/legacy/index.js +2 -0
  229. package/dist/schema/modules/view-access/legacy/view-assignment.js +24 -0
  230. package/dist/schema/modules/view-access/policies/holds-view-access.d.ts +2 -0
  231. package/dist/schema/modules/view-access/policies/holds-view-access.js +5 -0
  232. package/dist/schema/modules/view-access/policies/index.d.ts +1 -0
  233. package/dist/schema/modules/view-access/policies/index.js +1 -0
  234. package/dist/schema/policies/has-role.d.ts +6 -0
  235. package/dist/schema/policies/has-role.js +9 -0
  236. package/dist/schema/policies/index.d.ts +1 -0
  237. package/dist/schema/policies/index.js +1 -4
  238. package/dist/schema/policies/legacy/administers-shell-class.js +21 -0
  239. package/dist/schema/policies/legacy/index.js +4 -0
  240. package/dist/schema/policies/legacy/is-self.js +9 -0
  241. package/dist/schema/policies/legacy/owns-class.js +12 -0
  242. package/dist/schema/policies/legacy/uses-function.js +9 -0
  243. package/dist/schema/schema.d.ts +1849 -118
  244. package/dist/schema/schema.js +35 -15
  245. package/dist/schema/types/attributes.js +15 -0
  246. package/dist/schema/types/index.js +1 -0
  247. package/dist/utils/attributes/attributes.js +12 -0
  248. package/dist/utils/attributes/index.js +1 -0
  249. package/dist/utils/index.js +3 -0
  250. package/dist/utils/same-json/index.js +1 -0
  251. package/dist/utils/same-json/same-json.js +9 -0
  252. package/package.json +4 -4
  253. package/dist/functions/application/list-entrypoints.js +0 -6
  254. package/dist/functions/user/record-profile.js +0 -18
  255. package/dist/mutations/user/index.js +0 -1
  256. package/dist/queries/application/index.js +0 -1
  257. package/dist/queries/application/read-installed-entrypoints.js +0 -28
  258. package/dist/queries/user/index.js +0 -1
  259. package/dist/queries/user/read-user-profile.js +0 -9
  260. package/dist/schema/modules/application/functions/index.d.ts +0 -1
  261. package/dist/schema/modules/application/functions/index.js +0 -1
  262. package/dist/schema/modules/application/functions/list-entrypoints.d.ts +0 -15
  263. package/dist/schema/modules/application/functions/list-entrypoints.js +0 -16
  264. package/dist/schema/modules/application/index.d.ts +0 -2
  265. package/dist/schema/modules/application/index.js +0 -2
  266. package/dist/schema/modules/application/types/entrypoint-entry.d.ts +0 -8
  267. package/dist/schema/modules/application/types/entrypoint-entry.js +0 -7
  268. package/dist/schema/modules/application/types/index.d.ts +0 -1
  269. package/dist/schema/modules/application/types/index.js +0 -1
  270. package/dist/schema/modules/group/classes/member-of-group.js +0 -12
  271. package/dist/schema/modules/group/core/administrators-group.d.ts +0 -1
  272. package/dist/schema/modules/group/core/administrators-group.js +0 -6
  273. package/dist/schema/modules/group/core/applications-group.d.ts +0 -1
  274. package/dist/schema/modules/group/core/applications-group.js +0 -6
  275. package/dist/schema/modules/group/core/index.d.ts +0 -3
  276. package/dist/schema/modules/group/core/index.js +0 -3
  277. package/dist/schema/modules/group/core/members-group.d.ts +0 -1
  278. package/dist/schema/modules/group/core/members-group.js +0 -6
  279. package/dist/schema/modules/group/functions/assign-member-to-group.js +0 -11
  280. package/dist/schema/modules/group/functions/unassign-member-from-group.js +0 -11
  281. package/dist/schema/modules/group/policies/belongs-to-group.d.ts +0 -1
  282. package/dist/schema/modules/group/policies/belongs-to-group.js +0 -5
  283. package/dist/schema/modules/user/functions/record-profile.js +0 -14
  284. package/dist/schema/modules/user/types/user-profile.d.ts +0 -7
  285. package/dist/schema/modules/user/types/user-profile.js +0 -7
  286. package/dist/schema/modules/view-access/core/index.d.ts +0 -1
  287. package/dist/schema/modules/view-access/core/index.js +0 -1
  288. package/dist/schema/modules/view-access/core/view-only-group.d.ts +0 -5
  289. package/dist/schema/modules/view-access/core/view-only-group.js +0 -10
  290. package/dist/schema/modules/view-access/functions/assign-views.d.ts +0 -16
  291. package/dist/schema/modules/view-access/functions/assign-views.js +0 -17
  292. package/dist/schema/modules/view-access/functions/index.d.ts +0 -1
  293. package/dist/schema/modules/view-access/functions/index.js +0 -1
  294. package/dist/schema/modules/view-access/types/index.js +0 -1
  295. package/dist/schema/modules/view-access/types/view-assignment.js +0 -16
  296. package/dist/schema/policies/administers-shell-class.js +0 -11
  297. package/dist/schema/policies/is-self.js +0 -5
  298. package/dist/schema/policies/owns-class.js +0 -7
  299. package/dist/schema/policies/uses-function.js +0 -5
  300. /package/dist/functions/group/{assign-member-to-group.js → assign-member.js} +0 -0
  301. /package/dist/functions/group/{unassign-member-from-group.js → unassign-member.js} +0 -0
@@ -1,39 +1,47 @@
1
1
  import { defineWorkflow } from '@astrale-os/sdk/workflow';
2
- import { z } from 'zod';
3
- import { InvitationRequestFailure } from '../../integrations/user-invitation/index.js';
2
+ import { authority, readExtensionTargets } from '../authority/index.js';
3
+ import { grantRole } from '../../mutations/composition/index.js';
4
4
  import { canonicalNodeId } from '../../utils/canonical-node-path/index.js';
5
- export const invite = defineWorkflow()('User.invite', {
6
- errors: {
7
- INVALID_INVITATION_REFERENCE: {
8
- family: 'VALIDATION_ERROR',
9
- message: 'User must be an exact canonical @NodeId path.',
10
- details: z.strictObject({}),
11
- },
12
- INVITATION_CONFLICT: {
13
- family: 'CONFLICT',
14
- message: 'The invitation conflicts with an existing request.',
15
- details: z.strictObject({ requestId: z.string() }),
16
- },
17
- INVITATION_REJECTED: {
18
- family: 'VALIDATION_ERROR',
19
- message: 'Admin rejected the invitation request.',
20
- details: z.strictObject({ requestId: z.string() }),
21
- },
22
- },
23
- }, async (context) => {
5
+ import { inviteErrors } from './invite-errors.js';
6
+ // Seam L-SHELL-6: the `{ user, email }` form of callers older than profiles.
7
+ import { legacyInvite, legacyIsInvitation } from './legacy/index.js';
8
+ import { readAccount } from './read-account.js';
9
+ import { requestInvitation } from './request-invitation.js';
10
+ import { userErrors } from './user-errors.js';
11
+ const errors = { ...inviteErrors, ...userErrors };
12
+ /**
13
+ * Grant the account its profile, `member` by default, then have Admin send the invitation to the
14
+ * account's own email. A deactivated account is not invited; only an admin grants `admin`.
15
+ *
16
+ * @deprecated The invitation before durable invitations (L-SHELL-18). Replaced by
17
+ * `Invitation.send` (`functions/invitation/send-invitation.ts`). Removal: Shell major (delivery
18
+ * step 5), once Admin (step 2) and 1Pact (step 4) call `Invitation.send`.
19
+ */
20
+ export const invite = defineWorkflow()('User.invite', { errors }, async (context) => {
24
21
  const user = canonicalNodeId(context.input.user, () => context.error('INVALID_INVITATION_REFERENCE', {}));
25
- const requestId = context.execution.idempotencyKey ?? crypto.randomUUID();
26
- try {
27
- return await context.step.run('request-invitation', () => context.integrations.enrollment.invite({
28
- user,
29
- email: context.input.email,
30
- requestId,
31
- requestedBy: context.caller.identity,
32
- }));
33
- }
34
- catch (cause) {
35
- if (cause instanceof InvitationRequestFailure)
36
- throw context.error(cause.code, { requestId });
37
- throw cause;
22
+ if (legacyIsInvitation(context.input))
23
+ return legacyInvite(context, user);
24
+ const profile = context.input.profile ?? 'member';
25
+ const role = `workspace.${profile}`;
26
+ const account = await context.step.run('read-account', () => readAccount(context, user));
27
+ if (account === undefined)
28
+ throw context.error('USER_NOT_FOUND', {});
29
+ if (account.frozen)
30
+ throw context.error('USER_SUSPENDED', {});
31
+ if (account.email === undefined)
32
+ throw context.error('USER_EMAIL_INVALID', {});
33
+ const email = account.email;
34
+ if (profile === 'admin') {
35
+ const admitted = await context.step.run('check-profile', () => authority(context).callerHolds('workspace.admin'));
36
+ if (!admitted)
37
+ throw context.error('PROFILE_NOT_HELD', {});
38
38
  }
39
+ await context.step.run('grant-profile', async () => {
40
+ const held = await readExtensionTargets(context, user);
41
+ const node = await authority(context).roleNode(role);
42
+ if (node !== undefined && held.has(node))
43
+ return;
44
+ await context.mutate(grantRole, { holder: user, role: context.domain.core.nodes[role].ref });
45
+ });
46
+ return requestInvitation(context, { user, email, profile });
39
47
  });
@@ -0,0 +1,11 @@
1
+ import { readIdentities } from '../../../queries/identity/index.js';
2
+ /**
3
+ * @deprecated The account that claimed an email before accounts had `email` (L-SHELL-13): Shell
4
+ * neither creates nor provides a second account for it (U1), and does not trust the claim enough to
5
+ * return that account; `legacyMigrateAccounts` makes it the email's account. Replaced by the
6
+ * `email` key alone. Removal: Shell major (delivery step 5) after 1Pact step 4.
7
+ */
8
+ export async function legacyClaimedAccount({ query }, email) {
9
+ const [claimed] = await query(readIdentities, { kind: 'User', where: { claimedEmail: email } });
10
+ return claimed?.id;
11
+ }
@@ -0,0 +1,4 @@
1
+ export { legacyClaimedAccount } from './claimed-account.js';
2
+ export { legacyInvite, legacyIsInvitation } from './invite.js';
3
+ export { legacyRecordProfile } from './record-profile.js';
4
+ export { legacyMigrateAccounts } from './migrate-accounts.js';
@@ -0,0 +1,20 @@
1
+ import { requestInvitation } from '../request-invitation.js';
2
+ /**
3
+ * @deprecated Whether an invitation names its email, the form before profiles (L-SHELL-6).
4
+ * Replaced by `User.invite({ user, profile })`. Removal: Shell major (delivery step 5) after 1Pact step 4.
5
+ */
6
+ export function legacyIsInvitation(input) {
7
+ return input.email !== undefined;
8
+ }
9
+ /**
10
+ * @deprecated The `{ user, email }` invitation of callers older than profiles (L-SHELL-6): Admin
11
+ * sends it to the email the caller gives and assigns the Core group itself; Shell grants nothing.
12
+ * Replaced by `User.invite({ user, profile })`. Removal: Shell major (delivery step 5) after 1Pact
13
+ * step 4.
14
+ */
15
+ export function legacyInvite(context, user) {
16
+ const email = context.input.email;
17
+ if (email === undefined)
18
+ throw new TypeError('A legacy invitation names its email.');
19
+ return requestInvitation(context, { user, email });
20
+ }
@@ -0,0 +1,98 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { toGroup } from '../../group/index.js';
3
+ import { createProfile, groupProfileValues, newProfileValues } from '../../../mutations/identity/index.js';
4
+ import { readIdentities } from '../../../queries/identity/index.js';
5
+ import { readProfile } from '../../../queries/profile/index.js';
6
+ import { nodeReference, normalizeEmail, toAccount } from '../account.js';
7
+ import { readAccount } from '../read-account.js';
8
+ import { writeAccount } from '../write-account.js';
9
+ /**
10
+ * @deprecated (L-SHELL-14) The profile name the migration gives an account that has neither a name
11
+ * nor an email yet. Replaced by nothing: accounts are born with a name or an email. Removal: with
12
+ * `legacyMigrateAccounts`.
13
+ */
14
+ export const LEGACY_UNNAMED_ACCOUNT = 'Compte sans nom';
15
+ /**
16
+ * @deprecated Write `email` and `name` on the accounts created before them, from what
17
+ * each recorded about itself, and give every account and group stored before profiles its
18
+ * profile (L-SHELL-14). Idempotent: an account that has them is left as it is,
19
+ * and an email another account holds is reported, never taken. Replaced by nothing: accounts are
20
+ * born with them. Removal: Shell major (delivery step 5) after 1Pact step 4, once every instance
21
+ * has run it.
22
+ *
23
+ * A claimed email was recorded by the account itself at sign-in: an administrator runs this only
24
+ * where the GUI recorded the email its identity provider verified.
25
+ */
26
+ export const legacyMigrateAccounts = defineAction()('legacyMigrateAccounts', async (context) => {
27
+ const properties = context.domain.classes.User.properties;
28
+ const nodes = await context.query(readIdentities, { kind: 'User', where: {} });
29
+ const accounts = nodes.map((node) => ({
30
+ account: toAccount(context.domain, node),
31
+ props: node.props,
32
+ }));
33
+ const taken = new Map();
34
+ for (const { account } of accounts)
35
+ if (account.email !== undefined)
36
+ taken.set(account.email, account.id);
37
+ let migrated = 0;
38
+ const conflicts = [];
39
+ for (const { account, props } of accounts) {
40
+ const claimedEmail = props[properties.claimedEmail.key];
41
+ const claimedName = props[properties.claimedName.key];
42
+ const email = account.email === undefined && typeof claimedEmail === 'string'
43
+ ? normalizeEmail(claimedEmail)
44
+ : undefined;
45
+ if (email !== undefined && taken.has(email) && taken.get(email) !== account.id) {
46
+ conflicts.push({ user: nodeReference(account.id), email });
47
+ continue;
48
+ }
49
+ const name = account.name === undefined && typeof claimedName === 'string'
50
+ ? claimedName.slice(0, 100)
51
+ : undefined;
52
+ const written = await writeAccount(context, account, {
53
+ ...(email === undefined ? {} : { email }),
54
+ ...(name === undefined ? {} : { name }),
55
+ });
56
+ if (written === 'written') {
57
+ migrated += 1;
58
+ if (email !== undefined)
59
+ taken.set(email, account.id);
60
+ }
61
+ else if (written !== 'unchanged' && email !== undefined) {
62
+ conflicts.push({ user: nodeReference(account.id), email });
63
+ }
64
+ }
65
+ // Every account and group stored before profiles gets its profile, from its values now.
66
+ let profiles = 0;
67
+ for (const { account } of accounts) {
68
+ if ((await context.query(readProfile, { kind: 'User', node: account.id })) !== undefined)
69
+ continue;
70
+ const current = (await readAccount(context, account.id)) ?? account;
71
+ // An account with no name nor email, such as the owner `instance create` makes, still gets
72
+ // one: named for now, renamed with the account once it has a name.
73
+ const values = newProfileValues(current) ?? { name: LEGACY_UNNAMED_ACCOUNT };
74
+ const result = await context.mutate(createProfile, {
75
+ kind: 'User',
76
+ owner: account.id,
77
+ values,
78
+ });
79
+ if (result.ok)
80
+ profiles += 1;
81
+ }
82
+ const groups = await context.query(readIdentities, { kind: 'Group', where: {} });
83
+ for (const node of groups) {
84
+ const group = toGroup(context.domain, node);
85
+ if (group.name === undefined)
86
+ continue;
87
+ if ((await context.query(readProfile, { kind: 'Group', node: group.id })) !== undefined)
88
+ continue;
89
+ const result = await context.mutate(createProfile, {
90
+ kind: 'Group',
91
+ owner: group.id,
92
+ values: groupProfileValues(group),
93
+ });
94
+ if (result.ok)
95
+ profiles += 1;
96
+ }
97
+ return { migrated, conflicts, profiles };
98
+ });
@@ -0,0 +1,19 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { legacyRecordUserProfile } from '../../../mutations/user/legacy/index.js';
3
+ import { legacyReadUserProfile } from '../../../queries/user/legacy/index.js';
4
+ /**
5
+ * @deprecated The email and display name the GUI records at each sign-in (L-SHELL-5): one read and,
6
+ * only when something changed, one write, with Shell's own authority; the email in lower case.
7
+ * Replaced by `User.updateProfile`. Removal: Shell major (delivery step 5) after 1Pact step 4.
8
+ */
9
+ export const legacyRecordProfile = defineAction()('User.recordProfile', async ({ input, self, query, mutate }) => {
10
+ const asked = {
11
+ ...(input.email === undefined ? {} : { email: input.email.trim().toLowerCase() }),
12
+ ...(input.displayName === undefined ? {} : { displayName: input.displayName }),
13
+ };
14
+ const current = await query(legacyReadUserProfile, { user: self });
15
+ const set = Object.fromEntries(Object.entries(asked).filter(([key, value]) => current[key] !== value));
16
+ if (Object.keys(set).length > 0)
17
+ await mutate(legacyRecordUserProfile, { user: self, set });
18
+ return {};
19
+ });
@@ -0,0 +1,16 @@
1
+ import { authority } from '../authority/index.js';
2
+ /**
3
+ * U4 and U5 for a change that takes an account away from its holder: only an admin changes an
4
+ * account that holds an administration Role, and the last active admin stays. Answers the refusal.
5
+ */
6
+ export async function protectAdministrators(capabilities, user, options) {
7
+ const authorities = authority(capabilities);
8
+ if (!(await authorities.holdsAdministration(user)))
9
+ return undefined;
10
+ if (!(await authorities.callerHolds('workspace.admin')))
11
+ return 'USER_ADMINISTRATOR_PROTECTED';
12
+ if (!options.keepsLastAdministrator)
13
+ return undefined;
14
+ const active = await authorities.activeAdministrators();
15
+ return active.includes(user) && active.length <= 1 ? 'USER_LAST_ADMINISTRATOR' : undefined;
16
+ }
@@ -0,0 +1,70 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { createAccount } from '../../mutations/identity/index.js';
3
+ import { nodeReference, normalizeEmail } from './account.js';
4
+ // Seam L-SHELL-13: an email an account claimed before `email` existed is taken (U1).
5
+ import { legacyClaimedAccount } from './legacy/index.js';
6
+ import { findAccount } from './read-account.js';
7
+ import { userErrors } from './user-errors.js';
8
+ import { writeAccount } from './write-account.js';
9
+ /** A2: an account a directory adopts, local, or of the same directory and frozen. */
10
+ const adoptable = (account, directory) => account.directory === undefined || (account.directory === directory && account.frozen);
11
+ /**
12
+ * Write the account a directory provides (`dsync.user.created`, `dsync.user.updated`). The account
13
+ * of its directory id is updated; else the account of its email is adopted (A2); else one is
14
+ * created. Replaying an event writes nothing (A3); a conflict refuses it whole and names the
15
+ * account it conflicts with (A4).
16
+ */
17
+ export const provision = defineAction()('User.provision', { errors: userErrors }, async (context) => {
18
+ const { input, error } = context;
19
+ const email = normalizeEmail(input.email);
20
+ if (email === undefined)
21
+ throw error('USER_EMAIL_INVALID', {});
22
+ const { directory, externalId } = input;
23
+ const provided = {
24
+ email,
25
+ name: input.name,
26
+ givenName: input.givenName ?? null,
27
+ familyName: input.familyName ?? null,
28
+ frozen: !input.active,
29
+ };
30
+ const [byDirectory, byEmail] = await Promise.all([
31
+ findAccount(context, { directory, externalId }),
32
+ findAccount(context, { email }),
33
+ ]);
34
+ const conflict = (account) => error('DIRECTORY_CONFLICT', { user: nodeReference(account.id) });
35
+ let target = byDirectory;
36
+ let adopted = false;
37
+ if (byDirectory !== undefined) {
38
+ if (byEmail !== undefined && byEmail.id !== byDirectory.id)
39
+ throw conflict(byEmail);
40
+ }
41
+ else if (byEmail !== undefined) {
42
+ if (!adoptable(byEmail, directory))
43
+ throw conflict(byEmail);
44
+ target = byEmail;
45
+ adopted = true;
46
+ }
47
+ if (target === undefined) {
48
+ const claimed = await legacyClaimedAccount(context, email);
49
+ if (claimed !== undefined)
50
+ throw error('DIRECTORY_CONFLICT', { user: nodeReference(claimed) });
51
+ const result = await context.mutate(createAccount, {
52
+ values: {
53
+ email,
54
+ name: input.name,
55
+ ...(input.givenName === undefined ? {} : { givenName: input.givenName }),
56
+ ...(input.familyName === undefined ? {} : { familyName: input.familyName }),
57
+ directory,
58
+ externalId,
59
+ frozen: !input.active,
60
+ },
61
+ });
62
+ if (!result.ok)
63
+ throw error('ACCOUNT_CHANGED', {});
64
+ return { user: nodeReference(result.user), created: true, adopted: false };
65
+ }
66
+ const written = await writeAccount(context, target, { ...provided, directory, externalId });
67
+ if (written === 'DUPLICATE_KEY' || written === 'CHANGED')
68
+ throw error('ACCOUNT_CHANGED', {});
69
+ return { user: nodeReference(target.id), created: false, adopted };
70
+ });
@@ -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: false`: the account may sign in again (U4). */
7
+ export const reactivate = defineAction()('User.reactivate', { 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: false });
13
+ if (refusal !== undefined)
14
+ throw error(refusal, {});
15
+ const written = await writeAccount(context, account, { frozen: false });
16
+ if (written === 'CHANGED' || written === 'DUPLICATE_KEY')
17
+ throw error('ACCOUNT_CHANGED', {});
18
+ return {};
19
+ });
@@ -0,0 +1,12 @@
1
+ import { readIdentities } from '../../queries/identity/index.js';
2
+ import { toAccount } from './account.js';
3
+ /** The account at one Node, or undefined when there is none Shell reads. */
4
+ export async function readAccount({ query, domain }, user) {
5
+ const [node] = await query(readIdentities, { kind: 'User', ids: [user] });
6
+ return node === undefined ? undefined : toAccount(domain, node);
7
+ }
8
+ /** The account holding these values: an email, or a directory and its id there. */
9
+ export async function findAccount({ query, domain }, where) {
10
+ const [node] = await query(readIdentities, { kind: 'User', where });
11
+ return node === undefined ? undefined : toAccount(domain, node);
12
+ }
@@ -0,0 +1,17 @@
1
+ import { InvitationRequestFailure } from '../../integrations/user-invitation/index.js';
2
+ /** Ask Admin to send the invitation, once per invocation key. */
3
+ export async function requestInvitation(context, request) {
4
+ const requestId = context.execution.idempotencyKey ?? crypto.randomUUID();
5
+ try {
6
+ return await context.step.run('request-invitation', () => context.integrations.enrollment.invite({
7
+ ...request,
8
+ requestId,
9
+ requestedBy: context.caller.identity,
10
+ }));
11
+ }
12
+ catch (cause) {
13
+ if (cause instanceof InvitationRequestFailure)
14
+ throw context.error(cause.code, { requestId });
15
+ throw cause;
16
+ }
17
+ }
@@ -0,0 +1,22 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { isProvided } from './account.js';
3
+ import { readAccount } from './read-account.js';
4
+ import { userErrors } from './user-errors.js';
5
+ import { writeAccount } from './write-account.js';
6
+ /** The holder changes its own name or icon; a provided account's name is its directory's (U3). */
7
+ export const updateProfile = defineAction()('User.updateProfile', { errors: userErrors }, async (context) => {
8
+ const { input, error, self } = context;
9
+ const account = await readAccount(context, self);
10
+ if (account === undefined)
11
+ throw error('USER_NOT_FOUND', {});
12
+ if (isProvided(account) && input.name !== undefined && input.name !== account.name) {
13
+ throw error('USER_DIRECTORY_MANAGED', {});
14
+ }
15
+ const written = await writeAccount(context, account, {
16
+ ...(input.name === undefined ? {} : { name: input.name }),
17
+ ...(input.icon === undefined ? {} : { icon: input.icon }),
18
+ });
19
+ if (written === 'CHANGED' || written === 'DUPLICATE_KEY')
20
+ throw error('ACCOUNT_CHANGED', {});
21
+ return {};
22
+ });
@@ -0,0 +1,68 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { authority } from '../authority/index.js';
3
+ import { MAX_ATTRIBUTES, boundedAttributes } from '../../utils/index.js';
4
+ import { isProvided, nodeReference, normalizeEmail } from './account.js';
5
+ import { findAccount, readAccount } from './read-account.js';
6
+ import { userErrors } from './user-errors.js';
7
+ import { writeAccount } from './write-account.js';
8
+ /**
9
+ * Change an account's name, email, icon or attributes. Attributes are the instance's own, never
10
+ * the directory's (AM11). A provided account's name and email come from its
11
+ * directory (U3); an administrator's account changes only by an admin (U4); an email designates at
12
+ * most one account (U1).
13
+ */
14
+ export const update = defineAction()('User.update', { errors: userErrors }, async (context) => {
15
+ const { input, error, self } = context;
16
+ const account = await readAccount(context, self);
17
+ if (account === undefined)
18
+ throw error('USER_NOT_FOUND', {});
19
+ const attributes = input.attributes === undefined || input.attributes === null
20
+ ? input.attributes
21
+ : boundedAttributes(input.attributes);
22
+ if (input.attributes !== undefined && input.attributes !== null && attributes === undefined) {
23
+ throw error('USER_ATTRIBUTES_TOO_MANY', { max: MAX_ATTRIBUTES });
24
+ }
25
+ let email;
26
+ if (input.email !== undefined) {
27
+ email = normalizeEmail(input.email);
28
+ if (email === undefined)
29
+ throw error('USER_EMAIL_INVALID', {});
30
+ }
31
+ const directoryOwned = {
32
+ name: input.name,
33
+ email,
34
+ givenName: input.givenName,
35
+ familyName: input.familyName,
36
+ };
37
+ if (isProvided(account) &&
38
+ Object.entries(directoryOwned).some(([key, value]) => value !== undefined && value !== account[key])) {
39
+ throw error('USER_DIRECTORY_MANAGED', {});
40
+ }
41
+ const authorities = authority(context);
42
+ if ((await authorities.holdsAdministration(account.id)) &&
43
+ !(await authorities.callerHolds('workspace.admin'))) {
44
+ throw error('USER_ADMINISTRATOR_PROTECTED', {});
45
+ }
46
+ if (email !== undefined && email !== account.email) {
47
+ const holder = await findAccount(context, { email });
48
+ if (holder !== undefined)
49
+ throw error('USER_EMAIL_TAKEN', { user: nodeReference(holder.id) });
50
+ }
51
+ const written = await writeAccount(context, account, {
52
+ ...(input.name === undefined ? {} : { name: input.name }),
53
+ ...(email === undefined ? {} : { email }),
54
+ ...(input.givenName === undefined ? {} : { givenName: input.givenName }),
55
+ ...(input.familyName === undefined ? {} : { familyName: input.familyName }),
56
+ ...(input.icon === undefined ? {} : { icon: input.icon }),
57
+ ...(attributes === undefined ? {} : { attributes }),
58
+ });
59
+ if (written === 'DUPLICATE_KEY') {
60
+ const holder = email === undefined ? undefined : await findAccount(context, { email });
61
+ throw error('USER_EMAIL_TAKEN', {
62
+ user: holder === undefined ? '' : nodeReference(holder.id),
63
+ });
64
+ }
65
+ if (written === 'CHANGED')
66
+ throw error('ACCOUNT_CHANGED', {});
67
+ return {};
68
+ });
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ const none = z.strictObject({});
3
+ const account = z.strictObject({ user: z.string() });
4
+ /**
5
+ * The refusals of the account methods. The SDK declares no FORBIDDEN business family, so a law
6
+ * that forbids a change for the state of the account, its group or its caller answers CONFLICT.
7
+ */
8
+ export const userErrors = {
9
+ USER_NOT_FOUND: {
10
+ family: 'NOT_FOUND',
11
+ message: 'No account is at this path.',
12
+ details: none,
13
+ },
14
+ USER_EMAIL_INVALID: {
15
+ family: 'VALIDATION_ERROR',
16
+ message: 'The email is not a valid address, or the account has none.',
17
+ details: none,
18
+ },
19
+ USER_ATTRIBUTES_TOO_MANY: {
20
+ family: 'VALIDATION_ERROR',
21
+ message: 'An account carries at most 64 attributes.',
22
+ details: z.strictObject({ max: z.number() }),
23
+ },
24
+ USER_EMAIL_TAKEN: {
25
+ family: 'CONFLICT',
26
+ message: 'Another account already has this email.',
27
+ details: account,
28
+ },
29
+ USER_DIRECTORY_MANAGED: {
30
+ family: 'CONFLICT',
31
+ message: "This account is managed by your organization's directory. Change it in the directory.",
32
+ details: none,
33
+ },
34
+ USER_ADMINISTRATOR_PROTECTED: {
35
+ family: 'CONFLICT',
36
+ message: "Only an administrator may change an administrator's account.",
37
+ details: none,
38
+ },
39
+ USER_LAST_ADMINISTRATOR: {
40
+ family: 'CONFLICT',
41
+ message: 'This account is the last active administrator of the workspace.',
42
+ details: none,
43
+ },
44
+ USER_ACTIVE: {
45
+ family: 'CONFLICT',
46
+ message: 'Deactivate this account before deleting it.',
47
+ details: none,
48
+ },
49
+ USER_SUSPENDED: {
50
+ family: 'CONFLICT',
51
+ message: 'This account is deactivated; reactivate it before inviting it.',
52
+ details: none,
53
+ },
54
+ PROFILE_NOT_HELD: {
55
+ family: 'CONFLICT',
56
+ message: 'Only an administrator may grant the admin profile.',
57
+ details: none,
58
+ },
59
+ DIRECTORY_CONFLICT: {
60
+ family: 'CONFLICT',
61
+ message: 'The directory event conflicts with an existing account; nothing changed.',
62
+ details: account,
63
+ },
64
+ ACCOUNT_CHANGED: {
65
+ family: 'CONFLICT',
66
+ message: 'The account changed concurrently; retry the operation.',
67
+ details: none,
68
+ },
69
+ };
@@ -0,0 +1,57 @@
1
+ import { PROFILE_PROPERTIES, newProfileValues, updateAccount, } from '../../mutations/identity/index.js';
2
+ import { readProfile } from '../../queries/profile/index.js';
3
+ import { sameJson } from '../../utils/index.js';
4
+ /** The values of an account a write is decided on. */
5
+ const DECIDED = [
6
+ 'name',
7
+ 'email',
8
+ 'givenName',
9
+ 'familyName',
10
+ 'icon',
11
+ 'directory',
12
+ 'externalId',
13
+ 'frozen',
14
+ ];
15
+ /**
16
+ * Write what differs between an account and a change (`null` erases). `frozen` is written as is: it
17
+ * alone states whether the account may sign in. The write holds only on the values it was decided on. It answers whether
18
+ * anything changed, or the refusal: a key another account holds, or a concurrent change.
19
+ */
20
+ export async function writeAccount({ mutate, query }, account, change) {
21
+ const set = {};
22
+ const unset = [];
23
+ for (const [key, value] of Object.entries(change)) {
24
+ const current = account[key];
25
+ if (value === null || value === undefined) {
26
+ if (value === null && current !== undefined)
27
+ unset.push(key);
28
+ }
29
+ else if (!sameJson(value, current))
30
+ set[key] = value;
31
+ }
32
+ if (Object.keys(set).length === 0 && unset.length === 0)
33
+ return 'unchanged';
34
+ const expected = Object.fromEntries(DECIDED.map((key) => [key, account[key]]));
35
+ // The profile carries the shared values; an account stored before profiles gets one now.
36
+ const shares = PROFILE_PROPERTIES.some((key) => key in set || unset.includes(key));
37
+ const profileNode = shares
38
+ ? await query(readProfile, { kind: 'User', node: account.id })
39
+ : undefined;
40
+ const merged = Object.fromEntries(Object.entries({ ...account, ...set }).filter(([key]) => !unset.includes(key)));
41
+ const created = profileNode === undefined ? newProfileValues(merged) : undefined;
42
+ const profile = !shares
43
+ ? undefined
44
+ : profileNode !== undefined
45
+ ? { id: profileNode }
46
+ : created === undefined
47
+ ? undefined
48
+ : { create: created };
49
+ const result = await mutate(updateAccount, {
50
+ user: account.id,
51
+ set: set,
52
+ unset,
53
+ expected,
54
+ ...(profile === undefined ? {} : { profile }),
55
+ });
56
+ return result.ok ? 'written' : result.refusal;
57
+ }
@@ -1 +1,2 @@
1
- export { assignViews } from './assign-views.js';
1
+ // Seam L-SHELL-12: the legacy handler the Runtime registers.
2
+ export { legacyAssignViews } from './legacy/index.js';
@@ -1,17 +1,21 @@
1
1
  import { defineAction } from '@astrale-os/sdk/action';
2
2
  import { Path } from '@astrale-os/sdk/graph/path';
3
- import { setViewAccess } from '../../mutations/view-access/index.js';
4
- import { readViewAccess } from '../../queries/view-access/index.js';
5
- import { canonicalNodeId } from '../../utils/canonical-node-path/index.js';
6
- import { viewAccessErrors } from './view-access-errors.js';
3
+ import { setViewAccess } from '../../../mutations/view-access/index.js';
4
+ import { readViewAccess } from '../../../queries/view-access/index.js';
5
+ import { canonicalNodeId } from '../../../utils/canonical-node-path/index.js';
6
+ import { legacyViewAccessErrors } from './view-access-errors.js';
7
7
  /**
8
- * One read and at most one Mutation, both with Shell's own authority: holding `can_use` on this
9
- * Function is what admits the caller, Core admin by default. The write changes only what differs
8
+ * @deprecated Give and withdraw Views for each identity in one write (L-SHELL-12). Replaced by a
9
+ * Mutation on `view_access`, which `workspace.admin` holders write. Removal: Shell major (delivery
10
+ * step 5) after 1Pact step 4.
11
+ *
12
+ * One read and at most one Mutation, both with Shell's own authority: `workspace.admin` admits the
13
+ * caller. The write changes only what differs
10
14
  * from the Edges read, under their guard, so a View already given or already withdrawn costs
11
15
  * nothing and a concurrent change refuses the whole write. An identity listed twice gets both
12
16
  * lists; one listed with no View is not read.
13
17
  */
14
- export const assignViews = defineAction()('assignViews', { errors: viewAccessErrors }, async ({ error, input, query, mutate }) => {
18
+ export const legacyAssignViews = defineAction()('assignViews', { errors: legacyViewAccessErrors }, async ({ error, input, query, mutate }) => {
15
19
  const nodeId = (reference) => {
16
20
  const invalid = () => error('INVALID_VIEW_ASSIGNMENT_REFERENCE', {});
17
21
  try {
@@ -0,0 +1 @@
1
+ export { legacyAssignViews } from './assign-views.js';