@jini-ai/user-management 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +201 -0
  3. package/README.md +104 -0
  4. package/dist/admin/adapters/http.d.ts +29 -0
  5. package/dist/admin/adapters/http.js +69 -0
  6. package/dist/admin/adapters/memory.d.ts +61 -0
  7. package/dist/admin/adapters/memory.js +217 -0
  8. package/dist/admin/auth.module.d.ts +30 -0
  9. package/dist/admin/auth.module.js +12 -0
  10. package/dist/admin/conformance/index.d.ts +56 -0
  11. package/dist/admin/conformance/index.js +156 -0
  12. package/dist/admin/controllers/auth.controller.d.ts +23 -0
  13. package/dist/admin/controllers/auth.controller.js +33 -0
  14. package/dist/admin/controllers/controller-store.d.ts +18 -0
  15. package/dist/admin/controllers/controller-store.js +23 -0
  16. package/dist/admin/controllers/members.controller.d.ts +33 -0
  17. package/dist/admin/controllers/members.controller.js +119 -0
  18. package/dist/admin/controllers/roles.controller.d.ts +47 -0
  19. package/dist/admin/controllers/roles.controller.js +282 -0
  20. package/dist/admin/controllers/users.controller.d.ts +53 -0
  21. package/dist/admin/controllers/users.controller.js +220 -0
  22. package/dist/admin/index.d.ts +21 -0
  23. package/dist/admin/index.js +15 -0
  24. package/dist/admin/members.module.d.ts +34 -0
  25. package/dist/admin/members.module.js +13 -0
  26. package/dist/admin/messages.en.d.ts +37 -0
  27. package/dist/admin/messages.en.js +21 -0
  28. package/dist/admin/models.d.ts +133 -0
  29. package/dist/admin/models.js +2 -0
  30. package/dist/admin/people.rules.d.ts +29 -0
  31. package/dist/admin/people.rules.js +51 -0
  32. package/dist/admin/ports.d.ts +187 -0
  33. package/dist/admin/ports.js +17 -0
  34. package/dist/admin/react/components/DestructiveDialog.d.ts +5 -0
  35. package/dist/admin/react/components/DestructiveDialog.js +6 -0
  36. package/dist/admin/react/components/MemberRow.d.ts +5 -0
  37. package/dist/admin/react/components/MemberRow.js +8 -0
  38. package/dist/admin/react/components/MembersConfirmation.d.ts +2 -0
  39. package/dist/admin/react/components/MembersConfirmation.js +8 -0
  40. package/dist/admin/react/components/PermissionPanel.d.ts +5 -0
  41. package/dist/admin/react/components/PermissionPanel.js +9 -0
  42. package/dist/admin/react/components/PolicyRow.d.ts +5 -0
  43. package/dist/admin/react/components/PolicyRow.js +11 -0
  44. package/dist/admin/react/components/RoleRow.d.ts +5 -0
  45. package/dist/admin/react/components/RoleRow.js +10 -0
  46. package/dist/admin/react/components/UserRow.d.ts +5 -0
  47. package/dist/admin/react/components/UserRow.js +8 -0
  48. package/dist/admin/react/components/UsersConfirmation.d.ts +3 -0
  49. package/dist/admin/react/components/UsersConfirmation.js +9 -0
  50. package/dist/admin/react/hooks/AuthBinding.hooks.d.ts +39 -0
  51. package/dist/admin/react/hooks/AuthBinding.hooks.js +21 -0
  52. package/dist/admin/react/hooks/LoginPage.hooks.d.ts +54 -0
  53. package/dist/admin/react/hooks/LoginPage.hooks.js +23 -0
  54. package/dist/admin/react/hooks/MembersBinding.hooks.d.ts +35 -0
  55. package/dist/admin/react/hooks/MembersBinding.hooks.js +21 -0
  56. package/dist/admin/react/hooks/MembersPage.hooks.d.ts +99 -0
  57. package/dist/admin/react/hooks/MembersPage.hooks.js +38 -0
  58. package/dist/admin/react/hooks/PeopleController.hooks.d.ts +18 -0
  59. package/dist/admin/react/hooks/PeopleController.hooks.js +20 -0
  60. package/dist/admin/react/hooks/PermissionPanel.hooks.d.ts +30 -0
  61. package/dist/admin/react/hooks/PermissionPanel.hooks.js +15 -0
  62. package/dist/admin/react/hooks/PoliciesTab.hooks.d.ts +47 -0
  63. package/dist/admin/react/hooks/PoliciesTab.hooks.js +26 -0
  64. package/dist/admin/react/hooks/RolesBinding.hooks.d.ts +39 -0
  65. package/dist/admin/react/hooks/RolesBinding.hooks.js +23 -0
  66. package/dist/admin/react/hooks/RolesController.hooks.d.ts +43 -0
  67. package/dist/admin/react/hooks/RolesController.hooks.js +25 -0
  68. package/dist/admin/react/hooks/RolesPage.hooks.d.ts +105 -0
  69. package/dist/admin/react/hooks/RolesPage.hooks.js +33 -0
  70. package/dist/admin/react/hooks/RolesTab.hooks.d.ts +35 -0
  71. package/dist/admin/react/hooks/RolesTab.hooks.js +19 -0
  72. package/dist/admin/react/hooks/UsersBinding.hooks.d.ts +36 -0
  73. package/dist/admin/react/hooks/UsersBinding.hooks.js +21 -0
  74. package/dist/admin/react/hooks/UsersPage.hooks.d.ts +106 -0
  75. package/dist/admin/react/hooks/UsersPage.hooks.js +22 -0
  76. package/dist/admin/react/hooks/UsersTab.hooks.d.ts +101 -0
  77. package/dist/admin/react/hooks/UsersTab.hooks.js +53 -0
  78. package/dist/admin/react/index.d.ts +203 -0
  79. package/dist/admin/react/index.js +32 -0
  80. package/dist/admin/react/pages/LoginPage.d.ts +3 -0
  81. package/dist/admin/react/pages/LoginPage.js +13 -0
  82. package/dist/admin/react/pages/MembersPage.d.ts +3 -0
  83. package/dist/admin/react/pages/MembersPage.js +19 -0
  84. package/dist/admin/react/pages/RolesPage.d.ts +3 -0
  85. package/dist/admin/react/pages/RolesPage.js +19 -0
  86. package/dist/admin/react/pages/UsersPage.d.ts +3 -0
  87. package/dist/admin/react/pages/UsersPage.js +19 -0
  88. package/dist/admin/react/tabs/AuthTab.d.ts +4 -0
  89. package/dist/admin/react/tabs/AuthTab.js +8 -0
  90. package/dist/admin/react/tabs/MembersTab.d.ts +4 -0
  91. package/dist/admin/react/tabs/MembersTab.js +9 -0
  92. package/dist/admin/react/tabs/PoliciesTab.d.ts +4 -0
  93. package/dist/admin/react/tabs/PoliciesTab.js +9 -0
  94. package/dist/admin/react/tabs/RolesTab.d.ts +4 -0
  95. package/dist/admin/react/tabs/RolesTab.js +9 -0
  96. package/dist/admin/react/tabs/UsersTab.d.ts +4 -0
  97. package/dist/admin/react/tabs/UsersTab.js +9 -0
  98. package/dist/admin/roles.module.d.ts +48 -0
  99. package/dist/admin/roles.module.js +15 -0
  100. package/dist/admin/rules.d.ts +32 -0
  101. package/dist/admin/rules.js +32 -0
  102. package/dist/admin/users.module.d.ts +39 -0
  103. package/dist/admin/users.module.js +14 -0
  104. package/dist/core/builtin-permissions.d.ts +10 -0
  105. package/dist/core/builtin-permissions.js +402 -0
  106. package/dist/core/index.d.ts +6 -0
  107. package/dist/core/index.js +9 -0
  108. package/dist/core/permissions.d.ts +40 -0
  109. package/dist/core/permissions.js +52 -0
  110. package/dist/core/ports.d.ts +237 -0
  111. package/dist/core/ports.js +2 -0
  112. package/dist/core/runtime-ports.d.ts +8 -0
  113. package/dist/core/runtime-ports.js +2 -0
  114. package/dist/core/types.d.ts +224 -0
  115. package/dist/core/types.js +93 -0
  116. package/dist/react/auth/Login.d.ts +9 -0
  117. package/dist/react/auth/Login.js +8 -0
  118. package/dist/react/auth/hooks/use-login.hooks.d.ts +22 -0
  119. package/dist/react/auth/hooks/use-login.hooks.js +24 -0
  120. package/dist/react/errors.d.ts +10 -0
  121. package/dist/react/errors.js +13 -0
  122. package/dist/react/index.d.ts +18 -0
  123. package/dist/react/index.js +9 -0
  124. package/dist/react/members/Members.d.ts +6 -0
  125. package/dist/react/members/Members.js +38 -0
  126. package/dist/react/members/hooks/use-members.hooks.d.ts +32 -0
  127. package/dist/react/members/hooks/use-members.hooks.js +120 -0
  128. package/dist/react/members/rules.d.ts +26 -0
  129. package/dist/react/members/rules.js +35 -0
  130. package/dist/react/models.d.ts +50 -0
  131. package/dist/react/models.js +2 -0
  132. package/dist/react/ports.d.ts +157 -0
  133. package/dist/react/ports.js +2 -0
  134. package/dist/react/roles/Roles.d.ts +100 -0
  135. package/dist/react/roles/Roles.hooks.d.ts +17 -0
  136. package/dist/react/roles/Roles.hooks.js +40 -0
  137. package/dist/react/roles/Roles.js +150 -0
  138. package/dist/react/roles/hooks/use-roles.hooks.d.ts +80 -0
  139. package/dist/react/roles/hooks/use-roles.hooks.js +303 -0
  140. package/dist/react/roles/rules.d.ts +35 -0
  141. package/dist/react/roles/rules.js +31 -0
  142. package/dist/react/roles/text.d.ts +27 -0
  143. package/dist/react/roles/text.js +13 -0
  144. package/dist/react/testing.d.ts +51 -0
  145. package/dist/react/testing.js +275 -0
  146. package/dist/react/users/Users.d.ts +72 -0
  147. package/dist/react/users/Users.js +129 -0
  148. package/dist/react/users/hooks/use-reset-password-fields.hooks.d.ts +16 -0
  149. package/dist/react/users/hooks/use-reset-password-fields.hooks.js +21 -0
  150. package/dist/react/users/hooks/use-users.hooks.d.ts +77 -0
  151. package/dist/react/users/hooks/use-users.hooks.js +286 -0
  152. package/dist/react/users/rules.d.ts +33 -0
  153. package/dist/react/users/rules.js +61 -0
  154. package/dist/server/admin-crud-service.d.ts +295 -0
  155. package/dist/server/admin-crud-service.js +561 -0
  156. package/dist/server/agent-tool-input.d.ts +58 -0
  157. package/dist/server/agent-tool-input.js +158 -0
  158. package/dist/server/agent-tool-registrations.d.ts +51 -0
  159. package/dist/server/agent-tool-registrations.js +417 -0
  160. package/dist/server/agent-tools.d.ts +88 -0
  161. package/dist/server/agent-tools.js +229 -0
  162. package/dist/server/auth-service.d.ts +107 -0
  163. package/dist/server/auth-service.js +170 -0
  164. package/dist/server/authorize.d.ts +92 -0
  165. package/dist/server/authorize.js +96 -0
  166. package/dist/server/grant-service.d.ts +219 -0
  167. package/dist/server/grant-service.js +419 -0
  168. package/dist/server/hasher.d.ts +64 -0
  169. package/dist/server/hasher.js +53 -0
  170. package/dist/server/identity-transaction.d.ts +9 -0
  171. package/dist/server/identity-transaction.js +8 -0
  172. package/dist/server/index.d.ts +18 -0
  173. package/dist/server/index.js +20 -0
  174. package/dist/server/messages.d.ts +7 -0
  175. package/dist/server/messages.js +5 -0
  176. package/dist/server/password-policy.d.ts +59 -0
  177. package/dist/server/password-policy.js +66 -0
  178. package/dist/server/permission-migrations.d.ts +84 -0
  179. package/dist/server/permission-migrations.js +77 -0
  180. package/dist/server/repo.memory-transactions.d.ts +16 -0
  181. package/dist/server/repo.memory-transactions.js +79 -0
  182. package/dist/server/repo.memory.d.ts +186 -0
  183. package/dist/server/repo.memory.js +230 -0
  184. package/dist/server/seed.d.ts +56 -0
  185. package/dist/server/seed.js +435 -0
  186. package/dist/server/session-tokens.d.ts +10 -0
  187. package/dist/server/session-tokens.js +12 -0
  188. package/dist/server/username.d.ts +14 -0
  189. package/dist/server/username.js +14 -0
  190. package/package.json +144 -0
@@ -0,0 +1,286 @@
1
+ import { interpolate } from "@jini-ai/ui/panel-kit";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import {} from "../../models.js";
4
+ import { useFetchMutation, useFetchQuery } from "@jini-ai/ui/panel-kit";
5
+ import { useAsyncAction } from "@jini-ai/ui/panel-kit";
6
+ import { describeApiError as describeFeatureError, KEYS } from "../rules.js";
7
+ async function runGrantMutation(generationAtStart, toggleGenerationRef, mutate, onSuccess, setGrantSaving, setGrantError, describeError) {
8
+ setGrantSaving(true);
9
+ setGrantError(null);
10
+ try {
11
+ await mutate();
12
+ if (toggleGenerationRef.current === generationAtStart)
13
+ onSuccess();
14
+ }
15
+ catch (e) {
16
+ if (toggleGenerationRef.current === generationAtStart)
17
+ setGrantError(describeError(e));
18
+ }
19
+ finally {
20
+ if (toggleGenerationRef.current === generationAtStart)
21
+ setGrantSaving(false);
22
+ }
23
+ }
24
+ export function useUsers({ port, translate, queryScope }, { openOwnPasswordReset = false, onOwnPasswordResetClosed } = {}) {
25
+ const describeApiError = (error, fallback) => describeFeatureError({ error, fallback, translate });
26
+ const t = translate;
27
+ const boundT = translate;
28
+ const portRef = useRef(port);
29
+ const list = useFetchQuery({
30
+ key: [queryScope, ...KEYS.list],
31
+ fetch: async () => {
32
+ const [u, r, p] = await Promise.all([port.listUsers({}), port.listRoles({}), port.listPolicies({})]);
33
+ return { users: u.users, roles: r.roles, policies: p.policies };
34
+ },
35
+ });
36
+ const users = list.data?.users ?? null;
37
+ const roles = list.data?.roles ?? null;
38
+ const policies = list.data?.policies ?? null;
39
+ const error = list.error ? describeApiError(list.error, t("failed to load users")) : null;
40
+ const [formOpen, setFormOpen] = useState(false);
41
+ const [username, setUsername] = useState("");
42
+ const [email, setEmail] = useState("");
43
+ const [password, setPassword] = useState("");
44
+ const createUser = useAsyncAction();
45
+ const createUserMutation = useFetchMutation({
46
+ run: ({ input }) => port.createUser({ username: input.username, password: input.password }, { email: input.email })
47
+ }, {
48
+ invalidates: [[queryScope, ...KEYS.list]],
49
+ });
50
+ const [expandedId, setExpandedId] = useState(null);
51
+ const toggleGenerationRef = useRef(0);
52
+ const [pendingRoleId, setPendingRoleId] = useState("");
53
+ const [pendingPolicyId, setPendingPolicyId] = useState("");
54
+ const [grantSaving, setGrantSaving] = useState(false);
55
+ const [grantError, setGrantError] = useState(null);
56
+ const assignRoleMutation = useFetchMutation({
57
+ run: ({ input }) => port.assignRole(input)
58
+ }, {
59
+ invalidates: [[queryScope, ...KEYS.list]],
60
+ });
61
+ const attachPolicyMutation = useFetchMutation({
62
+ run: ({ input }) => port.attachPolicy(input)
63
+ }, {
64
+ invalidates: [[queryScope, ...KEYS.list]],
65
+ });
66
+ const [editEmail, setEditEmail] = useState("");
67
+ const updateEmailMutation = useFetchMutation({
68
+ run: ({ input }) => port.updateUser({ principalId: input.principalId }, { email: input.email })
69
+ }, {
70
+ invalidates: [[queryScope, ...KEYS.list]],
71
+ });
72
+ const [toggleSavingId, setToggleSavingId] = useState(null);
73
+ const [toggleError, setToggleError] = useState(null);
74
+ const [notice, setNotice] = useState(null);
75
+ const toggleStatusMutation = useFetchMutation({
76
+ run: ({ input: user }) => (user.status === "active" ? port.disableUser({ principalId: user.principalId }) : port.enableUser({ principalId: user.principalId }))
77
+ }, {
78
+ invalidates: [[queryScope, ...KEYS.list]],
79
+ });
80
+ const [confirmingDisable, setConfirmingDisable] = useState(null);
81
+ const [resetPasswordFor, setResetPasswordFor] = useState(null);
82
+ const [newPassword, setNewPassword] = useState("");
83
+ const resetPassword = useAsyncAction();
84
+ const [ownPrincipalId, setOwnPrincipalId] = useState(null);
85
+ const [canManageUserTrash, setCanManageUserTrash] = useState(false);
86
+ useEffect(() => {
87
+ let cancelled = false;
88
+ portRef.current
89
+ .me({})
90
+ .then((res) => {
91
+ if (!cancelled) {
92
+ setOwnPrincipalId(res.user.id);
93
+ setCanManageUserTrash(res.canManageUserTrash);
94
+ }
95
+ })
96
+ .catch(() => {
97
+ });
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, []);
102
+ const deepLinkAttemptedRef = useRef(false);
103
+ const openedViaDeepLinkRef = useRef(false);
104
+ useEffect(() => {
105
+ if (!openOwnPasswordReset || deepLinkAttemptedRef.current)
106
+ return;
107
+ if (!users || ownPrincipalId === null)
108
+ return;
109
+ const ownRow = users.find((u) => u.principalId === ownPrincipalId);
110
+ if (!ownRow)
111
+ return;
112
+ deepLinkAttemptedRef.current = true;
113
+ openedViaDeepLinkRef.current = true;
114
+ openResetPassword(ownRow);
115
+ }, [openOwnPasswordReset, users, ownPrincipalId]);
116
+ useEffect(() => {
117
+ if (resetPasswordFor !== null || !openedViaDeepLinkRef.current)
118
+ return;
119
+ openedViaDeepLinkRef.current = false;
120
+ onOwnPasswordResetClosed?.();
121
+ }, [resetPasswordFor, onOwnPasswordResetClosed]);
122
+ const resetPasswordMutation = useFetchMutation({
123
+ run: ({ input }) => port.resetUserPassword(input),
124
+ });
125
+ async function onCreate(e) {
126
+ e.preventDefault();
127
+ await createUser.run({ action: async () => {
128
+ await createUserMutation.mutate({ input: { username, password, email: email || undefined } });
129
+ setUsername("");
130
+ setEmail("");
131
+ setPassword("");
132
+ setFormOpen(false);
133
+ }, describeError: (e) => describeApiError(e, t("failed to create user")) });
134
+ }
135
+ function toggleExpanded(user) {
136
+ setGrantError(null);
137
+ setGrantSaving(false);
138
+ setPendingRoleId("");
139
+ setPendingPolicyId("");
140
+ setEditEmail(user.email ?? "");
141
+ toggleGenerationRef.current += 1;
142
+ setExpandedId((current) => (current === user.principalId ? null : user.principalId));
143
+ }
144
+ async function onAssignRole({ principalId }) {
145
+ if (!pendingRoleId)
146
+ return;
147
+ await runGrantMutation(toggleGenerationRef.current, toggleGenerationRef, () => assignRoleMutation.mutate({ input: { principalId, roleId: pendingRoleId } }), () => setPendingRoleId(""), setGrantSaving, setGrantError, (e) => describeApiError(e, t("failed to assign role")));
148
+ }
149
+ async function onAttachPolicy({ principalId }) {
150
+ if (!pendingPolicyId)
151
+ return;
152
+ await runGrantMutation(toggleGenerationRef.current, toggleGenerationRef, () => attachPolicyMutation.mutate({ input: { principalId, policyId: pendingPolicyId } }), () => setPendingPolicyId(""), setGrantSaving, setGrantError, (e) => describeApiError(e, t("failed to attach policy")));
153
+ }
154
+ async function onSaveEmail({ principalId }) {
155
+ setGrantError(null);
156
+ try {
157
+ await updateEmailMutation.mutate({ input: { principalId, email: editEmail } });
158
+ }
159
+ catch (e) {
160
+ setGrantError(describeApiError(e, t("failed to update email")));
161
+ }
162
+ }
163
+ const emailSaving = updateEmailMutation.status === "pending";
164
+ function openResetPassword(user) {
165
+ if (toggleSavingId || resetPassword.saving)
166
+ return;
167
+ resetPassword.setError(null);
168
+ setNewPassword("");
169
+ setResetPasswordFor(user);
170
+ }
171
+ async function confirmResetPassword() {
172
+ if (!resetPasswordFor || !newPassword)
173
+ return;
174
+ await resetPassword.run({ action: async () => {
175
+ await resetPasswordMutation.mutate({ input: { principalId: resetPasswordFor.principalId, password: newPassword } });
176
+ setNotice(resetPasswordFor.principalId === ownPrincipalId
177
+ ? t("Password changed. Sign in again with your new password.")
178
+ : interpolate({ template: t('Password reset for "{username}" — every active session for this user was revoked.'), vars: { username: resetPasswordFor.username } }));
179
+ setResetPasswordFor(null);
180
+ setNewPassword("");
181
+ }, describeError: (e) => describeApiError(e, t("failed to reset password")) });
182
+ }
183
+ async function onToggleStatus(user) {
184
+ setToggleSavingId(user.principalId);
185
+ setToggleError(null);
186
+ try {
187
+ await toggleStatusMutation.mutate({ input: user });
188
+ }
189
+ catch (e) {
190
+ setToggleError(describeApiError(e, t("failed to change status")));
191
+ }
192
+ finally {
193
+ setToggleSavingId((current) => (current === user.principalId ? null : current));
194
+ }
195
+ }
196
+ function requestDisable(user) {
197
+ setToggleError(null);
198
+ setConfirmingDisable(user);
199
+ }
200
+ async function confirmDisable() {
201
+ if (!confirmingDisable)
202
+ return;
203
+ const user = confirmingDisable;
204
+ await onToggleStatus(user);
205
+ setConfirmingDisable((current) => (current?.principalId === user.principalId ? null : current));
206
+ }
207
+ const deleteUserMutation = useFetchMutation({
208
+ run: ({ input: principalId }) => port.deleteUser({ principalId: principalId })
209
+ }, {
210
+ invalidates: [[queryScope, ...KEYS.list]],
211
+ });
212
+ const [confirmingDelete, setConfirmingDelete] = useState(null);
213
+ function requestDelete(user) {
214
+ setToggleError(null);
215
+ setConfirmingDelete(user);
216
+ }
217
+ async function confirmDelete() {
218
+ if (!confirmingDelete)
219
+ return;
220
+ const user = confirmingDelete;
221
+ try {
222
+ await deleteUserMutation.mutate({ input: user.principalId });
223
+ }
224
+ catch (e) {
225
+ setToggleError(describeApiError(e, t("failed to delete user")));
226
+ }
227
+ setConfirmingDelete((current) => (current?.principalId === user.principalId ? null : current));
228
+ }
229
+ return {
230
+ users,
231
+ roles,
232
+ policies,
233
+ error,
234
+ formOpen,
235
+ setFormOpen,
236
+ username,
237
+ setUsername,
238
+ email,
239
+ setEmail,
240
+ password,
241
+ setPassword,
242
+ saving: createUser.saving,
243
+ formError: createUser.error,
244
+ onCreate,
245
+ expandedId,
246
+ toggleExpanded,
247
+ pendingRoleId,
248
+ setPendingRoleId,
249
+ pendingPolicyId,
250
+ setPendingPolicyId,
251
+ grantSaving,
252
+ grantError,
253
+ onAssignRole,
254
+ onAttachPolicy,
255
+ editEmail,
256
+ setEditEmail,
257
+ emailSaving,
258
+ onSaveEmail,
259
+ toggleSavingId,
260
+ toggleError,
261
+ notice,
262
+ confirmingDisable,
263
+ setConfirmingDisable,
264
+ requestDisable,
265
+ confirmDisable,
266
+ onToggleStatus,
267
+ resetPasswordFor,
268
+ setResetPasswordFor,
269
+ newPassword,
270
+ setNewPassword,
271
+ passwordSaving: resetPassword.saving,
272
+ passwordError: resetPassword.error,
273
+ setPasswordError: resetPassword.setError,
274
+ openResetPassword,
275
+ confirmResetPassword,
276
+ canManageUserTrash,
277
+ confirmingDelete,
278
+ setConfirmingDelete,
279
+ requestDelete,
280
+ deleteSaving: deleteUserMutation.status === "pending",
281
+ confirmDelete,
282
+ t: boundT,
283
+ translate,
284
+ };
285
+ }
286
+ //# sourceMappingURL=use-users.hooks.js.map
@@ -0,0 +1,33 @@
1
+ import type { AdminIdentityUser } from "../models.js";
2
+ import type { Translate } from "@jini-ai/ui/panel-kit";
3
+ import type { RowMenuItem } from "@jini-ai/admin/react";
4
+ import type { QueryKey } from "@jini-ai/ui/panel-kit";
5
+ export declare const KEYS: {
6
+ list: QueryKey;
7
+ };
8
+ export declare function describeApiError({ error, fallback, translate }: {
9
+ error: unknown;
10
+ fallback: string;
11
+ translate: Translate;
12
+ }): string;
13
+ export interface UserRowMenuHandlers {
14
+ onRequestDisable: (user: AdminIdentityUser) => void;
15
+ onEnable: (user: AdminIdentityUser) => void;
16
+ onManage: (user: AdminIdentityUser) => void;
17
+ onResetPassword: (user: AdminIdentityUser) => void;
18
+ onRequestDelete: (user: AdminIdentityUser) => void;
19
+ }
20
+ export declare function userRowMenuItems({ user, toggleSaving, handlers, translate, canDelete }: {
21
+ user: AdminIdentityUser;
22
+ toggleSaving: boolean;
23
+ handlers: UserRowMenuHandlers;
24
+ translate: Translate;
25
+ canDelete: boolean;
26
+ }): RowMenuItem[];
27
+ export declare function formatGrantLabel({ ids, byId }: {
28
+ ids: readonly string[];
29
+ byId: ReadonlyMap<string, {
30
+ name: string;
31
+ }>;
32
+ }): string | null;
33
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { describeIdentityError } from "../errors.js";
2
+ export const KEYS = {
3
+ list: ["users-roles-policies"],
4
+ };
5
+ const STATIC_ERROR_MESSAGES = {
6
+ GRANT_EXCEEDS_ISSUER: "You cannot grant a permission you do not hold.",
7
+ FORBIDDEN: "You do not have permission to do that.",
8
+ RESOURCE_CONFLICT: "That username is already in use.",
9
+ OWNER_REQUIRED: "The workspace must keep at least one active owner.",
10
+ SELF_DELETE: "You cannot delete your own account.",
11
+ USER_IN_TRASH: "This user is in the Trash; restore them first.",
12
+ USERNAME_IN_TRASH: "A user with this username is in the Trash; restore or delete them permanently first.",
13
+ };
14
+ export function describeApiError({ error, fallback, translate }) {
15
+ return describeIdentityError({ error, fallback, translate }, { messages: STATIC_ERROR_MESSAGES });
16
+ }
17
+ export function userRowMenuItems({ user, toggleSaving, handlers, translate, canDelete }) {
18
+ const items = [
19
+ {
20
+ key: "toggle",
21
+ label: user.status === "active" ? translate("Disable") : translate("Enable"),
22
+ tone: user.status === "active" ? "warning" : "default",
23
+ onSelect: () => {
24
+ if (toggleSaving)
25
+ return;
26
+ if (user.status === "active") {
27
+ handlers.onRequestDisable(user);
28
+ }
29
+ else {
30
+ handlers.onEnable(user);
31
+ }
32
+ },
33
+ },
34
+ {
35
+ key: "manage",
36
+ label: translate("Manage"),
37
+ onSelect: () => handlers.onManage(user),
38
+ },
39
+ {
40
+ key: "reset-password",
41
+ label: translate("Reset password"),
42
+ tone: "warning",
43
+ onSelect: () => handlers.onResetPassword(user),
44
+ },
45
+ ];
46
+ if (canDelete) {
47
+ items.push({
48
+ key: "delete",
49
+ label: translate("Delete"),
50
+ tone: "danger",
51
+ onSelect: () => handlers.onRequestDelete(user),
52
+ });
53
+ }
54
+ return items;
55
+ }
56
+ export function formatGrantLabel({ ids, byId }) {
57
+ if (ids.length === 0)
58
+ return null;
59
+ return ids.map((id) => byId.get(id)?.name ?? id).join(", ");
60
+ }
61
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1,295 @@
1
+ import type { UUID } from "@jini-ai/core/primitives";
2
+ import type { AuthServiceDeps } from "./auth-service.js";
3
+ import { type PolicyPermissionRecord, type PolicyRecord, type PrincipalRecord, type RoleRecord, type UserRecord } from "../core/types.js";
4
+ /**
5
+ * @file An amendment that completes the users/roles/policies admin CRUD surface
6
+ * (feature.spec.md..19).
7
+ *
8
+ * Purpose:
9
+ * Two of these nine transitions (`disablePrincipal`, `writePolicyPermission`) were fully specified
10
+ * since v0.5.0/v0.5.3 but never implemented — this file is their first implementation, not a
11
+ * re-specification. The other seven (`enablePrincipal`, `updateUser`,
12
+ * `resetUserPassword`, `updateRole`, `updatePolicy`, `deleteRole`, `deletePolicy`) are new
13
+ * transitions this amendment adds. Grouped in one file, separate from `grant-service.ts`'s
14
+ * "human grant-writing transitions" (`CREATE_*`/`ASSIGN_ROLE`/`ATTACH_POLICY`) because most of
15
+ * these are plain CRUD, not grant-writing — `writePolicyPermission` is the one exception (it IS
16
+ * -clamped) and reuses `grant-service.ts`'s exported `assertGrantClamp` rather than
17
+ * duplicating it.
18
+ *
19
+ * Architectural role:
20
+ * Ordinary core functions — same shape as `grant-service.ts`: one exported async
21
+ * function per transition, plain `Error` subclasses for control flow, the caller-permission gate
22
+ * enforced here (not pushed to the route layer).
23
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
24
+ */
25
+ /**
26
+ * `DISABLE_PRINCIPAL` (feature.spec.md, state.spec §3). Gated by
27
+ * `user.manage`. Refuses the seeded owner unconditionally ( CLI/core resolution target —
28
+ * disabling it would strand the CLI-only management plane) and refuses any disable that would drop
29
+ * the workspace's active owner-`*` count to zero — both checked, and the status update
30
+ * performed inside one identity transaction. Asynchronous repository calls yield even in a
31
+ * single-event-loop composition, so the transaction port must serialize competing owner/status
32
+ * writes for the whole workspace rather than rely on "atomic by construction" reasoning.
33
+ *
34
+ * `input.seededOwnerPrincipalId` is the caller's job to resolve (`await deps.ownerPrincipalId` at
35
+ * the route layer, mirroring `identityReady`'s existing await-before-use convention) — this
36
+ * function takes the already-resolved id so it stays a plain, directly-testable function.
37
+ *
38
+ * @complexity O(n) in the workspace's principal count (one `list` call to count active owner-`*`
39
+ * holders) — bounded by the same operator-managed-roster assumption `identity/INFO.md` already
40
+ * makes for `PrincipalRepoPort.list`.
41
+ * @overallScore 100
42
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
43
+ */
44
+ export declare function disablePrincipal(required: {
45
+ deps: AuthServiceDeps;
46
+ input: {
47
+ workspaceId: UUID;
48
+ callerPrincipalId: UUID;
49
+ principalId: UUID;
50
+ seededOwnerPrincipalId: UUID;
51
+ };
52
+ }): Promise<{
53
+ principal: PrincipalRecord;
54
+ }>;
55
+ /**
56
+ * `ENABLE_PRINCIPAL` (feature.spec.md ). Gated by `user.manage`. Target must be
57
+ * `kind='user'` — a `system`/`api_key` target is rejected `VALIDATION_ERROR` (mirrors
58
+ * human-only scoping; re-activating the disabled legacy `user-local` stub or an api_key principal
59
+ * outside `ISSUE_API_KEY`'s own lifecycle is never a valid call). Already-active is idempotent
60
+ * (mirrors `disablePrincipal`'s own already-disabled no-op above).
61
+ *
62
+ * @complexity O(1) — one lookup, one save.
63
+ * @overallScore 100
64
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
65
+ */
66
+ export declare function enablePrincipal(required: {
67
+ deps: AuthServiceDeps;
68
+ input: {
69
+ workspaceId: UUID;
70
+ callerPrincipalId: UUID;
71
+ principalId: UUID;
72
+ };
73
+ }): Promise<{
74
+ principal: PrincipalRecord;
75
+ }>;
76
+ /**
77
+ * `UPDATE_USER` (feature.spec.md ). Gated by `user.manage`; existing self-profile edits also
78
+ * accept `member.manage`. Only owners may edit owner targets — `email` only; `username`/`password` are not
79
+ * this transition's concern ( respectively). An absent or empty-string `email`
80
+ * clears the stored value to `undefined` ( — no distinct "blank but present" state).
81
+ *
82
+ * @complexity O(1) — one lookup, one save.
83
+ * @overallScore 100
84
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
85
+ */
86
+ export declare function updateUser(required: {
87
+ deps: AuthServiceDeps;
88
+ input: {
89
+ workspaceId: UUID;
90
+ callerPrincipalId: UUID;
91
+ principalId: UUID;
92
+ };
93
+ }, optional?: {
94
+ email?: string | undefined;
95
+ }): Promise<{
96
+ user: UserRecord;
97
+ }>;
98
+ /**
99
+ * `RESET_USER_PASSWORD` (feature.spec.md ). Gated by **`user.manage`** only
100
+ * (resetting a credential is at least as sensitive as disabling the account, own reasoning). Hashes the new password identically to `CREATE_USER` (argon2id,
101
+ * ), then revokes every one of the target's active sessions (idempotent no-op if it has none,
102
+ * ) — a reset that left old sessions alive would not actually contain a compromised account.
103
+ *
104
+ * Refuses a THIRD PARTY resetting the seeded owner's password ( reasoning, same
105
+ * `OwnerRequiredError` `disablePrincipal` uses): `user.manage` is independently grantable and not
106
+ * owner-exclusive (`permissions.ts` — "Create/disable operator users and principals"), so without
107
+ * this check a caller holding only that one delegated permission could set a password of their own
108
+ * choosing on the seeded owner's account and log in as owner — a full takeover from a routine
109
+ * delegation. Checked before the password-policy validation so the refusal doesn't depend on the
110
+ * caller supplying a well-formed password first.
111
+ *
112
+ * Deliberately NOT unconditional the way `disablePrincipal`'s owner check is: the owner resetting
113
+ * ITS OWN password (`callerPrincipalId === principalId === seededOwnerPrincipalId`) is refused
114
+ * nothing — that self-service path is real, shipped infrastructure
115
+ * (`reset-admin-password-self-verified.ts`, driven by both the `HOST_ADMIN_RESET_PASSWORD`
116
+ * boot-time incident-recovery hook and the `backfill-reset-admin-password.ts` CLI script), added
117
+ * specifically to let an operator recover a locked-out owner account out-of-band. Disabling has no
118
+ * legitimate self-case (an owner disabling itself only ever stands the workspace down), so
119
+ * `disablePrincipal` can refuse unconditionally; a credential rotation the owner performs on itself
120
+ * is ordinary hygiene and the documented recovery path, so only a MISMATCHED caller — the actual
121
+ * escalation shape — is refused here.
122
+ *
123
+ * Deliberately NOT -clamped (unlike `disablePrincipal`'s second guard): a reset changes a
124
+ * credential, it never changes `status`, so it can never drop the workspace's active owner-`*`
125
+ * count — headcount guard has nothing to protect here.
126
+ *
127
+ * `input.seededOwnerPrincipalId` is the caller's job to resolve (`await deps.ownerPrincipalId` at
128
+ * the route layer, mirroring `disablePrincipal`'s own contract above) — this function takes the
129
+ * already-resolved id so it stays a plain, directly-testable function.
130
+ *
131
+ * @complexity O(s) in the target's session count, s = active session rows to revoke — bounded by
132
+ * the same operator-managed-roster scale assumption every other identity list call here makes.
133
+ * @overallScore 100
134
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
135
+ */
136
+ export declare function resetUserPassword(required: {
137
+ deps: AuthServiceDeps;
138
+ input: {
139
+ workspaceId: UUID;
140
+ callerPrincipalId: UUID;
141
+ principalId: UUID;
142
+ password: string;
143
+ seededOwnerPrincipalId: UUID;
144
+ };
145
+ }): Promise<{
146
+ user: UserRecord;
147
+ }>;
148
+ /**
149
+ * `UPDATE_ROLE` (feature.spec.md ). Gated by `role.manage`. A built-in target is
150
+ * refused `VALIDATION_ERROR` — extends existing undeletable/immutable/un-attachable-to
151
+ * rule to also cover un-renameable (same escalation shape: relabeling `viewer` to read as trusted
152
+ * is a social-engineering variant of what already blocks structurally).
153
+ *
154
+ * @complexity O(1) — one lookup, one save.
155
+ * @overallScore 100
156
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
157
+ */
158
+ export declare function updateRole(required: {
159
+ deps: AuthServiceDeps;
160
+ input: {
161
+ workspaceId: UUID;
162
+ callerPrincipalId: UUID;
163
+ roleId: UUID;
164
+ name: string;
165
+ };
166
+ }): Promise<{
167
+ role: RoleRecord;
168
+ }>;
169
+ /**
170
+ * `UPDATE_POLICY` (feature.spec.md ). Gated by `role.manage`. A built-in or
171
+ * `is_frozen` target is refused `VALIDATION_ERROR` — the frozen-policy half mirrors
172
+ * `WRITE_POLICY_PERMISSION`'s existing refusal : an issuance snapshot's identity is as
173
+ * immutable as its permission set. At least one of `name`/`description` must be present.
174
+ *
175
+ * @complexity O(1) — one lookup, one save.
176
+ * @overallScore 100
177
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
178
+ */
179
+ export declare function updatePolicy(required: {
180
+ deps: AuthServiceDeps;
181
+ input: {
182
+ workspaceId: UUID;
183
+ callerPrincipalId: UUID;
184
+ policyId: UUID;
185
+ };
186
+ }, optional?: {
187
+ name?: string | undefined;
188
+ description?: string | undefined;
189
+ }): Promise<{
190
+ policy: PolicyRecord;
191
+ }>;
192
+ /**
193
+ * `DELETE_ROLE` (feature.spec.md ). Gated by `role.manage`. A built-in
194
+ * target is refused `VALIDATION_ERROR` (checked before the reference count, matching
195
+ * stated precedence). A still-referenced target (≥1 `principal_roles` row) is refused
196
+ * `IdentityConflictError` (409 `RESOURCE_CONFLICT`) — the reference check and the delete happen in
197
+ * one identity transaction ( atomicity, same isolation requirement as
198
+ * `disablePrincipal`'s owner-count guard above). Concurrent grant writers use the same port so
199
+ * no assignment can appear between the reference check and deletion.
200
+ *
201
+ * @complexity O(a) in the role's assignment count (`principalRoles.listByRoleId`) — bounded by the
202
+ * same operator-managed-roster assumption every other identity list call makes.
203
+ * @overallScore 100
204
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
205
+ */
206
+ export declare function deleteRole(required: {
207
+ deps: AuthServiceDeps;
208
+ input: {
209
+ workspaceId: UUID;
210
+ callerPrincipalId: UUID;
211
+ roleId: UUID;
212
+ };
213
+ }): Promise<void>;
214
+ /**
215
+ * `DELETE_POLICY` (feature.spec.md ). Gated by `role.manage`. A
216
+ * built-in or `is_frozen` target is refused `VALIDATION_ERROR` (checked before the reference
217
+ * count). A still-referenced target (≥1 `role_policies` or `principal_policies` row) is refused
218
+ * `IdentityConflictError`. On success, cascades to the policy's OWN `policy_permissions` rows only
219
+ * (never a different policy's) before deleting the policy row itself. Both deletes and the
220
+ * reference guard share one transaction; failure rolls back the permission cascade.
221
+ *
222
+ * @complexity O(r + p) — role-reference count + principal-reference count, both bounded by the same
223
+ * operator-managed-roster assumption every other identity list call makes.
224
+ * @overallScore 100
225
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
226
+ */
227
+ export declare function deletePolicy(required: {
228
+ deps: AuthServiceDeps;
229
+ input: {
230
+ workspaceId: UUID;
231
+ callerPrincipalId: UUID;
232
+ policyId: UUID;
233
+ };
234
+ }): Promise<void>;
235
+ /**
236
+ * `WRITE_POLICY_PERMISSION` (feature.spec.md, state.spec §3). First HTTP
237
+ * implementation of a transition specified since v0.5.3 ( — this is a route, not a
238
+ * re-specification). Gated by `role.manage`. Refuses an unregistered permission
239
+ * (`PermissionUnknownError`), a built-in or `is_frozen` parent policy, and
240
+ * (via the shared `assertGrantClamp`) a permission the caller does not itself hold unconstrained
241
+ * (`GrantExceedsIssuerError`).
242
+ *
243
+ * @complexity O(assertGrantClamp) — see that function's own doc (resolveEffectivePermissions +
244
+ * O(1) local filtering, one permission).
245
+ * @overallScore 100
246
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
247
+ */
248
+ export declare function writePolicyPermission(required: {
249
+ deps: AuthServiceDeps;
250
+ input: {
251
+ workspaceId: UUID;
252
+ callerPrincipalId: UUID;
253
+ policyId: UUID;
254
+ permission: string;
255
+ };
256
+ }, optional?: {
257
+ resourceType?: string | null;
258
+ constraintJson?: string | null;
259
+ }): Promise<{
260
+ policyPermission: PolicyPermissionRecord;
261
+ }>;
262
+ /**
263
+ * `REMOVE_POLICY_PERMISSION` — the inverse of {@link writePolicyPermission}, and the
264
+ * transition whose absence made a policy's permission set append-only: the only way to shrink one
265
+ * was `deletePolicy` + recreate, which blocks outright the moment anything references the
266
+ * policy.
267
+ *
268
+ * Gated by `role.manage`, the same permission `writePolicyPermission` and `deletePolicy` require.
269
+ * Deliberately NOT clamped by `assertGrantClamp` : the clamp exists to stop a caller
270
+ * conferring authority it does not itself hold, and removal confers nothing — it is a strict
271
+ * de-escalation. Requiring the clamp here would mean an admin who cannot grant `content.write`
272
+ * also cannot take it back, which is the wrong direction for a safety rule. This is strictly less
273
+ * powerful than the already-unclamped `deletePolicy`, which drops EVERY permission row the policy
274
+ * has.
275
+ *
276
+ * Refuses a built-in or `is_frozen` parent policy, checked BEFORE the row lookup so
277
+ * a frozen policy reports why it is frozen rather than leaking whether the row exists. A row that
278
+ * is absent — or that exists but belongs to a DIFFERENT policy — is one and the same
279
+ * `IdentityNotFoundError`: membership is proven from `listByPolicyId`'s result, so a caller can
280
+ * never delete across a policy boundary by guessing an id.
281
+ *
282
+ * @complexity O(n) in the policy's own permission-row count (one `listByPolicyId`) — bounded by the
283
+ * same operator-managed-roster assumption every other identity list call makes.
284
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
285
+ */
286
+ export declare function removePolicyPermission(required: {
287
+ deps: AuthServiceDeps;
288
+ input: {
289
+ workspaceId: UUID;
290
+ callerPrincipalId: UUID;
291
+ policyId: UUID;
292
+ policyPermissionId: UUID;
293
+ };
294
+ }): Promise<void>;
295
+ //# sourceMappingURL=admin-crud-service.d.ts.map