@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,217 @@
1
+ import { RolesApiError } from '../ports.js';
2
+ import { BUILTIN_PERMISSIONS } from '../../core/builtin-permissions.js';
3
+ import { canManageRoles } from '../rules.js';
4
+ /** Isolated fake of the server wire contract, including immutable and issuer guards.
5
+ * It is not a production authorizer or a persistent identity repository. */
6
+ export function createMemoryRolesApi({ workspaceId }, options = {}) {
7
+ const grants = Object.freeze([...(options.permissions ?? [])]), catalog = new Set(options.catalog ?? BUILTIN_PERMISSIONS.map(p => p.id));
8
+ const roles = new Map(), policies = new Map(), permissions = new Map();
9
+ for (const r of options.roles ?? [])
10
+ if (r.workspaceId === workspaceId)
11
+ roles.set(r.id, { ...r });
12
+ for (const p of options.policies ?? [])
13
+ if (p.workspaceId === workspaceId)
14
+ policies.set(p.id, { ...p });
15
+ for (const p of options.policyPermissions ?? [])
16
+ if (p.workspaceId === workspaceId)
17
+ permissions.set(p.id, { ...p });
18
+ let sequence = 0;
19
+ const id = () => {
20
+ let next;
21
+ do {
22
+ next = `memory-${++sequence}`;
23
+ } while (roles.has(next) || policies.has(next) || permissions.has(next));
24
+ return next;
25
+ };
26
+ const fail = (code, message) => { throw new RolesApiError({ code, message }); };
27
+ const check = (o) => { o.signal?.throwIfAborted(); };
28
+ const write = (o) => {
29
+ check(o);
30
+ if (!canManageRoles({ permissions: grants }))
31
+ fail('FORBIDDEN', 'Missing role.manage.');
32
+ };
33
+ const name = (v) => {
34
+ const n = v.trim().normalize('NFC');
35
+ if (!n)
36
+ fail('VALIDATION_ERROR', 'Name is required.');
37
+ return n;
38
+ };
39
+ const unique = (map, n, except) => {
40
+ if ([...map.values()].some(r => r.id !== except && r.name.toLowerCase() === n.toLowerCase()))
41
+ fail('RESOURCE_CONFLICT', 'Name already exists.');
42
+ };
43
+ const getRole = (roleId) => roles.get(roleId) ?? fail('RESOURCE_NOT_FOUND', 'Role not found.');
44
+ const getPolicy = (policyId) => policies.get(policyId) ?? fail('RESOURCE_NOT_FOUND', 'Policy not found.');
45
+ const editableRole = (roleId) => {
46
+ const r = getRole(roleId);
47
+ if (r.isBuiltin)
48
+ fail('VALIDATION_ERROR', 'Built-in role is immutable.');
49
+ return r;
50
+ };
51
+ const editablePolicy = (policyId) => {
52
+ const p = getPolicy(policyId);
53
+ if (p.isBuiltin || p.isFrozen)
54
+ fail('VALIDATION_ERROR', 'Built-in or frozen policy is immutable.');
55
+ return p;
56
+ };
57
+ return {
58
+ async listRoles(_r, o = {}) { check(o); return { roles: [...roles.values()].map(r => ({ ...r })) }; },
59
+ async listPolicies(_r, o = {}) { check(o); return { policies: [...policies.values()].map(p => ({ ...p })) }; },
60
+ async createRole(r, o = {}) { write(o); const n = name(r.name); unique(roles, n); const role = { id: id(), workspaceId, name: n, isBuiltin: false }; roles.set(role.id, role); return { role: { ...role } }; },
61
+ async updateRole(r, o = {}) { write(o); const old = editableRole(r.roleId), n = name(r.name); unique(roles, n, old.id); const role = { ...old, name: n }; roles.set(role.id, role); return { role: { ...role } }; },
62
+ async deleteRole(r, o = {}) {
63
+ write(o);
64
+ editableRole(r.roleId);
65
+ if (options.referencedRoleIds?.includes(r.roleId))
66
+ fail('RESOURCE_CONFLICT', 'Role is still assigned.');
67
+ roles.delete(r.roleId);
68
+ },
69
+ async createPolicy(r, o = {}) { write(o); const n = name(r.name); unique(policies, n); const policy = { id: id(), workspaceId, name: n, isBuiltin: false, isFrozen: false, ...(o.description !== undefined ? { description: o.description } : {}) }; policies.set(policy.id, policy); return { policy: { ...policy } }; },
70
+ async updatePolicy(r, o = {}) {
71
+ write(o);
72
+ const old = editablePolicy(r.policyId);
73
+ if (o.name === undefined && o.description === undefined)
74
+ fail('VALIDATION_ERROR', 'At least one of name or description is required.');
75
+ const n = o.name === undefined ? old.name : name(o.name);
76
+ unique(policies, n, old.id);
77
+ const policy = { ...old, name: n, ...(o.description !== undefined ? { description: o.description } : {}) };
78
+ policies.set(policy.id, policy);
79
+ return { policy: { ...policy } };
80
+ },
81
+ async deletePolicy(r, o = {}) {
82
+ write(o);
83
+ editablePolicy(r.policyId);
84
+ if (options.referencedPolicyIds?.includes(r.policyId))
85
+ fail('RESOURCE_CONFLICT', 'Policy is still attached.');
86
+ policies.delete(r.policyId);
87
+ for (const p of permissions.values())
88
+ if (p.policyId === r.policyId)
89
+ permissions.delete(p.id);
90
+ },
91
+ async listPolicyPermissions(r, o = {}) { check(o); getPolicy(r.policyId); return { policyPermissions: [...permissions.values()].filter(p => p.policyId === r.policyId).map(p => ({ ...p })) }; },
92
+ async writePolicyPermission(r, o = {}) {
93
+ write(o);
94
+ editablePolicy(r.policyId);
95
+ if (!catalog.has(r.permission))
96
+ fail('PERMISSION_UNKNOWN', 'Unrecognized permission.');
97
+ if (!grants.includes('*') && !grants.includes(r.permission))
98
+ fail('GRANT_EXCEEDS_ISSUER', 'Grant exceeds issuer.');
99
+ const resourceType = o.resourceType || null;
100
+ const policyPermission = { id: id(), workspaceId, policyId: r.policyId, permission: r.permission, resourceType, constraintJson: null };
101
+ permissions.set(policyPermission.id, policyPermission);
102
+ return { policyPermission: { ...policyPermission } };
103
+ },
104
+ async removePolicyPermission(r, o = {}) {
105
+ write(o);
106
+ editablePolicy(r.policyId);
107
+ const p = permissions.get(r.policyPermissionId);
108
+ if (!p || p.policyId !== r.policyId)
109
+ fail('RESOURCE_NOT_FOUND', 'Permission not found on this policy.');
110
+ permissions.delete(r.policyPermissionId);
111
+ },
112
+ };
113
+ }
114
+ import { PeopleApiError } from '../ports.js';
115
+ import { hasAdminGrant, userActionAllowed } from '../people.rules.js';
116
+ /** Complete fake ownership data. Production hosts must resolve effective unconstrained wildcard
117
+ * grants, never role display names. Unknown ids deliberately fail closed. */
118
+ export function createMemoryUsersSafety({ principalIds, ownerPrincipalIds, seededOwnerPrincipalId }, _optional = {}) {
119
+ const known = new Set(principalIds), owners = new Set(ownerPrincipalIds);
120
+ return { seededOwnerPrincipalId, ownerStatus: r => known.has(r.principalId) ? owners.has(r.principalId) ? 'owner' : 'operator' : 'unknown' };
121
+ }
122
+ /** Fake wire backend only: no persistent password storage, real sessions, audit or Trash service. */
123
+ export function createMemoryUsersApi({ workspaceId }, options = {}) {
124
+ const grants = Object.freeze([...(options.permissions ?? [])]);
125
+ const users = new Map((options.users ?? []).filter(u => u.workspaceId === workspaceId).map(u => [u.principalId, clone(u)]));
126
+ const roles = (options.roles ?? []).filter(r => r.workspaceId === workspaceId).map(r => ({ ...r }));
127
+ const policies = (options.policies ?? []).filter(p => p.workspaceId === workspaceId).map(p => ({ ...p }));
128
+ const owners = new Set(options.ownerPrincipalIds ?? []), trash = new Set();
129
+ const caller = options.caller ? structuredClone(options.caller) : { user: { id: 'memory-caller', username: 'caller' }, canManageUserTrash: false };
130
+ const safety = { seededOwnerPrincipalId: options.seededOwnerPrincipalId ?? null, ownerStatus: r => users.has(r.principalId) ? owners.has(r.principalId) ? 'owner' : 'operator' : 'unknown' };
131
+ let sequence = 0;
132
+ function clone(u) { return { ...u, roleIds: [...u.roleIds], policyIds: [...u.policyIds] }; }
133
+ const fail = (code, message) => { throw new PeopleApiError({ code, message }); };
134
+ const check = (o) => o.signal?.throwIfAborted();
135
+ const get = (id) => users.get(id) ?? fail('RESOURCE_NOT_FOUND', 'User not found.');
136
+ function write(id, action, o) {
137
+ check(o);
138
+ const user = get(id);
139
+ if (trash.has(id))
140
+ fail('USER_IN_TRASH', 'This user is in the Trash; restore them first.');
141
+ if (action === 'delete' && id === caller.user.id)
142
+ fail('SELF_DELETE', 'You cannot delete your own account.');
143
+ if (!userActionAllowed({ user, action, permissions: grants, caller, safety }))
144
+ fail('FORBIDDEN', 'Mutation denied.');
145
+ return user;
146
+ }
147
+ const password = (value) => { if (!value || [...value].length > 512)
148
+ fail('VALIDATION_ERROR', 'Password must be present and no more than 512 characters.'); };
149
+ function clamp(contents) { if (grants.includes('*'))
150
+ return; if (!contents || contents.some(p => !grants.includes(p)))
151
+ fail('GRANT_EXCEEDS_ISSUER', 'Grant exceeds issuer.'); }
152
+ return {
153
+ async listUsers(_r, o = {}) { check(o); return { users: [...users.values()].filter(u => !trash.has(u.principalId)).map(clone) }; },
154
+ async listRoles(_r, o = {}) { check(o); return { roles: roles.map(r => ({ ...r })) }; },
155
+ async listPolicies(_r, o = {}) { check(o); return { policies: policies.map(p => ({ ...p })) }; },
156
+ async me(_r, o = {}) { check(o); return structuredClone(caller); },
157
+ async createUser(r, o = {}) {
158
+ check(o);
159
+ if (!hasAdminGrant({ permissions: grants, permission: 'user.manage' }))
160
+ fail('FORBIDDEN', 'Missing user.manage.');
161
+ const username = r.username.trim().normalize('NFC').toLowerCase();
162
+ if (!username)
163
+ fail('VALIDATION_ERROR', 'Username is required.');
164
+ password(r.password);
165
+ const existing = [...users.values()].find(u => u.username.normalize('NFC').toLowerCase() === username);
166
+ if (existing)
167
+ fail(trash.has(existing.principalId) ? 'USERNAME_IN_TRASH' : 'RESOURCE_CONFLICT', 'Username already exists.');
168
+ let principalId;
169
+ do {
170
+ principalId = `memory-user-${++sequence}`;
171
+ } while (users.has(principalId));
172
+ const user = { principalId, workspaceId, username, status: 'active', createdAt: new Date(0).toISOString(), roleIds: [], policyIds: [], ...(o.email !== undefined ? { email: o.email } : {}) };
173
+ users.set(principalId, user);
174
+ return { user: clone(user) };
175
+ },
176
+ async updateUser(r, o = {}) { const u = write(r.principalId, 'email', o); const user = { ...u, ...(o.email !== undefined ? { email: o.email } : {}) }; users.set(u.principalId, user); return { user: clone(user) }; },
177
+ async disableUser(r, o = {}) {
178
+ const u = write(r.principalId, 'disable', o);
179
+ if (owners.has(u.principalId) && [...users.values()].filter(v => v.status === 'active' && owners.has(v.principalId)).length <= 1)
180
+ fail('OWNER_REQUIRED', 'The workspace must keep at least one active owner.');
181
+ const user = { ...u, status: 'disabled' };
182
+ users.set(u.principalId, user);
183
+ return { user: clone(user) };
184
+ },
185
+ async enableUser(r, o = {}) { const u = write(r.principalId, 'enable', o); const user = { ...u, status: 'active' }; users.set(u.principalId, user); return { user: clone(user) }; },
186
+ async resetUserPassword(r, o = {}) { write(r.principalId, 'reset', o); password(r.password); options.onPasswordReset?.(r); },
187
+ async assignRole(r, o = {}) { const u = write(r.principalId, 'grants', o); if (!roles.some(v => v.id === r.roleId))
188
+ fail('RESOURCE_NOT_FOUND', 'Role not found.'); clamp(options.rolePermissions?.[r.roleId]); users.set(u.principalId, { ...u, roleIds: [...new Set([...u.roleIds, r.roleId])] }); return { assignment: { principalId: u.principalId, roleId: r.roleId } }; },
189
+ async attachPolicy(r, o = {}) { const u = write(r.principalId, 'grants', o); if (!policies.some(v => v.id === r.policyId))
190
+ fail('RESOURCE_NOT_FOUND', 'Policy not found.'); clamp(options.policyPermissions?.[r.policyId]); users.set(u.principalId, { ...u, policyIds: [...new Set([...u.policyIds, r.policyId])] }); return { attachment: { principalId: u.principalId, policyId: r.policyId } }; },
191
+ async deleteUser(r, o = {}) { const u = write(r.principalId, 'delete', o); if (owners.has(u.principalId) && [...users.values()].filter(v => v.status === 'active' && owners.has(v.principalId)).length <= 1)
192
+ fail('OWNER_REQUIRED', 'The workspace must keep at least one active owner.'); users.set(u.principalId, { ...u, status: 'disabled' }); trash.add(u.principalId); },
193
+ };
194
+ }
195
+ export function createMemoryMembersApi({ workspaceId }, { members = [], permissions = [] } = {}) {
196
+ const grants = Object.freeze([...permissions]);
197
+ const rows = new Map(members.filter(m => m.workspaceId === workspaceId).map(m => [m.id, { ...m }]));
198
+ const check = (o) => o.signal?.throwIfAborted();
199
+ const fail = (code, message) => { throw new PeopleApiError({ code, message }); };
200
+ const gate = (o) => { check(o); if (!hasAdminGrant({ permissions: grants, permission: 'member.manage' }))
201
+ fail('FORBIDDEN', 'Missing member.manage.'); };
202
+ const get = (id) => rows.get(id) ?? fail('RESOURCE_NOT_FOUND', 'Member not found.');
203
+ return {
204
+ async listMembers(_r, o = {}) { check(o); const limit = o.limit !== undefined && Number.isFinite(o.limit) && o.limit > 0 ? Math.floor(o.limit) : 100; return { members: [...rows.values()].sort((a, b) => a.id.localeCompare(b.id)).filter(m => o.afterId === undefined || m.id > o.afterId).slice(0, limit).map(m => ({ ...m })) }; },
205
+ async getMember(r, o = {}) { check(o); return { member: { ...get(r.id) } }; },
206
+ async disableMember(r, o = {}) { gate(o); const old = get(r.id); const member = old.status === 'disabled' ? old : { ...old, status: 'disabled', version: old.version + 1, updatedAt: new Date(0).toISOString() }; rows.set(member.id, member); return { member: { ...member } }; },
207
+ async requestMemberMagicLink(r, o = {}) { gate(o); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(r.email.trim()))
208
+ fail('VALIDATION_ERROR', 'Invalid email.'); return { delivered: true }; },
209
+ };
210
+ }
211
+ /** Deterministic credential fake; production hashing/session policy stays in the server entry. */
212
+ export function createMemoryAuthApi({ accounts }, _optional = {}) {
213
+ const rows = accounts.map(a => ({ ...a, user: { ...a.user } }));
214
+ return { async login(r, o = {}) { o.signal?.throwIfAborted(); const found = rows.find(a => a.user.username.normalize('NFC').toLowerCase() === r.username.trim().normalize('NFC').toLowerCase()); if (!r.password || !found || found.disabled || found.password !== r.password)
215
+ throw new PeopleApiError({ code: 'UNAUTHENTICATED', message: 'invalid username or password' }); return { user: { ...found.user } }; } };
216
+ }
217
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,30 @@
1
+ import type { AuthApiPort } from './ports.js';
2
+ export declare const authApiToken: import("@jini-ai/admin/core/module").AdminPortToken<AuthApiPort, "jini.identity.authApi">;
3
+ /** Reads remain server-authorized; grants gate mutation affordances, denied by default.
4
+ * Login is mounted by the signed-out host, before the authenticated shell. */
5
+ export declare const authModule: {
6
+ readonly id: "auth";
7
+ readonly requires: {
8
+ readonly authApi: import("@jini-ai/admin/core/module").AdminPortToken<AuthApiPort, "jini.identity.authApi">;
9
+ };
10
+ readonly provides: {};
11
+ readonly messages: {
12
+ readonly title: "Sign in to your workspace";
13
+ readonly submit: "Sign in";
14
+ readonly busy: "Signing in…";
15
+ readonly failed: "login failed";
16
+ };
17
+ readonly pages: {
18
+ readonly auth: {
19
+ readonly path: "/login";
20
+ readonly label: "Login";
21
+ readonly permissions: readonly [];
22
+ readonly tabs: {
23
+ readonly auth: {
24
+ readonly label: "Login";
25
+ };
26
+ };
27
+ };
28
+ };
29
+ };
30
+ //# sourceMappingURL=auth.module.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { adminPort, defineAdminModule } from '@jini-ai/admin/core/module';
2
+ import { authMessagesEn } from './messages.en.js';
3
+ export const authApiToken = adminPort({ id: 'jini.identity.authApi' });
4
+ /** Reads remain server-authorized; grants gate mutation affordances, denied by default.
5
+ * Login is mounted by the signed-out host, before the authenticated shell. */
6
+ export const authModule = defineAdminModule({
7
+ id: 'auth', requires: { authApi: authApiToken }, provides: {}, messages: authMessagesEn,
8
+ pages: { auth: { path: '/login', label: 'Login', permissions: [],
9
+ tabs: { auth: { label: 'Login' } },
10
+ } },
11
+ });
12
+ //# sourceMappingURL=auth.module.js.map
@@ -0,0 +1,56 @@
1
+ import type { RolesApiPort } from '../ports.js';
2
+ /** Mutates a disposable, authenticated backend. Caller must hold role.manage and content.read.
3
+ * Permission vocabulary can be supplied for another server's catalog. Cleanup failures are failures. */
4
+ export declare function runRolesApiConformance({ api }: {
5
+ api: RolesApiPort;
6
+ }, { permission, prefix }?: {
7
+ permission?: string;
8
+ prefix?: string;
9
+ }): Promise<{
10
+ passed: string[];
11
+ failed: {
12
+ name: string;
13
+ error: unknown;
14
+ }[];
15
+ }>;
16
+ import type { UsersApiPort, MembersApiPort, AuthApiPort } from '../ports.js';
17
+ /** Isolated disposable host required. User deletion moves to Trash; its retention cleanup belongs
18
+ * to the host. Members are disable-only, so conformance takes a dedicated fixture id. */
19
+ export declare function runUsersApiConformance({ api }: {
20
+ api: UsersApiPort;
21
+ }, { prefix, roleId, policyId }?: {
22
+ prefix?: string;
23
+ roleId?: string;
24
+ policyId?: string;
25
+ }): Promise<{
26
+ passed: string[];
27
+ failed: {
28
+ name: string;
29
+ error: unknown;
30
+ }[];
31
+ }>;
32
+ export declare function runMembersApiConformance({ api, memberId }: {
33
+ api: MembersApiPort;
34
+ memberId: string;
35
+ }, _optional?: Record<string, never>): Promise<{
36
+ passed: string[];
37
+ failed: {
38
+ name: string;
39
+ error: unknown;
40
+ }[];
41
+ }>;
42
+ export declare function runAuthApiConformance({ api, credentials, expectedUserId }: {
43
+ api: AuthApiPort;
44
+ credentials: {
45
+ username: string;
46
+ password: string;
47
+ };
48
+ expectedUserId: string;
49
+ }, _optional?: Record<string, never>): Promise<{
50
+ passed: string[];
51
+ failed: {
52
+ name: string;
53
+ error: unknown;
54
+ }[];
55
+ }>;
56
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,156 @@
1
+ /** Mutates a disposable, authenticated backend. Caller must hold role.manage and content.read.
2
+ * Permission vocabulary can be supplied for another server's catalog. Cleanup failures are failures. */
3
+ export async function runRolesApiConformance({ api }, { permission = 'content.read', prefix = `conformance-${Date.now()}` } = {}) {
4
+ const passed = [], failed = [];
5
+ const assert = (value, message) => {
6
+ if (!value)
7
+ throw new Error(message);
8
+ };
9
+ async function check(name, run) {
10
+ try {
11
+ await run();
12
+ passed.push(name);
13
+ }
14
+ catch (error) {
15
+ failed.push({ name, error });
16
+ }
17
+ }
18
+ let roleId, policyId, otherPolicyId, rowId;
19
+ try {
20
+ await check('create custom role', async () => { const { role } = await api.createRole({ name: `${prefix}-role` }); roleId = role.id; assert(!role.isBuiltin, 'created role must be custom'); });
21
+ await check('list roles', async () => assert((await api.listRoles({})).roles.some(r => r.id === roleId), 'created role absent'));
22
+ await check('update role', async () => { assert(roleId, 'create role failed'); assert((await api.updateRole({ roleId: roleId, name: `${prefix}-renamed` })).role.name === `${prefix}-renamed`, 'rename missing'); });
23
+ await check('snapshot isolation', async () => { const first = await api.listRoles({}); const r = first.roles.find(r => r.id === roleId); assert(r, 'role missing'); r.name = 'mutated'; assert((await api.listRoles({})).roles.find(r => r.id === roleId)?.name === `${prefix}-renamed`, 'mutable adapter storage exposed'); });
24
+ await check('create custom policy', async () => { const { policy } = await api.createPolicy({ name: `${prefix}-policy` }, { description: 'before' }); policyId = policy.id; assert(!policy.isBuiltin && !policy.isFrozen, 'created policy must be mutable'); });
25
+ await check('list policies', async () => assert((await api.listPolicies({})).policies.some(p => p.id === policyId), 'created policy absent'));
26
+ await check('partial update preserves name and clears description', async () => { assert(policyId, 'policy missing'); const { policy } = await api.updatePolicy({ policyId: policyId }, { description: '' }); assert(policy.name === `${prefix}-policy` && policy.description === '', 'partial update lost fields'); });
27
+ await check('write scoped permission', async () => { assert(policyId, 'policy missing'); const { policyPermission: p } = await api.writePolicyPermission({ policyId: policyId, permission }, { resourceType: 'article' }); rowId = p.id; assert(p.policyId === policyId && p.permission === permission && p.resourceType === 'article', 'wrong permission record'); });
28
+ await check('list permission ids', async () => assert((await api.listPolicyPermissions({ policyId: policyId })).policyPermissions.some(p => p.id === rowId), 'permission absent'));
29
+ await check('cross-policy removal fails closed', async () => {
30
+ const { policy } = await api.createPolicy({ name: `${prefix}-other` });
31
+ otherPolicyId = policy.id;
32
+ let rejected = false;
33
+ try {
34
+ await api.removePolicyPermission({ policyId: policy.id, policyPermissionId: rowId });
35
+ }
36
+ catch {
37
+ rejected = true;
38
+ }
39
+ assert(rejected, 'removed foreign policy permission');
40
+ assert((await api.listPolicyPermissions({ policyId: policyId })).policyPermissions.some(p => p.id === rowId), 'foreign row lost');
41
+ });
42
+ await check('aborted writes do not persist', async () => {
43
+ const abort = new AbortController();
44
+ abort.abort();
45
+ let rejected = false;
46
+ try {
47
+ await api.createRole({ name: `${prefix}-aborted` }, { signal: abort.signal });
48
+ }
49
+ catch {
50
+ rejected = true;
51
+ }
52
+ assert(rejected, 'abort ignored');
53
+ assert(!(await api.listRoles({})).roles.some(r => r.name === `${prefix}-aborted`), 'aborted write persisted');
54
+ });
55
+ await check('remove permission', async () => { await api.removePolicyPermission({ policyId: policyId, policyPermissionId: rowId }); assert((await api.listPolicyPermissions({ policyId: policyId })).policyPermissions.length === 0, 'removal absent'); });
56
+ await check('policy deletion cascades own permissions', async () => { await api.writePolicyPermission({ policyId: policyId, permission }); await api.deletePolicy({ policyId: policyId }); assert(!(await api.listPolicies({})).policies.some(p => p.id === policyId), 'policy deletion absent'); policyId = undefined; });
57
+ }
58
+ finally {
59
+ if (roleId)
60
+ await check('cleanup role', async () => { await api.deleteRole({ roleId: roleId }); assert(!(await api.listRoles({})).roles.some(r => r.id === roleId), 'role deletion absent'); });
61
+ if (policyId)
62
+ await check('cleanup policy', async () => { await api.deletePolicy({ policyId: policyId }); });
63
+ if (otherPolicyId)
64
+ await check('cleanup other policy', async () => { await api.deletePolicy({ policyId: otherPolicyId }); });
65
+ }
66
+ return { passed, failed };
67
+ }
68
+ /** Isolated disposable host required. User deletion moves to Trash; its retention cleanup belongs
69
+ * to the host. Members are disable-only, so conformance takes a dedicated fixture id. */
70
+ export async function runUsersApiConformance({ api }, { prefix = `users-${Date.now()}`, roleId, policyId } = {}) {
71
+ const passed = [], failed = [];
72
+ const assert = (value, message) => { if (!value)
73
+ throw new Error(message); };
74
+ const check = async (name, run) => { try {
75
+ await run();
76
+ passed.push(name);
77
+ }
78
+ catch (error) {
79
+ failed.push({ name, error });
80
+ } };
81
+ let principalId;
82
+ try {
83
+ await check('create operator', async () => { const { user } = await api.createUser({ username: prefix, password: 'test-secret' }, { email: 'before@example.test' }); principalId = user.principalId; assert(user.username === prefix.toLowerCase() && user.status === 'active', 'wrong created user'); });
84
+ await check('list users roles policies and caller', async () => { const [u, r, p, me] = await Promise.all([api.listUsers({}), api.listRoles({}), api.listPolicies({}), api.me({})]); assert(u.users.some(u => u.principalId === principalId) && Array.isArray(r.roles) && Array.isArray(p.policies) && me.user.id, 'missing roster/caller'); });
85
+ await check('snapshot isolation', async () => { const u = (await api.listUsers({})).users.find(u => u.principalId === principalId); u.roleIds.push('foreign'); assert(!(await api.listUsers({})).users.find(u => u.principalId === principalId).roleIds.includes('foreign'), 'mutable role storage exposed'); });
86
+ await check('email patch preserves omitted email', async () => { await api.updateUser({ principalId: principalId }, { email: 'after@example.test' }); assert((await api.updateUser({ principalId: principalId })).user.email === 'after@example.test', 'omitted email cleared'); });
87
+ await check('disable and enable operator', async () => { assert((await api.disableUser({ principalId: principalId })).user.status === 'disabled', 'disable missing'); assert((await api.enableUser({ principalId: principalId })).user.status === 'active', 'enable missing'); });
88
+ await check('reset password', async () => { await api.resetUserPassword({ principalId: principalId, password: 'replacement' }); });
89
+ if (roleId)
90
+ await check('assign role', async () => { await api.assignRole({ principalId: principalId, roleId }); assert((await api.listUsers({})).users.find(u => u.principalId === principalId)?.roleIds.includes(roleId), 'role missing'); });
91
+ if (policyId)
92
+ await check('attach policy', async () => { await api.attachPolicy({ principalId: principalId, policyId }); assert((await api.listUsers({})).users.find(u => u.principalId === principalId)?.policyIds.includes(policyId), 'policy missing'); });
93
+ await check('aborted write does not persist', async () => { const abort = new AbortController(); abort.abort(); let rejected = false; try {
94
+ await api.updateUser({ principalId: principalId }, { email: 'bad@example.test', signal: abort.signal });
95
+ }
96
+ catch {
97
+ rejected = true;
98
+ } assert(rejected && (await api.listUsers({})).users.find(u => u.principalId === principalId)?.email === 'after@example.test', 'abort ignored'); });
99
+ }
100
+ finally {
101
+ if (principalId)
102
+ await check('delete moves out of roster', async () => { await api.deleteUser({ principalId: principalId }); assert(!(await api.listUsers({})).users.some(u => u.principalId === principalId), 'trashed user still listed'); });
103
+ }
104
+ return { passed, failed };
105
+ }
106
+ export async function runMembersApiConformance({ api, memberId }, _optional = {}) {
107
+ const passed = [], failed = [];
108
+ const assert = (value, message) => { if (!value)
109
+ throw new Error(message); };
110
+ const check = async (name, run) => { try {
111
+ await run();
112
+ passed.push(name);
113
+ }
114
+ catch (error) {
115
+ failed.push({ name, error });
116
+ } };
117
+ await check('list and read member', async () => { assert((await api.listMembers({})).members.some(m => m.id === memberId), 'member absent'); assert((await api.getMember({ id: memberId })).member.id === memberId, 'wrong member'); });
118
+ await check('snapshot isolation', async () => { const { member } = await api.getMember({ id: memberId }); member.email = 'mutated@example.test'; assert((await api.getMember({ id: memberId })).member.email !== member.email, 'mutable storage exposed'); });
119
+ await check('constant magic-link response', async () => { assert((await api.requestMemberMagicLink({ email: 'unregistered@example.test' })).delivered === true, 'enumeration response'); });
120
+ await check('aborted disable does not persist', async () => { const before = (await api.getMember({ id: memberId })).member; const abort = new AbortController(); abort.abort(); let rejected = false; try {
121
+ await api.disableMember({ id: memberId }, { signal: abort.signal });
122
+ }
123
+ catch {
124
+ rejected = true;
125
+ } assert(rejected && (await api.getMember({ id: memberId })).member.status === before.status, 'abort ignored'); });
126
+ await check('disable is idempotent', async () => { const first = (await api.disableMember({ id: memberId })).member, second = (await api.disableMember({ id: memberId })).member; assert(first.status === 'disabled' && second.status === 'disabled' && first.version === second.version, 'non-idempotent disable'); });
127
+ return { passed, failed };
128
+ }
129
+ export async function runAuthApiConformance({ api, credentials, expectedUserId }, _optional = {}) {
130
+ const passed = [], failed = [];
131
+ const check = async (name, run) => { try {
132
+ await run();
133
+ passed.push(name);
134
+ }
135
+ catch (error) {
136
+ failed.push({ name, error });
137
+ } };
138
+ await check('login returns expected account', async () => { if ((await api.login(credentials)).user.id !== expectedUserId)
139
+ throw new Error('wrong account'); });
140
+ await check('empty password is rejected', async () => { let rejected = false; try {
141
+ await api.login({ ...credentials, password: '' });
142
+ }
143
+ catch {
144
+ rejected = true;
145
+ } if (!rejected)
146
+ throw new Error('empty password authenticated'); });
147
+ await check('aborted login is rejected', async () => { const abort = new AbortController(); abort.abort(); let rejected = false; try {
148
+ await api.login(credentials, { signal: abort.signal });
149
+ }
150
+ catch {
151
+ rejected = true;
152
+ } if (!rejected)
153
+ throw new Error('abort ignored'); });
154
+ return { passed, failed };
155
+ }
156
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,23 @@
1
+ import type { AuthApiPort } from '../ports.js';
2
+ import type { AdminAccount, AuthState } from '../models.js';
3
+ /** The host owns session cookies and shell switching; the form owns only credentials and errors. */
4
+ export declare function createAuthController({ api, onLogin }: {
5
+ api: AuthApiPort;
6
+ onLogin: (required: {
7
+ user: AdminAccount;
8
+ }, optional?: Record<string, never>) => void;
9
+ }, { initialUsername }?: {
10
+ initialUsername?: string;
11
+ }): {
12
+ getSnapshot: (_required?: Record<string, never>, _optional?: Record<string, never>) => Readonly<AuthState>;
13
+ subscribe: ({ listener }: {
14
+ listener: () => void;
15
+ }, _optional?: Record<string, never>) => () => void;
16
+ dispose(_required?: Record<string, never>, _optional?: Record<string, never>): void;
17
+ setDraft({ patch }: {
18
+ patch: Pick<Partial<AuthState>, "username" | "password">;
19
+ }, _optional?: Record<string, never>): void;
20
+ submit(_required: Record<string, never>, _optional?: Record<string, never>): Promise<boolean>;
21
+ };
22
+ export type AuthController = ReturnType<typeof createAuthController>;
23
+ //# sourceMappingURL=auth.controller.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { createControllerStore } from './controller-store.js';
2
+ /** The host owns session cookies and shell switching; the form owns only credentials and errors. */
3
+ export function createAuthController({ api, onLogin }, { initialUsername = 'admin' } = {}) {
4
+ const store = createControllerStore({ initial: { username: initialUsername, password: '', error: null, busy: false } });
5
+ const set = (patch) => store.set({ patch });
6
+ return {
7
+ getSnapshot: store.getSnapshot, subscribe: store.subscribe,
8
+ dispose(_required = {}, _optional = {}) { set({ password: '' }); store.dispose({}); },
9
+ setDraft({ patch }, _optional = {}) { set(patch); },
10
+ async submit(_required, _optional = {}) {
11
+ const s = store.getSnapshot({});
12
+ if (!store.active({}) || s.busy)
13
+ return false;
14
+ set({ busy: true, error: null });
15
+ try {
16
+ const { user } = await api.login({ username: s.username, password: s.password }, store.call);
17
+ if (!store.active({}))
18
+ return false;
19
+ set({ password: '' });
20
+ onLogin({ user });
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ set({ error: error instanceof Error ? error.message : 'login failed' });
25
+ return false;
26
+ }
27
+ finally {
28
+ set({ busy: false });
29
+ }
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=auth.controller.js.map
@@ -0,0 +1,18 @@
1
+ /** Effect owners dispose the controller. Settlements never update a retired scope. */
2
+ export declare function createControllerStore<S extends object>({ initial }: {
3
+ initial: S;
4
+ }, _optional?: Record<string, never>): {
5
+ getSnapshot(_required?: Record<string, never>, _optional?: Record<string, never>): Readonly<S>;
6
+ active(_required?: Record<string, never>, _optional?: Record<string, never>): boolean;
7
+ set({ patch }: {
8
+ patch: Partial<S>;
9
+ }, _optional?: Record<string, never>): void;
10
+ subscribe({ listener }: {
11
+ listener: () => void;
12
+ }, _optional?: Record<string, never>): () => void;
13
+ dispose(_required?: Record<string, never>, _optional?: Record<string, never>): void;
14
+ call: {
15
+ signal: AbortSignal;
16
+ };
17
+ };
18
+ //# sourceMappingURL=controller-store.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** Effect owners dispose the controller. Settlements never update a retired scope. */
2
+ export function createControllerStore({ initial }, _optional = {}) {
3
+ let state = Object.freeze({ ...initial }), disposed = false;
4
+ const listeners = new Set();
5
+ const abort = new AbortController();
6
+ return {
7
+ getSnapshot(_required = {}, _optional = {}) { return state; },
8
+ active(_required = {}, _optional = {}) { return !disposed; },
9
+ set({ patch }, _optional = {}) {
10
+ if (disposed)
11
+ return;
12
+ state = Object.freeze({ ...state, ...patch });
13
+ for (const listener of listeners)
14
+ listener();
15
+ },
16
+ subscribe({ listener }, _optional = {}) { if (!disposed)
17
+ listeners.add(listener); return () => { listeners.delete(listener); }; },
18
+ dispose(_required = {}, _optional = {}) { if (disposed)
19
+ return; disposed = true; abort.abort(); listeners.clear(); },
20
+ call: { signal: abort.signal },
21
+ };
22
+ }
23
+ //# sourceMappingURL=controller-store.js.map
@@ -0,0 +1,33 @@
1
+ import type { MembersApiPort, MembersRefreshPort } from '../ports.js';
2
+ import type { MembersState, MemberRowState } from '../models.js';
3
+ /** Separate list and detail generations prevent stale errors/spinners from painting onto a
4
+ * different row, including collapse/reopen of the same row. Refresh invalidates cached detail. */
5
+ export declare function createMembersController({ api }: {
6
+ api: MembersApiPort;
7
+ }, { permissions, refresh }?: {
8
+ permissions?: readonly string[];
9
+ refresh?: MembersRefreshPort;
10
+ }): {
11
+ getSnapshot: (_required?: Record<string, never>, _optional?: Record<string, never>) => Readonly<MembersState>;
12
+ subscribe: ({ listener }: {
13
+ listener: () => void;
14
+ }, _optional?: Record<string, never>) => () => void;
15
+ load: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<void>;
16
+ stateFor: ({ id }: {
17
+ id: string;
18
+ }, _optional?: Record<string, never>) => MemberRowState;
19
+ toggleDetail: ({ id }: {
20
+ id: string;
21
+ }, _optional?: Record<string, never>) => Promise<void>;
22
+ resendSignInLink: ({ id }: {
23
+ id: string;
24
+ }, _optional?: Record<string, never>) => Promise<boolean>;
25
+ confirmDestructive: (_required: Record<string, never>, _optional?: Record<string, never>) => Promise<boolean>;
26
+ requestDestructive({ id }: {
27
+ id: string;
28
+ }, _optional?: Record<string, never>): void;
29
+ cancelDestructive(_required: Record<string, never>, _optional?: Record<string, never>): void;
30
+ dispose(_required?: Record<string, never>, _optional?: Record<string, never>): void;
31
+ };
32
+ export type MembersController = ReturnType<typeof createMembersController>;
33
+ //# sourceMappingURL=members.controller.d.ts.map