@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,88 @@
1
+ import type { AgentToolDefinition as KernelAgentToolDefinition } from "@jini-ai/core";
2
+ /**
3
+ * @file The identity (users/roles) agent-tool catalog — this domain's instance of the
4
+ * per-domain `agent-tools.ts` convention already used by `features/content-types`,
5
+ * `features/database`, and `features/recovery`.
6
+ *
7
+ * Purpose:
8
+ * A static, in-process catalog describing every agent-callable identity tool and the permission
9
+ * each one carries. `assistant/tool-registrations.ts` maps these entries onto handlers that call
10
+ * `grant-service.ts` / `admin-crud-service.ts` — the SAME functions the human admin routes under
11
+ * `server/routes/admin/users/` call. There is deliberately no path from a tool to the database
12
+ * that skips those functions, because they are where the domain's authorization gates live.
13
+ *
14
+ * WHAT THIS CATALOG DELIBERATELY OMITS, and why (identity is the escalation-dense
15
+ * surface, so the omissions are part of the design rather than a backlog):
16
+ *
17
+ * - `resetUserPassword`. The one identity transition that sets a credential on
18
+ * an account the caller does not own, and — unlike `ASSIGN_ROLE`/`ATTACH_POLICY` — it carries
19
+ * NO grant-authority clamp. A `user.manage` holder may reset the OWNER's password and
20
+ * then authenticate as owner, so it is a full account-takeover primitive rather than a
21
+ * permission grant. An assistant is reachable by prompt injection through ordinary operator
22
+ * content (a form submission, a comment) in a way a human clicking the admin button is not.
23
+ * Left human-UI-only, mirroring `features/content-types/agent-tools.ts`'s reasoning for never
24
+ * exposing the destructive-cleanup confirm step to an agent.
25
+ * - `writePolicyPermission`. This is the one policy transition still
26
+ * deliberately excluded, and on different grounds than the five wired below: unlike
27
+ * `identity_role_assign`/`identity_policy_attach`, which each confer whatever a role/policy
28
+ * ALREADY carries onto exactly one named target principal, `writePolicyPermission` MUTATES a
29
+ * shared, reusable `PolicyRecord` in place — and any non-built-in, non-frozen policy can
30
+ * already be attached to an arbitrary number of OTHER users via `identity_policy_attach` (or to
31
+ * roles via the seed-only `role_policies` table) before this call happens. The clamp
32
+ * (`assertGrantClamp`) still bounds WHAT the caller can add — only a permission it itself holds
33
+ * unconstrained — but it says nothing about WHO ends up holding it: every principal already
34
+ * attached to that policy inherits the new permission the instant it is written, invisibly to
35
+ * whoever asked for the one change. That is a materially different blast-radius shape than the
36
+ * single-target primitives this catalog does wire (see the tool descriptions below), and exactly
37
+ * the "grant that ripples to principals never named in the call" class this pass's directive
38
+ * asks to be excluded and explained rather than wired reflexively. Left human-UI-only, where
39
+ * the operator Roles screen's "Add permission" control still lives.
40
+ *
41
+ * Every OTHER policy transition (`identity_policy_list`, `identity_policy_create`,
42
+ * `identity_policy_update`, `identity_policy_delete`, `identity_policy_attach`) IS wired below,
43
+ * mirroring the risk profile this catalog already accepted for the equivalent role transitions
44
+ * (`identity_role_list`/`identity_role_create`/`identity_role_rename`/`identity_role_delete`/
45
+ * `identity_role_assign`) — see each entry's own description for why its shape matches its role
46
+ * counterpart exactly.
47
+ *
48
+ * - Role REVOCATION. Not a choice: no such operation exists anywhere in this codebase.
49
+ * `PrincipalRoleRepoPort` (`ports.ts`) declares `listByPrincipalId`, `save`, and `listByRoleId`
50
+ * — there is no `delete`, and no service function unassigns a role. A tool cannot be written
51
+ * against an operation the domain does not have, and adding a repo-level delete here to
52
+ * manufacture one would be exactly the "skip the service layer" failure this work exists to
53
+ * close. The same is true of "detach a policy from a principal" — `PrincipalPolicyRepoPort`
54
+ * has no delete either, so `identity_policy_attach` is one-directional exactly like
55
+ * `identity_role_assign`.
56
+ *
57
+ * Architectural role:
58
+ * `user-management` library declaration only. Performs no I/O and no enforcement — `authorize` and each
59
+ * service function's own caller-permission gate enforce at call time. The kernel supplies the
60
+ * structural catalog contract; this domain only requires inputSchema for every wired tool,
61
+ * so a low-level library never acquires a dependency on a host feature module.
62
+ * See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
63
+ *
64
+ * The permission the tool's underlying service function gates on.
65
+ *
66
+ * `orPermission` is present where the service accepts an OR: listing users, and updating one's
67
+ * own email (other operator targets require `user.manage`). Recording only the first would make this catalog quietly
68
+ * disagree with the code it describes; `__tests__/agent-tools.authorization.test.ts` derives its
69
+ * expected permission set from BOTH fields, so an undeclared OR fails the suite.
70
+ *
71
+ * JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
72
+ * AND interpreted as the enforcing boundary parser by `agent-tool-input.ts`.
73
+ *
74
+ * Required even though the kernel permits omission: every entry here is
75
+ * wired, so there is no unwired entry whose input shape is undesigned.
76
+ */
77
+ export type IdentityAgentToolDefinition = KernelAgentToolDefinition & {
78
+ inputSchema: Readonly<Record<string, unknown>>;
79
+ };
80
+ /**
81
+ * The identity domain's fixed agent-tool catalog.
82
+ *
83
+ * Ordered read-tools-first because that is the order a correct multi-step call chain uses: a model
84
+ * cannot assign a role without a `roleId`, and the only way to learn one for an EXISTING role
85
+ * (including the four seeded built-ins) is `identity_role_list`.
86
+ */
87
+ export declare const identityAgentToolCatalog: IdentityAgentToolDefinition[];
88
+ //# sourceMappingURL=agent-tools.d.ts.map
@@ -0,0 +1,229 @@
1
+ /** A workspace-scoped principal id, as returned by `identity_user_list` or `identity_user_create`. */
2
+ const PRINCIPAL_ID_SCHEMA = {
3
+ type: "string",
4
+ minLength: 1,
5
+ description: "The user's principalId. Get it from identity_user_list, or from identity_user_create's result.",
6
+ };
7
+ /** A workspace-scoped role id, as returned by `identity_role_list` or `identity_role_create`. */
8
+ const ROLE_ID_SCHEMA = {
9
+ type: "string",
10
+ minLength: 1,
11
+ description: "The role's id. Get it from identity_role_list, or from identity_role_create's result.",
12
+ };
13
+ /** A workspace-scoped policy id, as returned by `identity_policy_list` or `identity_policy_create`. */
14
+ const POLICY_ID_SCHEMA = {
15
+ type: "string",
16
+ minLength: 1,
17
+ description: "The policy's id. Get it from identity_policy_list, or from identity_policy_create's result.",
18
+ };
19
+ /** A required, non-blank human-authored name. `minLength` is published because the service
20
+ * functions reject a blank/whitespace-only name — leaving it unstated would show the model a
21
+ * contract wider than the one enforced. */
22
+ const REQUIRED_NAME_SCHEMA = {
23
+ type: "string",
24
+ minLength: 1,
25
+ description: "Human-readable name. Must not be blank or whitespace-only.",
26
+ };
27
+ /** The input shape of the three read tools — they take no arguments at all. */
28
+ const NO_INPUT_SCHEMA = {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ required: [],
32
+ properties: {},
33
+ };
34
+ /** `{principalId}` — shared by the two principal-status transitions, which take exactly that. */
35
+ const PRINCIPAL_ONLY_INPUT_SCHEMA = {
36
+ type: "object",
37
+ additionalProperties: false,
38
+ required: ["principalId"],
39
+ properties: { principalId: PRINCIPAL_ID_SCHEMA },
40
+ };
41
+ /**
42
+ * The identity domain's fixed agent-tool catalog.
43
+ *
44
+ * Ordered read-tools-first because that is the order a correct multi-step call chain uses: a model
45
+ * cannot assign a role without a `roleId`, and the only way to learn one for an EXISTING role
46
+ * (including the four seeded built-ins) is `identity_role_list`.
47
+ */
48
+ export const identityAgentToolCatalog = [
49
+ {
50
+ name: "identity_user_list",
51
+ description: "Lists the workspace's human operator users with their principalId, username, status, and assigned role ids. Read-only. Call this to find a user's principalId before updating, disabling, enabling, or assigning a role to them. There is no password-reset tool. The owner resets a user's password from the Users screen in the admin (Reset password).",
52
+ sideEffects: "none",
53
+ authorization: { permission: "user.manage", orPermission: "member.manage" },
54
+ inputSchema: NO_INPUT_SCHEMA,
55
+ },
56
+ {
57
+ name: "identity_role_list",
58
+ description: "Lists the workspace's roles (the four built-ins plus any custom ones) with their id, name, and isBuiltin flag. Read-only. Call this to find a roleId before assigning, renaming, or deleting a role.",
59
+ sideEffects: "none",
60
+ authorization: { permission: "role.manage" },
61
+ inputSchema: NO_INPUT_SCHEMA,
62
+ },
63
+ {
64
+ name: "identity_policy_list",
65
+ description: "Lists the workspace's policies (built-ins plus any custom ones) with their id, name, description, isBuiltin, and isFrozen flags. Read-only. Call this to find a policyId before renaming, deleting, or attaching a policy.",
66
+ sideEffects: "none",
67
+ authorization: { permission: "role.manage" },
68
+ inputSchema: NO_INPUT_SCHEMA,
69
+ },
70
+ {
71
+ name: "identity_user_create",
72
+ description: "Creates a new human operator user, minting a new principal and its credential row together. The new user starts with NO roles and therefore no permissions — grant access afterwards with identity_role_assign. Fails if the username is already taken.",
73
+ sideEffects: "mutates-durable-state",
74
+ authorization: { permission: "user.manage" },
75
+ inputSchema: {
76
+ type: "object",
77
+ additionalProperties: false,
78
+ required: ["username", "password"],
79
+ properties: {
80
+ username: {
81
+ type: "string",
82
+ minLength: 1,
83
+ description: "Login name. Stored lowercased and Unicode-NFC-normalized, and unique per workspace on that normalized form.",
84
+ },
85
+ password: {
86
+ type: "string",
87
+ minLength: 1,
88
+ description: "Initial password. Hashed with argon2id before storage; never stored or returned in plaintext.",
89
+ },
90
+ email: { type: "string", description: "Optional contact email." },
91
+ },
92
+ },
93
+ },
94
+ {
95
+ name: "identity_user_update_email",
96
+ description: "Sets or clears an existing user's email address. Requires user.manage for other operator users; member.manage permits only editing your own email. Only an owner may edit an owner account. Omitting 'email' (or passing an empty string) CLEARS the stored value rather than leaving it unchanged. Username and password are not editable through this tool. There is no password-reset tool. The owner resets a user's password from the Users screen in the admin (Reset password).",
97
+ sideEffects: "mutates-durable-state",
98
+ authorization: { permission: "user.manage", orPermission: "member.manage" },
99
+ inputSchema: {
100
+ type: "object",
101
+ additionalProperties: false,
102
+ required: ["principalId"],
103
+ properties: {
104
+ principalId: PRINCIPAL_ID_SCHEMA,
105
+ email: { type: "string", description: "The new email. Omit it or pass an empty string to clear the stored value." },
106
+ },
107
+ },
108
+ },
109
+ {
110
+ name: "identity_user_disable",
111
+ description: "Disables a user, revoking their access. Principals are never hard-deleted because the audit trail references them, so this is how a user is removed. Refuses to disable the seeded owner, and refuses any disable that would leave the workspace with no active owner. Already-disabled is a no-op.",
112
+ sideEffects: "mutates-durable-state",
113
+ authorization: { permission: "user.manage" },
114
+ inputSchema: PRINCIPAL_ONLY_INPUT_SCHEMA,
115
+ },
116
+ {
117
+ name: "identity_user_enable",
118
+ description: "Re-enables a previously disabled human user. Already-active is a no-op. Only kind='user' principals can be enabled this way.",
119
+ sideEffects: "mutates-durable-state",
120
+ authorization: { permission: "user.manage" },
121
+ inputSchema: PRINCIPAL_ONLY_INPUT_SCHEMA,
122
+ },
123
+ {
124
+ name: "identity_role_create",
125
+ description: "Creates a new custom role. The role starts with no policies attached and therefore confers no permissions until one is attached, which is not an agent-callable operation.",
126
+ sideEffects: "mutates-durable-state",
127
+ authorization: { permission: "role.manage" },
128
+ inputSchema: {
129
+ type: "object",
130
+ additionalProperties: false,
131
+ required: ["name"],
132
+ properties: { name: REQUIRED_NAME_SCHEMA },
133
+ },
134
+ },
135
+ {
136
+ name: "identity_role_assign",
137
+ description: "Assigns an existing role to an existing user. This is the step that actually grants permissions. The caller can only confer permissions it already holds itself unconstrained, so assigning a role more powerful than the caller's own is refused. There is no tool to remove a role from a user, and no unassign operation exists anywhere in this system yet; tell the owner rather than trying another tool.",
138
+ sideEffects: "mutates-durable-state",
139
+ authorization: { permission: "role.manage" },
140
+ inputSchema: {
141
+ type: "object",
142
+ additionalProperties: false,
143
+ required: ["principalId", "roleId"],
144
+ properties: { principalId: PRINCIPAL_ID_SCHEMA, roleId: ROLE_ID_SCHEMA },
145
+ },
146
+ },
147
+ {
148
+ name: "identity_role_rename",
149
+ description: "Renames a custom role. Built-in roles cannot be renamed — relabelling 'viewer' to read as trusted is a social-engineering variant of privilege escalation, so it is refused.",
150
+ sideEffects: "mutates-durable-state",
151
+ authorization: { permission: "role.manage" },
152
+ inputSchema: {
153
+ type: "object",
154
+ additionalProperties: false,
155
+ required: ["roleId", "name"],
156
+ properties: { roleId: ROLE_ID_SCHEMA, name: REQUIRED_NAME_SCHEMA },
157
+ },
158
+ },
159
+ {
160
+ name: "identity_role_delete",
161
+ description: "Deletes a custom role. Built-in roles cannot be deleted. A role still assigned to at least one user is refused — this system has no unassign operation, so a role in use cannot be removed at all.",
162
+ sideEffects: "mutates-durable-state",
163
+ authorization: { permission: "role.manage" },
164
+ inputSchema: {
165
+ type: "object",
166
+ additionalProperties: false,
167
+ required: ["roleId"],
168
+ properties: { roleId: ROLE_ID_SCHEMA },
169
+ },
170
+ },
171
+ {
172
+ name: "identity_policy_create",
173
+ description: "Creates a new custom policy. The policy starts with no permissions and therefore confers none until permissions are added, which is not an agent-callable operation — identity_policy_write_permission is deliberately not wired (see this file's own header comment). Attaching a freshly created, still-empty policy to anyone is a no-op.",
174
+ sideEffects: "mutates-durable-state",
175
+ authorization: { permission: "role.manage" },
176
+ inputSchema: {
177
+ type: "object",
178
+ additionalProperties: false,
179
+ required: ["name"],
180
+ properties: {
181
+ name: REQUIRED_NAME_SCHEMA,
182
+ description: { type: "string", description: "Optional human-readable description." },
183
+ },
184
+ },
185
+ },
186
+ {
187
+ name: "identity_policy_update",
188
+ description: "Renames and/or re-describes a custom policy. Built-in and frozen policies are refused. At least one of 'name'/'description' must be supplied.",
189
+ sideEffects: "mutates-durable-state",
190
+ authorization: { permission: "role.manage" },
191
+ inputSchema: {
192
+ type: "object",
193
+ additionalProperties: false,
194
+ required: ["policyId"],
195
+ properties: {
196
+ policyId: POLICY_ID_SCHEMA,
197
+ name: { type: "string", minLength: 1, description: "New name. Omit to leave unchanged." },
198
+ description: { type: "string", description: "New description. Omit to leave unchanged." },
199
+ },
200
+ },
201
+ },
202
+ {
203
+ name: "identity_policy_delete",
204
+ description: "Deletes a custom policy. Built-in and frozen policies cannot be deleted. A policy still referenced by at least one role or attached to at least one user is refused — deletion fails safe (it only ever removes access, never orphans a reference) exactly like identity_role_delete.",
205
+ sideEffects: "mutates-durable-state",
206
+ authorization: { permission: "role.manage" },
207
+ inputSchema: {
208
+ type: "object",
209
+ additionalProperties: false,
210
+ required: ["policyId"],
211
+ properties: { policyId: POLICY_ID_SCHEMA },
212
+ },
213
+ },
214
+ {
215
+ name: "identity_policy_attach",
216
+ description:
217
+ // See docs/decisions/DR-002-grant-authority-and-admin-safety.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
218
+ "Attaches an existing policy directly to an existing user, granting every permission the policy carries. This is the direct-grant counterpart to identity_role_assign, targets exactly one named user per call, and carries the identical grant-authority clamp: the caller can only confer permissions it already holds itself unconstrained, so attaching a policy more powerful than the caller's own is refused. There is no detach operation.",
219
+ sideEffects: "mutates-durable-state",
220
+ authorization: { permission: "role.manage" },
221
+ inputSchema: {
222
+ type: "object",
223
+ additionalProperties: false,
224
+ required: ["principalId", "policyId"],
225
+ properties: { principalId: PRINCIPAL_ID_SCHEMA, policyId: POLICY_ID_SCHEMA },
226
+ },
227
+ },
228
+ ];
229
+ //# sourceMappingURL=agent-tools.js.map
@@ -0,0 +1,107 @@
1
+ import { type UserManagementMessages } from "./messages.js";
2
+ import type { SessionTokenPort } from "../core/runtime-ports.js";
3
+ import type { Clock, IdGenerator, ISODateTime, UUID } from "@jini-ai/core/primitives";
4
+ import type { IdentityRepos, PasswordHasherPort } from "../core/ports.js";
5
+ import { type PrincipalRecord, type SessionRecord } from "../core/types.js";
6
+ export interface AuthServiceDeps {
7
+ repos: IdentityRepos;
8
+ hasher: PasswordHasherPort;
9
+ clock: Clock;
10
+ idGen: IdGenerator;
11
+ tokens: SessionTokenPort;
12
+ }
13
+ /**
14
+ * Mint and persist a session for a principal the caller has ALREADY identified by some means
15
+ * other than this function (password verification in `login()` below, or a caller-specific proof
16
+ * such as a loopback-only single-use boot token). Factored out of `login()`'s former inline
17
+ * session-construction block (2026-09-06) so both paths share one minter — a host with no
18
+ * password to verify no longer has to hand-roll its own token hashing (and risk drifting from
19
+ * this file's private `hashToken`) just to mint a session for a principal it has already proven
20
+ * by other means.
21
+ *
22
+ * @complexity O(1) — one session write.
23
+ * @overallScore 100
24
+ */
25
+ export declare function createSessionForPrincipal(required: {
26
+ deps: AuthServiceDeps;
27
+ input: {
28
+ workspaceId: UUID;
29
+ principalId: UUID;
30
+ };
31
+ }, optional?: {
32
+ ip?: string | undefined;
33
+ userAgent?: string | undefined;
34
+ sessionTtlMs?: number | undefined;
35
+ messages?: UserManagementMessages | undefined;
36
+ }): Promise<{
37
+ session: SessionRecord;
38
+ rawToken: string;
39
+ }>;
40
+ /**
41
+ * Verify `username`+`password` for an `active` principal and mint a session
42
+ * Constant-shaped failure: a nonexistent username, a
43
+ * disabled principal, and a wrong password are all `AuthInvalidCredentialsError`
44
+ * with the same message — no user-enumeration signal.
45
+ *
46
+ * @complexity O(1) — one username lookup, one principal lookup, one hash
47
+ * verify, one session write (via `createSessionForPrincipal`), one `lastLoginAt` write.
48
+ * @overallScore 100
49
+ * See docs/decisions/DR-001-identity-and-session-boundary.md.
50
+ */
51
+ export declare function login(required: {
52
+ deps: AuthServiceDeps;
53
+ input: {
54
+ workspaceId: UUID;
55
+ username: string;
56
+ password: string;
57
+ };
58
+ }, optional?: {
59
+ ip?: string;
60
+ userAgent?: string;
61
+ sessionTtlMs?: number;
62
+ messages?: UserManagementMessages;
63
+ }): Promise<{
64
+ principal: PrincipalRecord;
65
+ session: SessionRecord;
66
+ rawToken: string;
67
+ }>;
68
+ /**
69
+ * Resolve a session cookie's raw token to its principal, applying every
70
+ * fail-closed check server-side : unknown
71
+ * token, revoked, past absolute `expiresAt`, or a disabled bound principal
72
+ * all resolve to `null` (the caller maps that to 401 `UNAUTHENTICATED` and
73
+ * never reaches `authorize`).
74
+ *
75
+ * @complexity O(1) — one token-hash lookup, one principal lookup.
76
+ * @overallScore 100
77
+ * See docs/decisions/DR-001-identity-and-session-boundary.md.
78
+ */
79
+ export declare function validateSession(required: {
80
+ deps: AuthServiceDeps;
81
+ input: {
82
+ workspaceId: UUID;
83
+ rawToken: string;
84
+ };
85
+ }, optional?: {
86
+ nowIso?: ISODateTime;
87
+ }): Promise<{
88
+ principal: PrincipalRecord;
89
+ session: SessionRecord;
90
+ } | null>;
91
+ /** Revoke the session bound to `rawToken`. Idempotent — already-revoked/unknown is a no-op. */
92
+ export declare function logout(required: {
93
+ deps: AuthServiceDeps;
94
+ input: {
95
+ workspaceId: UUID;
96
+ rawToken: string;
97
+ };
98
+ }): Promise<void>;
99
+ /** The introspection surface for `AUTH_ME` : dotted permission strings, `["*"]` for owner. See docs/decisions/DR-001-identity-and-session-boundary.md. */
100
+ export declare function getEffectivePermissions(required: {
101
+ deps: IdentityRepos;
102
+ input: {
103
+ workspaceId: UUID;
104
+ principalId: UUID;
105
+ };
106
+ }): Promise<string[]>;
107
+ //# sourceMappingURL=auth-service.d.ts.map
@@ -0,0 +1,170 @@
1
+ import { defaultUserManagementMessages } from "./messages.js";
2
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
3
+ import { resolveEffectivePermissions } from "./authorize.js";
4
+ import { AuthInvalidCredentialsError } from "../core/types.js";
5
+ import { normalizeUsername } from "./username.js";
6
+ /**
7
+ * @file Login / logout / session validation (state.spec `LOGIN`/`LOGOUT`).
8
+ *
9
+ * Purpose:
10
+ * The credential-verification + session-lifecycle surface `middleware/dev-auth.ts`
11
+ * wires into HTTP. Session tokens are hashed with SHA-256 before storage — the
12
+ * same technique `members/write-service.ts` uses for magic-link/member-session
13
+ * tokens (: the raw token is never persisted). Password verification is
14
+ * argon2id (`hasher.ts`), never a raw comparison.
15
+ *
16
+ * `createSessionForPrincipal` (2026-09-06) is the one session minter; `login` calls it after
17
+ * verifying credentials, and a host that identifies a principal by some other means (e.g. a
18
+ * loopback-only single-use boot token, which has nothing to verify a password against) calls it
19
+ * directly instead of re-implementing session construction. Before this export existed, a
20
+ * downstream host had duplicated this file's private `hashToken` byte-for-byte because
21
+ * `login` was the only minter and hard-requires a password — this closes that fork.
22
+ *
23
+ * Architectural role:
24
+ * Ordinary core functions (like `updatePost`/`membersWriteService`), not a
25
+ * port — session/credential logic has one implementation.
26
+ * See docs/decisions/DR-001-identity-and-session-boundary.md.
27
+ */
28
+ /** /behavior.spec §4: absolute session lifetime, fixed at creation; defaults to 30 days and is host-configurable. See docs/decisions/DR-001-identity-and-session-boundary.md. */
29
+ const DEFAULT_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
30
+ function isoPlusMs(nowIso, ms) {
31
+ return new Date(new Date(nowIso).getTime() + ms).toISOString();
32
+ }
33
+ /**
34
+ * Mint and persist a session for a principal the caller has ALREADY identified by some means
35
+ * other than this function (password verification in `login()` below, or a caller-specific proof
36
+ * such as a loopback-only single-use boot token). Factored out of `login()`'s former inline
37
+ * session-construction block (2026-09-06) so both paths share one minter — a host with no
38
+ * password to verify no longer has to hand-roll its own token hashing (and risk drifting from
39
+ * this file's private `hashToken`) just to mint a session for a principal it has already proven
40
+ * by other means.
41
+ *
42
+ * @complexity O(1) — one session write.
43
+ * @overallScore 100
44
+ */
45
+ export async function createSessionForPrincipal(required, optional = {}) {
46
+ const { deps } = required;
47
+ const input = { ...required.input, ...optional };
48
+ const nowIso = kernelNowIso({ clock: deps.clock });
49
+ const sessionTtlMs = optional.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
50
+ if (!Number.isFinite(sessionTtlMs) || sessionTtlMs <= 0)
51
+ throw new RangeError((optional.messages ?? defaultUserManagementMessages).invalidSessionTtl);
52
+ const rawToken = deps.tokens.newToken({});
53
+ const session = {
54
+ id: deps.idGen.newId(),
55
+ workspaceId: input.workspaceId,
56
+ principalId: input.principalId,
57
+ tokenHash: deps.tokens.hashToken({ rawToken }),
58
+ createdAt: nowIso,
59
+ expiresAt: isoPlusMs(nowIso, sessionTtlMs),
60
+ ip: input.ip,
61
+ userAgent: input.userAgent,
62
+ };
63
+ await deps.repos.sessions.save(session);
64
+ return { session, rawToken };
65
+ }
66
+ /**
67
+ * Verify `username`+`password` for an `active` principal and mint a session
68
+ * Constant-shaped failure: a nonexistent username, a
69
+ * disabled principal, and a wrong password are all `AuthInvalidCredentialsError`
70
+ * with the same message — no user-enumeration signal.
71
+ *
72
+ * @complexity O(1) — one username lookup, one principal lookup, one hash
73
+ * verify, one session write (via `createSessionForPrincipal`), one `lastLoginAt` write.
74
+ * @overallScore 100
75
+ * See docs/decisions/DR-001-identity-and-session-boundary.md.
76
+ */
77
+ export async function login(required, optional = {}) {
78
+ const { deps } = required;
79
+ const input = { ...required.input, ...optional };
80
+ const username = normalizeUsername({ raw: input.username });
81
+ if (!username || !input.password) {
82
+ throw new AuthInvalidCredentialsError({ message: (optional.messages ?? defaultUserManagementMessages).invalidCredentials });
83
+ }
84
+ const userRow = await deps.repos.users.findByUsername({ workspaceId: input.workspaceId, username });
85
+ if (!userRow) {
86
+ throw new AuthInvalidCredentialsError({ message: (optional.messages ?? defaultUserManagementMessages).invalidCredentials });
87
+ }
88
+ const principal = await deps.repos.principals.findById({
89
+ workspaceId: input.workspaceId,
90
+ id: userRow.principalId,
91
+ });
92
+ if (!principal || principal.status !== "active") {
93
+ throw new AuthInvalidCredentialsError({ message: (optional.messages ?? defaultUserManagementMessages).invalidCredentials });
94
+ }
95
+ const passwordOk = await deps.hasher.verify({ hash: userRow.passwordHash, password: input.password });
96
+ if (!passwordOk) {
97
+ throw new AuthInvalidCredentialsError({ message: (optional.messages ?? defaultUserManagementMessages).invalidCredentials });
98
+ }
99
+ const { session, rawToken } = await createSessionForPrincipal({ deps, input: { workspaceId: input.workspaceId, principalId: principal.id } }, { ip: input.ip, userAgent: input.userAgent, sessionTtlMs: optional.sessionTtlMs, messages: optional.messages });
100
+ // Reuses the session's own `createdAt` rather than a second `kernelNowIso({ clock: clock })` call — both are
101
+ // meant to be the same instant, and reading one avoids two clock reads ever disagreeing.
102
+ await deps.repos.users.save({ ...userRow, lastLoginAt: session.createdAt });
103
+ return { principal, session, rawToken };
104
+ }
105
+ /**
106
+ * Resolve a session cookie's raw token to its principal, applying every
107
+ * fail-closed check server-side : unknown
108
+ * token, revoked, past absolute `expiresAt`, or a disabled bound principal
109
+ * all resolve to `null` (the caller maps that to 401 `UNAUTHENTICATED` and
110
+ * never reaches `authorize`).
111
+ *
112
+ * @complexity O(1) — one token-hash lookup, one principal lookup.
113
+ * @overallScore 100
114
+ * See docs/decisions/DR-001-identity-and-session-boundary.md.
115
+ */
116
+ export async function validateSession(required, optional = {}) {
117
+ const { deps } = required;
118
+ const input = { ...required.input, ...optional };
119
+ const nowIso = input.nowIso ?? kernelNowIso({ clock: deps.clock });
120
+ const session = await deps.repos.sessions.findByTokenHash({
121
+ workspaceId: input.workspaceId,
122
+ tokenHash: deps.tokens.hashToken({ rawToken: input.rawToken }),
123
+ });
124
+ if (!session)
125
+ return null;
126
+ if (session.revokedAt)
127
+ return null;
128
+ // past absolute expiry is treated as revoked, never reaching authorize. See docs/decisions/DR-001-identity-and-session-boundary.md.
129
+ if (session.expiresAt <= nowIso)
130
+ return null;
131
+ const principal = await deps.repos.principals.findById({
132
+ workspaceId: input.workspaceId,
133
+ id: session.principalId,
134
+ });
135
+ // a disabled principal's live session stops validating immediately. See docs/decisions/DR-001-identity-and-session-boundary.md.
136
+ if (!principal || principal.status !== "active")
137
+ return null;
138
+ return { principal, session };
139
+ }
140
+ /** Revoke the session bound to `rawToken`. Idempotent — already-revoked/unknown is a no-op. */
141
+ export async function logout(required) {
142
+ const { deps, input } = required;
143
+ const session = await deps.repos.sessions.findByTokenHash({
144
+ workspaceId: input.workspaceId,
145
+ tokenHash: deps.tokens.hashToken({ rawToken: input.rawToken }),
146
+ });
147
+ if (!session || session.revokedAt)
148
+ return;
149
+ await deps.repos.sessions.revoke({
150
+ workspaceId: input.workspaceId,
151
+ id: session.id,
152
+ revokedAt: kernelNowIso({ clock: deps.clock }),
153
+ });
154
+ }
155
+ /** The introspection surface for `AUTH_ME` : dotted permission strings, `["*"]` for owner. See docs/decisions/DR-001-identity-and-session-boundary.md. */
156
+ export async function getEffectivePermissions(required) {
157
+ const rows = await resolveEffectivePermissions({
158
+ deps: {
159
+ principals: required.deps.principals,
160
+ principalRoles: required.deps.principalRoles,
161
+ rolePolicies: required.deps.rolePolicies,
162
+ principalPolicies: required.deps.principalPolicies,
163
+ policyPermissions: required.deps.policyPermissions,
164
+ },
165
+ principalId: required.input.principalId,
166
+ workspaceId: required.input.workspaceId,
167
+ });
168
+ return [...new Set(rows.map((row) => row.permission))];
169
+ }
170
+ //# sourceMappingURL=auth-service.js.map