@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,119 @@
1
+ import { hasAdminGrant, describePeopleError } from '../people.rules.js';
2
+ import { createControllerStore } from './controller-store.js';
3
+ const emptyRow = Object.freeze({ disabling: false, resending: false, error: null, notice: null });
4
+ /** Separate list and detail generations prevent stale errors/spinners from painting onto a
5
+ * different row, including collapse/reopen of the same row. Refresh invalidates cached detail. */
6
+ export function createMembersController({ api }, { permissions = [], refresh } = {}) {
7
+ const grants = Object.freeze([...permissions]);
8
+ const store = createControllerStore({ initial: { members: null, error: null, rows: {}, expandedId: null, detailById: {}, detailError: null, detailLoadingId: null, pending: null, confirming: false } });
9
+ const read = store.getSnapshot, call = store.call, set = (patch) => store.set({ patch });
10
+ let listGeneration = 0, detailGeneration = 0;
11
+ const report = (error, fallback) => describePeopleError({ error, fallback });
12
+ const row = (id) => read({}).members?.find(m => m.id === id);
13
+ function stateFor({ id }, _optional = {}) { return read({}).rows[id] ?? emptyRow; }
14
+ function patchRow(id, patch) { set({ rows: Object.freeze({ ...read({}).rows, [id]: Object.freeze({ ...stateFor({ id }), ...patch }) }) }); }
15
+ const gate = () => store.active({}) && hasAdminGrant({ permissions: grants, permission: 'member.manage' });
16
+ async function loadDetail(id) {
17
+ const generation = ++detailGeneration;
18
+ set({ detailLoadingId: id, detailError: null });
19
+ try {
20
+ const { member } = await api.getMember({ id }, call);
21
+ if (generation === detailGeneration && read({}).expandedId === id)
22
+ set({ detailById: Object.freeze({ ...read({}).detailById, [id]: Object.freeze({ ...member }) }) });
23
+ }
24
+ catch (error) {
25
+ if (generation === detailGeneration && read({}).expandedId === id)
26
+ set({ detailError: report(error, 'Failed to load member detail.') });
27
+ }
28
+ finally {
29
+ if (generation === detailGeneration)
30
+ set({ detailLoadingId: null });
31
+ }
32
+ }
33
+ async function load(_required, _optional = {}) {
34
+ if (!store.active({}))
35
+ return;
36
+ const generation = ++listGeneration;
37
+ set({ error: null });
38
+ try {
39
+ const { members } = await api.listMembers({}, call);
40
+ if (generation !== listGeneration || !store.active({}))
41
+ return;
42
+ ++detailGeneration;
43
+ set({ members: Object.freeze(members.map(m => Object.freeze({ ...m }))), detailById: {}, detailError: null, detailLoadingId: null });
44
+ const id = read({}).expandedId;
45
+ if (id && row(id))
46
+ await loadDetail(id);
47
+ else if (id)
48
+ set({ expandedId: null });
49
+ }
50
+ catch (error) {
51
+ if (generation === listGeneration)
52
+ set({ error: report(error, 'failed to load members') });
53
+ }
54
+ }
55
+ async function toggleDetail({ id }, _optional = {}) {
56
+ if (!store.active({}) || !row(id))
57
+ return;
58
+ ++detailGeneration;
59
+ const next = read({}).expandedId === id ? null : id;
60
+ set({ expandedId: next, detailError: null, detailLoadingId: null });
61
+ if (next && !read({}).detailById[next])
62
+ await loadDetail(next);
63
+ }
64
+ async function resendSignInLink({ id }, _optional = {}) {
65
+ const member = row(id);
66
+ if (!member || !gate() || stateFor({ id }).resending)
67
+ return false;
68
+ patchRow(id, { resending: true, error: null, notice: null });
69
+ try {
70
+ await api.requestMemberMagicLink({ email: member.email }, call);
71
+ patchRow(id, { notice: 'Sign-in link sent.' });
72
+ return store.active({});
73
+ }
74
+ catch (error) {
75
+ patchRow(id, { error: report(error, 'Failed to send sign-in link.') });
76
+ return false;
77
+ }
78
+ finally {
79
+ patchRow(id, { resending: false });
80
+ }
81
+ }
82
+ async function confirmDestructive(_required, _optional = {}) {
83
+ const target = read({}).pending;
84
+ if (!target || !gate() || read({}).confirming || !row(target.id))
85
+ return false;
86
+ set({ confirming: true });
87
+ patchRow(target.id, { disabling: true, error: null, notice: null });
88
+ try {
89
+ const { member } = await api.disableMember({ id: target.id }, call);
90
+ // Invalidate older reads before applying the write response, including the cached detail.
91
+ ++listGeneration;
92
+ ++detailGeneration;
93
+ set({ members: Object.freeze((read({}).members ?? []).map(m => m.id === member.id ? Object.freeze({ ...member }) : m)),
94
+ detailById: Object.freeze({ ...read({}).detailById, [member.id]: Object.freeze({ ...member }) }), detailLoadingId: null });
95
+ patchRow(target.id, { notice: 'Member disabled.' });
96
+ return store.active({});
97
+ }
98
+ catch (error) {
99
+ patchRow(target.id, { error: report(error, 'Failed to disable member.') });
100
+ return false;
101
+ }
102
+ finally {
103
+ patchRow(target.id, { disabling: false });
104
+ if (read({}).pending === target)
105
+ set({ pending: null });
106
+ set({ confirming: false });
107
+ }
108
+ }
109
+ const unsubscribe = refresh?.subscribe({ onRefresh: () => { void load({}); } });
110
+ return {
111
+ getSnapshot: store.getSnapshot, subscribe: store.subscribe, load, stateFor, toggleDetail, resendSignInLink, confirmDestructive,
112
+ requestDestructive({ id }, _optional = {}) { const member = row(id); if (gate() && member && member.status !== 'disabled' && !read({}).confirming)
113
+ set({ pending: Object.freeze({ ...member }) }); },
114
+ cancelDestructive(_required, _optional = {}) { if (!read({}).confirming)
115
+ set({ pending: null }); },
116
+ dispose(_required = {}, _optional = {}) { unsubscribe?.(); store.dispose({}); ++listGeneration; ++detailGeneration; },
117
+ };
118
+ }
119
+ //# sourceMappingURL=members.controller.js.map
@@ -0,0 +1,47 @@
1
+ import type { RolesApiPort } from '../ports.js';
2
+ import type { RolesState, RolesDraft, RoleRecord, PolicyRecord, DestructiveTarget } from '../models.js';
3
+ /** One controller backs both tabs: two hook calls would double reads and split shared errors.
4
+ * The lists always refresh together; permission edits only refresh the currently open panel.
5
+ * Controller methods use synchronous state, never React state updater side effects. */
6
+ export declare function createRolesController({ api }: {
7
+ api: RolesApiPort;
8
+ }, { permissions }?: {
9
+ permissions?: readonly string[];
10
+ }): {
11
+ getSnapshot(_required?: Record<string, never>, _optional?: Record<string, never>): RolesState;
12
+ subscribe({ listener }: {
13
+ listener: () => void;
14
+ }, _optional?: Record<string, never>): () => void;
15
+ dispose(_required?: Record<string, never>, _optional?: Record<string, never>): void;
16
+ setDraft({ patch }: {
17
+ patch: Partial<RolesDraft>;
18
+ }, _optional?: Record<string, never>): void;
19
+ cancelEdit({ kind }: {
20
+ kind: "role" | "policy";
21
+ }, _optional?: Record<string, never>): void;
22
+ load: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<void>;
23
+ loadPermissions: ({ policyId }: {
24
+ policyId: string;
25
+ }, _optional?: Record<string, never>) => Promise<void>;
26
+ togglePermissions: ({ policyId }: {
27
+ policyId: string;
28
+ }, _optional?: Record<string, never>) => Promise<void>;
29
+ createRole: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
30
+ createPolicy: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
31
+ editRole: ({ role: r }: {
32
+ role: RoleRecord;
33
+ }, _optional?: Record<string, never>) => void;
34
+ editPolicy: ({ policy: p }: {
35
+ policy: PolicyRecord;
36
+ }, _optional?: Record<string, never>) => void;
37
+ saveRole: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
38
+ savePolicy: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
39
+ writePermission: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
40
+ requestDestructive: ({ target }: {
41
+ target: DestructiveTarget;
42
+ }, _optional?: Record<string, never>) => void;
43
+ cancelDestructive: (_required: Record<string, never>, _optional?: Record<string, never>) => void;
44
+ confirmDestructive: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
45
+ };
46
+ export type RolesController = ReturnType<typeof createRolesController>;
47
+ //# sourceMappingURL=roles.controller.d.ts.map
@@ -0,0 +1,282 @@
1
+ import { canManageRoles, mutableRole, mutablePolicy, describeRolesError } from '../rules.js';
2
+ import { rolesMessagesEn as m } from '../messages.en.js';
3
+ const initial = {
4
+ roles: null, policies: null, error: null, rowError: null, roleError: null, policyError: null,
5
+ roleName: '', policyName: '', policyDescription: '', roleSaving: false, policySaving: false, rowSavingId: null,
6
+ editingRoleId: null, editingRoleName: '', editingPolicyId: null, editingPolicyName: '', editingPolicyDescription: '',
7
+ permissionPolicyId: null, permissionInput: '', resourceTypeInput: '', permissionRows: Object.freeze([]), permissionsLoading: false,
8
+ removingPermissionId: null, pending: null, confirming: false,
9
+ };
10
+ /** One controller backs both tabs: two hook calls would double reads and split shared errors.
11
+ * The lists always refresh together; permission edits only refresh the currently open panel.
12
+ * Controller methods use synchronous state, never React state updater side effects. */
13
+ export function createRolesController({ api }, { permissions = [] } = {}) {
14
+ const grants = Object.freeze([...permissions]);
15
+ let state = Object.freeze({ ...initial }), disposed = false, listGeneration = 0, permissionGeneration = 0, panelGeneration = 0;
16
+ const listeners = new Set();
17
+ const abort = new AbortController();
18
+ const call = { signal: abort.signal };
19
+ const set = (patch) => {
20
+ if (disposed)
21
+ return;
22
+ const immutablePatch = patch.permissionRows ? { ...patch, permissionRows: Object.freeze([...patch.permissionRows]) } : patch;
23
+ state = Object.freeze({ ...state, ...immutablePatch });
24
+ for (const listener of listeners)
25
+ listener();
26
+ };
27
+ const report = (error, fallback) => describeRolesError({ error, fallback });
28
+ const gate = () => {
29
+ if (disposed || state.confirming)
30
+ return false;
31
+ if (!canManageRoles({ permissions: grants })) {
32
+ set({ rowError: m.forbidden });
33
+ return false;
34
+ }
35
+ return true;
36
+ };
37
+ const role = (id) => state.roles?.find(r => r.id === id);
38
+ const policy = (id) => state.policies?.find(p => p.id === id);
39
+ const roleGate = (r) => gate() && !!r && (mutableRole({ role: r }) || (set({ rowError: m.immutable }), false));
40
+ const policyGate = (p) => gate() && !!p && (mutablePolicy({ policy: p }) || (set({ rowError: m.immutable }), false));
41
+ const rows = (items) => Object.freeze(items.map(r => Object.freeze({ ...r })));
42
+ async function load(_required, _optional = {}) {
43
+ if (disposed)
44
+ return;
45
+ const generation = ++listGeneration;
46
+ set({ error: null });
47
+ try {
48
+ const [r, p] = await Promise.all([api.listRoles({}, call), api.listPolicies({}, call)]);
49
+ if (generation === listGeneration)
50
+ set({ roles: rows(r.roles), policies: rows(p.policies) });
51
+ }
52
+ catch (error) {
53
+ if (generation === listGeneration)
54
+ set({ error: report(error, 'Failed to load roles/policies.') });
55
+ }
56
+ }
57
+ async function loadPermissions({ policyId }, _optional = {}) {
58
+ if (disposed || state.permissionPolicyId !== policyId)
59
+ return;
60
+ // Start order, rather than completion order, owns rows AND failures. Closing also invalidates.
61
+ const generation = ++permissionGeneration;
62
+ set({ permissionsLoading: true });
63
+ try {
64
+ const result = await api.listPolicyPermissions({ policyId }, call);
65
+ if (generation === permissionGeneration && state.permissionPolicyId === policyId)
66
+ set({ permissionRows: rows(result.policyPermissions) });
67
+ }
68
+ catch (error) {
69
+ if (generation === permissionGeneration && state.permissionPolicyId === policyId)
70
+ set({ permissionRows: [], rowError: report(error, 'Failed to load permissions.') });
71
+ }
72
+ finally {
73
+ if (generation === permissionGeneration)
74
+ set({ permissionsLoading: false });
75
+ }
76
+ }
77
+ async function togglePermissions({ policyId }, _optional = {}) {
78
+ if (disposed || !policy(policyId))
79
+ return;
80
+ const next = state.permissionPolicyId === policyId ? null : policyId;
81
+ ++panelGeneration;
82
+ ++permissionGeneration;
83
+ // Clear first so reopening cannot flash the previous policy's permissions.
84
+ set({ permissionPolicyId: next, permissionRows: [], permissionsLoading: false, permissionInput: '', resourceTypeInput: '', rowError: null });
85
+ if (next)
86
+ await loadPermissions({ policyId: next });
87
+ }
88
+ async function createRole(_required, _optional = {}) {
89
+ if (!gate() || state.roleSaving)
90
+ return false;
91
+ const name = state.roleName;
92
+ set({ roleSaving: true, roleError: null });
93
+ try {
94
+ await api.createRole({ name }, call);
95
+ if (state.roleName === name)
96
+ set({ roleName: '' });
97
+ await load({});
98
+ return !disposed;
99
+ }
100
+ catch (error) {
101
+ set({ roleError: report(error, 'Failed to create role.') });
102
+ return false;
103
+ }
104
+ finally {
105
+ set({ roleSaving: false });
106
+ }
107
+ }
108
+ async function createPolicy(_required, _optional = {}) {
109
+ if (!gate() || state.policySaving)
110
+ return false;
111
+ const name = state.policyName, description = state.policyDescription;
112
+ set({ policySaving: true, policyError: null });
113
+ try {
114
+ await api.createPolicy({ name }, { ...call, ...(description ? { description } : {}) });
115
+ if (state.policyName === name && state.policyDescription === description)
116
+ set({ policyName: '', policyDescription: '' });
117
+ await load({});
118
+ return !disposed;
119
+ }
120
+ catch (error) {
121
+ set({ policyError: report(error, 'Failed to create policy.') });
122
+ return false;
123
+ }
124
+ finally {
125
+ set({ policySaving: false });
126
+ }
127
+ }
128
+ function editRole({ role: r }, _optional = {}) {
129
+ if (roleGate(role(r.id)))
130
+ set({ editingRoleId: r.id, editingRoleName: r.name, rowError: null });
131
+ }
132
+ function editPolicy({ policy: p }, _optional = {}) {
133
+ if (policyGate(policy(p.id)))
134
+ set({ editingPolicyId: p.id, editingPolicyName: p.name, editingPolicyDescription: p.description ?? '', rowError: null });
135
+ }
136
+ async function saveRole(_required, _optional = {}) {
137
+ const id = state.editingRoleId, name = state.editingRoleName;
138
+ if (!id || !roleGate(role(id)) || state.rowSavingId)
139
+ return false;
140
+ set({ rowSavingId: id, rowError: null });
141
+ try {
142
+ await api.updateRole({ roleId: id, name }, call);
143
+ // A late save must not close another row's newer edit.
144
+ if (state.editingRoleId === id && state.editingRoleName === name)
145
+ set({ editingRoleId: null });
146
+ await load({});
147
+ return !disposed;
148
+ }
149
+ catch (error) {
150
+ set({ rowError: report(error, 'Failed to rename role.') });
151
+ return false;
152
+ }
153
+ finally {
154
+ if (state.rowSavingId === id)
155
+ set({ rowSavingId: null });
156
+ }
157
+ }
158
+ async function savePolicy(_required, _optional = {}) {
159
+ const id = state.editingPolicyId, name = state.editingPolicyName, description = state.editingPolicyDescription;
160
+ if (!id || !policyGate(policy(id)) || state.rowSavingId)
161
+ return false;
162
+ set({ rowSavingId: id, rowError: null });
163
+ try {
164
+ await api.updatePolicy({ policyId: id }, { ...call, name, description });
165
+ if (state.editingPolicyId === id && state.editingPolicyName === name && state.editingPolicyDescription === description)
166
+ set({ editingPolicyId: null });
167
+ await load({});
168
+ return !disposed;
169
+ }
170
+ catch (error) {
171
+ set({ rowError: report(error, 'Failed to update policy.') });
172
+ return false;
173
+ }
174
+ finally {
175
+ if (state.rowSavingId === id)
176
+ set({ rowSavingId: null });
177
+ }
178
+ }
179
+ async function writePermission(_required, _optional = {}) {
180
+ const id = state.permissionPolicyId, permission = state.permissionInput, resourceType = state.resourceTypeInput;
181
+ if (!id || !permission || !policyGate(policy(id)) || state.rowSavingId)
182
+ return false;
183
+ const generation = panelGeneration;
184
+ set({ rowSavingId: id, rowError: null });
185
+ try {
186
+ await api.writePolicyPermission({ policyId: id, permission }, { ...call, ...(resourceType ? { resourceType } : {}) });
187
+ // Ordering alone cannot protect a trailing refresh: it always mints the newest generation.
188
+ // Check the CURRENT panel identity, and preserve a newer draft even on the same panel.
189
+ if (generation === panelGeneration && state.permissionInput === permission && state.resourceTypeInput === resourceType)
190
+ set({ permissionInput: '', resourceTypeInput: '' });
191
+ if (state.permissionPolicyId === id)
192
+ await loadPermissions({ policyId: id });
193
+ return !disposed;
194
+ }
195
+ catch (error) {
196
+ set({ rowError: report(error, 'Failed to add permission.') });
197
+ return false;
198
+ }
199
+ finally {
200
+ if (state.rowSavingId === id)
201
+ set({ rowSavingId: null });
202
+ }
203
+ }
204
+ function targetGate(target) {
205
+ if (target.kind === 'role')
206
+ return roleGate(role(target.role.id));
207
+ return policyGate(policy(target.kind === 'policy' ? target.policy.id : target.policyId));
208
+ }
209
+ function requestDestructive({ target }, _optional = {}) {
210
+ if (state.confirming || !targetGate(target))
211
+ return;
212
+ if (target.kind === 'permission' && (state.permissionPolicyId !== target.policyId || !state.permissionRows.some(r => r.id === target.row.id)))
213
+ return;
214
+ // The menu closes after one click; shell confirmation survives that closure. Revoke also
215
+ // needs confirmation: removing from an attached policy immediately changes live authority.
216
+ const captured = target.kind === 'role' ? Object.freeze({ kind: 'role', role: Object.freeze({ ...target.role }) }) : target.kind === 'policy' ? Object.freeze({ kind: 'policy', policy: Object.freeze({ ...target.policy }) }) : Object.freeze({ kind: 'permission', policyId: target.policyId, row: Object.freeze({ ...target.row }) });
217
+ set({ pending: captured, rowError: null });
218
+ }
219
+ function cancelDestructive(_required, _optional = {}) {
220
+ if (!state.confirming)
221
+ set({ pending: null });
222
+ }
223
+ async function confirmDestructive(_required, _optional = {}) {
224
+ const target = state.pending;
225
+ if (!target || state.confirming || state.rowSavingId || !targetGate(target))
226
+ return false;
227
+ set({ confirming: true, rowError: null });
228
+ try {
229
+ if (target.kind === 'role') {
230
+ await api.deleteRole({ roleId: target.role.id }, call);
231
+ await load({});
232
+ }
233
+ else if (target.kind === 'policy') {
234
+ await api.deletePolicy({ policyId: target.policy.id }, call);
235
+ if (state.permissionPolicyId === target.policy.id) {
236
+ ++permissionGeneration;
237
+ ++panelGeneration;
238
+ set({ permissionPolicyId: null, permissionRows: [], permissionsLoading: false });
239
+ }
240
+ await load({});
241
+ }
242
+ else {
243
+ set({ removingPermissionId: target.row.id });
244
+ await api.removePolicyPermission({ policyId: target.policyId, policyPermissionId: target.row.id }, call);
245
+ if (state.permissionPolicyId === target.policyId)
246
+ await loadPermissions({ policyId: target.policyId });
247
+ }
248
+ return !disposed;
249
+ }
250
+ catch (error) {
251
+ set({ rowError: report(error, target.kind === 'permission' ? 'Failed to remove permission.' : `Failed to delete ${target.kind}.`) });
252
+ return false;
253
+ }
254
+ finally {
255
+ if (state.pending === target)
256
+ set({ pending: null });
257
+ set({ confirming: false, removingPermissionId: null });
258
+ }
259
+ }
260
+ return {
261
+ getSnapshot(_required = {}, _optional = {}) { return state; },
262
+ subscribe({ listener }, _optional = {}) {
263
+ if (!disposed)
264
+ listeners.add(listener);
265
+ return () => { listeners.delete(listener); };
266
+ },
267
+ dispose(_required = {}, _optional = {}) {
268
+ if (disposed)
269
+ return;
270
+ disposed = true;
271
+ abort.abort();
272
+ ++listGeneration;
273
+ ++permissionGeneration;
274
+ listeners.clear();
275
+ },
276
+ setDraft({ patch }, _optional = {}) { set(patch); },
277
+ cancelEdit({ kind }, _optional = {}) { set(kind === 'role' ? { editingRoleId: null } : { editingPolicyId: null }); },
278
+ load, loadPermissions, togglePermissions, createRole, createPolicy, editRole, editPolicy, saveRole, savePolicy, writePermission,
279
+ requestDestructive, cancelDestructive, confirmDestructive,
280
+ };
281
+ }
282
+ //# sourceMappingURL=roles.controller.js.map
@@ -0,0 +1,53 @@
1
+ import type { UsersApiPort, UsersSafetyPort } from '../ports.js';
2
+ import type { UsersState, UsersDraft, UserAction } from '../models.js';
3
+ /** Users, roles and policies refresh together. Only the caller read is best-effort: failure keeps
4
+ * all caller-dependent actions closed. Each panel transition invalidates both success and error
5
+ * settlements, including close/reopen of the same row. Password resets do not refresh the table. */
6
+ export declare function createUsersController({ api, safety }: {
7
+ api: UsersApiPort;
8
+ safety: UsersSafetyPort;
9
+ }, { permissions, openOwnPasswordReset, onOwnPasswordResetClosed, }?: {
10
+ permissions?: readonly string[];
11
+ openOwnPasswordReset?: boolean;
12
+ onOwnPasswordResetClosed?: (required: Record<string, never>, optional?: Record<string, never>) => void;
13
+ }): {
14
+ getSnapshot: (_required?: Record<string, never>, _optional?: Record<string, never>) => Readonly<UsersState>;
15
+ subscribe: ({ listener }: {
16
+ listener: () => void;
17
+ }, _optional?: Record<string, never>) => () => void;
18
+ allowed: ({ principalId, action }: {
19
+ principalId: string;
20
+ action: UserAction;
21
+ }, _optional?: Record<string, never>) => boolean;
22
+ load: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<void>;
23
+ loadCaller: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<void>;
24
+ createUser: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
25
+ toggleExpanded: ({ principalId }: {
26
+ principalId: string;
27
+ }, _optional?: Record<string, never>) => void;
28
+ savePanel: ({ principalId, kind }: {
29
+ principalId: string;
30
+ kind: "email" | "role" | "policy";
31
+ }, _optional?: Record<string, never>) => Promise<boolean>;
32
+ enableUser: ({ principalId }: {
33
+ principalId: string;
34
+ }, _optional?: Record<string, never>) => Promise<boolean>;
35
+ requestDestructive: ({ principalId, kind }: {
36
+ principalId: string;
37
+ kind: "disable" | "delete" | "reset";
38
+ }, _optional?: Record<string, never>) => void;
39
+ confirmDestructive: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
40
+ cancelDestructive(_required: Record<string, never>, _optional?: Record<string, never>): void;
41
+ setFormOpen({ open }: {
42
+ open: boolean;
43
+ }, _optional?: Record<string, never>): void;
44
+ setDraft({ patch }: {
45
+ patch: Partial<UsersDraft>;
46
+ }, _optional?: Record<string, never>): void;
47
+ togglePasswordVisible({ field }: {
48
+ field: "new" | "confirm";
49
+ }, _optional?: Record<string, never>): void;
50
+ dispose(_required?: Record<string, never>, _optional?: Record<string, never>): void;
51
+ };
52
+ export type UsersController = ReturnType<typeof createUsersController>;
53
+ //# sourceMappingURL=users.controller.d.ts.map