@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.
- package/CHANGELOG.md +77 -0
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/dist/admin/adapters/http.d.ts +29 -0
- package/dist/admin/adapters/http.js +69 -0
- package/dist/admin/adapters/memory.d.ts +61 -0
- package/dist/admin/adapters/memory.js +217 -0
- package/dist/admin/auth.module.d.ts +30 -0
- package/dist/admin/auth.module.js +12 -0
- package/dist/admin/conformance/index.d.ts +56 -0
- package/dist/admin/conformance/index.js +156 -0
- package/dist/admin/controllers/auth.controller.d.ts +23 -0
- package/dist/admin/controllers/auth.controller.js +33 -0
- package/dist/admin/controllers/controller-store.d.ts +18 -0
- package/dist/admin/controllers/controller-store.js +23 -0
- package/dist/admin/controllers/members.controller.d.ts +33 -0
- package/dist/admin/controllers/members.controller.js +119 -0
- package/dist/admin/controllers/roles.controller.d.ts +47 -0
- package/dist/admin/controllers/roles.controller.js +282 -0
- package/dist/admin/controllers/users.controller.d.ts +53 -0
- package/dist/admin/controllers/users.controller.js +220 -0
- package/dist/admin/index.d.ts +21 -0
- package/dist/admin/index.js +15 -0
- package/dist/admin/members.module.d.ts +34 -0
- package/dist/admin/members.module.js +13 -0
- package/dist/admin/messages.en.d.ts +37 -0
- package/dist/admin/messages.en.js +21 -0
- package/dist/admin/models.d.ts +133 -0
- package/dist/admin/models.js +2 -0
- package/dist/admin/people.rules.d.ts +29 -0
- package/dist/admin/people.rules.js +51 -0
- package/dist/admin/ports.d.ts +187 -0
- package/dist/admin/ports.js +17 -0
- package/dist/admin/react/components/DestructiveDialog.d.ts +5 -0
- package/dist/admin/react/components/DestructiveDialog.js +6 -0
- package/dist/admin/react/components/MemberRow.d.ts +5 -0
- package/dist/admin/react/components/MemberRow.js +8 -0
- package/dist/admin/react/components/MembersConfirmation.d.ts +2 -0
- package/dist/admin/react/components/MembersConfirmation.js +8 -0
- package/dist/admin/react/components/PermissionPanel.d.ts +5 -0
- package/dist/admin/react/components/PermissionPanel.js +9 -0
- package/dist/admin/react/components/PolicyRow.d.ts +5 -0
- package/dist/admin/react/components/PolicyRow.js +11 -0
- package/dist/admin/react/components/RoleRow.d.ts +5 -0
- package/dist/admin/react/components/RoleRow.js +10 -0
- package/dist/admin/react/components/UserRow.d.ts +5 -0
- package/dist/admin/react/components/UserRow.js +8 -0
- package/dist/admin/react/components/UsersConfirmation.d.ts +3 -0
- package/dist/admin/react/components/UsersConfirmation.js +9 -0
- package/dist/admin/react/hooks/AuthBinding.hooks.d.ts +39 -0
- package/dist/admin/react/hooks/AuthBinding.hooks.js +21 -0
- package/dist/admin/react/hooks/LoginPage.hooks.d.ts +54 -0
- package/dist/admin/react/hooks/LoginPage.hooks.js +23 -0
- package/dist/admin/react/hooks/MembersBinding.hooks.d.ts +35 -0
- package/dist/admin/react/hooks/MembersBinding.hooks.js +21 -0
- package/dist/admin/react/hooks/MembersPage.hooks.d.ts +99 -0
- package/dist/admin/react/hooks/MembersPage.hooks.js +38 -0
- package/dist/admin/react/hooks/PeopleController.hooks.d.ts +18 -0
- package/dist/admin/react/hooks/PeopleController.hooks.js +20 -0
- package/dist/admin/react/hooks/PermissionPanel.hooks.d.ts +30 -0
- package/dist/admin/react/hooks/PermissionPanel.hooks.js +15 -0
- package/dist/admin/react/hooks/PoliciesTab.hooks.d.ts +47 -0
- package/dist/admin/react/hooks/PoliciesTab.hooks.js +26 -0
- package/dist/admin/react/hooks/RolesBinding.hooks.d.ts +39 -0
- package/dist/admin/react/hooks/RolesBinding.hooks.js +23 -0
- package/dist/admin/react/hooks/RolesController.hooks.d.ts +43 -0
- package/dist/admin/react/hooks/RolesController.hooks.js +25 -0
- package/dist/admin/react/hooks/RolesPage.hooks.d.ts +105 -0
- package/dist/admin/react/hooks/RolesPage.hooks.js +33 -0
- package/dist/admin/react/hooks/RolesTab.hooks.d.ts +35 -0
- package/dist/admin/react/hooks/RolesTab.hooks.js +19 -0
- package/dist/admin/react/hooks/UsersBinding.hooks.d.ts +36 -0
- package/dist/admin/react/hooks/UsersBinding.hooks.js +21 -0
- package/dist/admin/react/hooks/UsersPage.hooks.d.ts +106 -0
- package/dist/admin/react/hooks/UsersPage.hooks.js +22 -0
- package/dist/admin/react/hooks/UsersTab.hooks.d.ts +101 -0
- package/dist/admin/react/hooks/UsersTab.hooks.js +53 -0
- package/dist/admin/react/index.d.ts +203 -0
- package/dist/admin/react/index.js +32 -0
- package/dist/admin/react/pages/LoginPage.d.ts +3 -0
- package/dist/admin/react/pages/LoginPage.js +13 -0
- package/dist/admin/react/pages/MembersPage.d.ts +3 -0
- package/dist/admin/react/pages/MembersPage.js +19 -0
- package/dist/admin/react/pages/RolesPage.d.ts +3 -0
- package/dist/admin/react/pages/RolesPage.js +19 -0
- package/dist/admin/react/pages/UsersPage.d.ts +3 -0
- package/dist/admin/react/pages/UsersPage.js +19 -0
- package/dist/admin/react/tabs/AuthTab.d.ts +4 -0
- package/dist/admin/react/tabs/AuthTab.js +8 -0
- package/dist/admin/react/tabs/MembersTab.d.ts +4 -0
- package/dist/admin/react/tabs/MembersTab.js +9 -0
- package/dist/admin/react/tabs/PoliciesTab.d.ts +4 -0
- package/dist/admin/react/tabs/PoliciesTab.js +9 -0
- package/dist/admin/react/tabs/RolesTab.d.ts +4 -0
- package/dist/admin/react/tabs/RolesTab.js +9 -0
- package/dist/admin/react/tabs/UsersTab.d.ts +4 -0
- package/dist/admin/react/tabs/UsersTab.js +9 -0
- package/dist/admin/roles.module.d.ts +48 -0
- package/dist/admin/roles.module.js +15 -0
- package/dist/admin/rules.d.ts +32 -0
- package/dist/admin/rules.js +32 -0
- package/dist/admin/users.module.d.ts +39 -0
- package/dist/admin/users.module.js +14 -0
- package/dist/core/builtin-permissions.d.ts +10 -0
- package/dist/core/builtin-permissions.js +402 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +9 -0
- package/dist/core/permissions.d.ts +40 -0
- package/dist/core/permissions.js +52 -0
- package/dist/core/ports.d.ts +237 -0
- package/dist/core/ports.js +2 -0
- package/dist/core/runtime-ports.d.ts +8 -0
- package/dist/core/runtime-ports.js +2 -0
- package/dist/core/types.d.ts +224 -0
- package/dist/core/types.js +93 -0
- package/dist/react/auth/Login.d.ts +9 -0
- package/dist/react/auth/Login.js +8 -0
- package/dist/react/auth/hooks/use-login.hooks.d.ts +22 -0
- package/dist/react/auth/hooks/use-login.hooks.js +24 -0
- package/dist/react/errors.d.ts +10 -0
- package/dist/react/errors.js +13 -0
- package/dist/react/index.d.ts +18 -0
- package/dist/react/index.js +9 -0
- package/dist/react/members/Members.d.ts +6 -0
- package/dist/react/members/Members.js +38 -0
- package/dist/react/members/hooks/use-members.hooks.d.ts +32 -0
- package/dist/react/members/hooks/use-members.hooks.js +120 -0
- package/dist/react/members/rules.d.ts +26 -0
- package/dist/react/members/rules.js +35 -0
- package/dist/react/models.d.ts +50 -0
- package/dist/react/models.js +2 -0
- package/dist/react/ports.d.ts +157 -0
- package/dist/react/ports.js +2 -0
- package/dist/react/roles/Roles.d.ts +100 -0
- package/dist/react/roles/Roles.hooks.d.ts +17 -0
- package/dist/react/roles/Roles.hooks.js +40 -0
- package/dist/react/roles/Roles.js +150 -0
- package/dist/react/roles/hooks/use-roles.hooks.d.ts +80 -0
- package/dist/react/roles/hooks/use-roles.hooks.js +303 -0
- package/dist/react/roles/rules.d.ts +35 -0
- package/dist/react/roles/rules.js +31 -0
- package/dist/react/roles/text.d.ts +27 -0
- package/dist/react/roles/text.js +13 -0
- package/dist/react/testing.d.ts +51 -0
- package/dist/react/testing.js +275 -0
- package/dist/react/users/Users.d.ts +72 -0
- package/dist/react/users/Users.js +129 -0
- package/dist/react/users/hooks/use-reset-password-fields.hooks.d.ts +16 -0
- package/dist/react/users/hooks/use-reset-password-fields.hooks.js +21 -0
- package/dist/react/users/hooks/use-users.hooks.d.ts +77 -0
- package/dist/react/users/hooks/use-users.hooks.js +286 -0
- package/dist/react/users/rules.d.ts +33 -0
- package/dist/react/users/rules.js +61 -0
- package/dist/server/admin-crud-service.d.ts +295 -0
- package/dist/server/admin-crud-service.js +561 -0
- package/dist/server/agent-tool-input.d.ts +58 -0
- package/dist/server/agent-tool-input.js +158 -0
- package/dist/server/agent-tool-registrations.d.ts +51 -0
- package/dist/server/agent-tool-registrations.js +417 -0
- package/dist/server/agent-tools.d.ts +88 -0
- package/dist/server/agent-tools.js +229 -0
- package/dist/server/auth-service.d.ts +107 -0
- package/dist/server/auth-service.js +170 -0
- package/dist/server/authorize.d.ts +92 -0
- package/dist/server/authorize.js +96 -0
- package/dist/server/grant-service.d.ts +219 -0
- package/dist/server/grant-service.js +419 -0
- package/dist/server/hasher.d.ts +64 -0
- package/dist/server/hasher.js +53 -0
- package/dist/server/identity-transaction.d.ts +9 -0
- package/dist/server/identity-transaction.js +8 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +20 -0
- package/dist/server/messages.d.ts +7 -0
- package/dist/server/messages.js +5 -0
- package/dist/server/password-policy.d.ts +59 -0
- package/dist/server/password-policy.js +66 -0
- package/dist/server/permission-migrations.d.ts +84 -0
- package/dist/server/permission-migrations.js +77 -0
- package/dist/server/repo.memory-transactions.d.ts +16 -0
- package/dist/server/repo.memory-transactions.js +79 -0
- package/dist/server/repo.memory.d.ts +186 -0
- package/dist/server/repo.memory.js +230 -0
- package/dist/server/seed.d.ts +56 -0
- package/dist/server/seed.js +435 -0
- package/dist/server/session-tokens.d.ts +10 -0
- package/dist/server/session-tokens.js +12 -0
- package/dist/server/username.d.ts +14 -0
- package/dist/server/username.js +14 -0
- package/package.json +144 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The enforcing boundary parser for identity agent-tool input — the identity domain's
|
|
3
|
+
* analogue of `features/content-types/field-defs.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Deliberate divergence from content-types, disclosed:
|
|
6
|
+
* content-types hand-authors its published `inputSchema` and hand-authors `field-defs.ts`
|
|
7
|
+
* separately, then pins the two together with a fixture corpus
|
|
8
|
+
* (`agent-tools.schema-agreement.unit.test.ts`) because deriving one from the other would have
|
|
9
|
+
* needed either a JSON Schema library this project does not depend on or a code generator. That
|
|
10
|
+
* reasoning does not carry over here: identity's schemas use a small keyword subset and describe
|
|
11
|
+
* flat objects of strings, so this file INTERPRETS that published schema directly rather than
|
|
12
|
+
* duplicating it. Sharing the artifact prevents metadata drift; the independent agreement tests
|
|
13
|
+
* still verify interpreter semantics, including Unicode length and unsupported schema shapes.
|
|
14
|
+
*
|
|
15
|
+
* What remains genuinely testable, and is tested in
|
|
16
|
+
* `__tests__/agent-tools.schema-agreement.test.ts`: that this interpreter actually implements
|
|
17
|
+
* every keyword the catalog uses (an unknown keyword fails loudly rather than being ignored, which
|
|
18
|
+
* is one way a schema could still promise something unenforced), and that the wired handlers
|
|
19
|
+
* really route their input through here rather than reading `ctx.input` directly.
|
|
20
|
+
*
|
|
21
|
+
* Architectural role:
|
|
22
|
+
* `identity` library, pure. No I/O, no enforcement of permissions — this is shape validation only.
|
|
23
|
+
* Returns a `Result` rather than throwing so the caller decides how a rejection is surfaced.
|
|
24
|
+
*/
|
|
25
|
+
/** The keywords this interpreter implements. Anything else in a schema is a hard error, never ignored. */
|
|
26
|
+
const OBJECT_KEYWORDS = new Set(["type", "properties", "required", "additionalProperties", "description"]);
|
|
27
|
+
const STRING_KEYWORDS = new Set(["type", "minLength", "description"]);
|
|
28
|
+
/**
|
|
29
|
+
* Asserts a schema uses only keywords {@link parseIdentityToolInput} implements.
|
|
30
|
+
*
|
|
31
|
+
* Separated from the parse loop and run over the WHOLE schema before any value is inspected, so an
|
|
32
|
+
* unimplemented keyword is reported even for an input that happens not to exercise it. Without
|
|
33
|
+
* that ordering a schema could publish, say, a `pattern` the parser silently ignores, and every
|
|
34
|
+
* test using a conforming fixture would still pass.
|
|
35
|
+
*
|
|
36
|
+
* @param schema - A tool's published `inputSchema`.
|
|
37
|
+
* @throws {Error} If the schema, or any property sub-schema, uses an unimplemented keyword.
|
|
38
|
+
* @complexity O(k) in the total keyword count across the schema and its property sub-schemas.
|
|
39
|
+
* @overallScore 100
|
|
40
|
+
*/
|
|
41
|
+
function assertSchemaIsInterpretable(schema) {
|
|
42
|
+
if (!isSchemaObject(schema))
|
|
43
|
+
throw new Error("identity agent-tool schema must be an object");
|
|
44
|
+
assertKeywords(schema, OBJECT_KEYWORDS);
|
|
45
|
+
if (schema.type !== "object") {
|
|
46
|
+
throw new Error(`identity agent-tool schema must be an object schema, got type '${String(schema.type)}'`);
|
|
47
|
+
}
|
|
48
|
+
if (schema.additionalProperties !== false) {
|
|
49
|
+
throw new Error("identity agent-tool schema must declare additionalProperties: false");
|
|
50
|
+
}
|
|
51
|
+
const properties = Object.hasOwn(schema, "properties") ? schema.properties : {};
|
|
52
|
+
if (!isSchemaObject(properties))
|
|
53
|
+
throw new Error("identity agent-tool schema properties must be an object");
|
|
54
|
+
const required = Object.hasOwn(schema, "required") ? schema.required : [];
|
|
55
|
+
if (!Array.isArray(required) || required.some(key => typeof key !== "string") || new Set(required).size !== required.length) {
|
|
56
|
+
throw new Error("identity agent-tool schema required must be an array of unique strings");
|
|
57
|
+
}
|
|
58
|
+
for (const key of required) {
|
|
59
|
+
if (!Object.hasOwn(properties, key))
|
|
60
|
+
throw new Error(`identity agent-tool schema requires undeclared property '${key}'`);
|
|
61
|
+
}
|
|
62
|
+
for (const [key, sub] of Object.entries(properties)) {
|
|
63
|
+
if (!isSchemaObject(sub))
|
|
64
|
+
throw new Error(`identity agent-tool schema property '${key}' must be an object`);
|
|
65
|
+
assertKeywords(sub, STRING_KEYWORDS);
|
|
66
|
+
if (sub.type !== "string") {
|
|
67
|
+
throw new Error(`identity agent-tool schema declares non-string property '${key}' — this parser only implements string properties`);
|
|
68
|
+
}
|
|
69
|
+
if (Object.hasOwn(sub, "minLength") && (typeof sub.minLength !== "number" || !Number.isInteger(sub.minLength) || sub.minLength < 0)) {
|
|
70
|
+
throw new Error(`identity agent-tool schema minLength for '${key}' must be a nonnegative integer`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** JSON Schema objects and keyword placement are checked before inspecting any input value. */
|
|
75
|
+
function isSchemaObject(value) {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
/** An unsupported or misplaced keyword is a developer error, never a silent no-op. */
|
|
79
|
+
function assertKeywords(schema, supported) {
|
|
80
|
+
for (const keyword of Object.keys(schema)) {
|
|
81
|
+
if (!supported.has(keyword)) {
|
|
82
|
+
throw new Error(`identity agent-tool schema uses unimplemented keyword '${keyword}' — extend agent-tool-input.ts rather than publishing a contract it cannot enforce`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (Object.hasOwn(schema, "description") && typeof schema.description !== "string") {
|
|
86
|
+
throw new Error("identity agent-tool schema description must be a string");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validates `input` against a tool's own published `inputSchema` and returns the extracted string
|
|
91
|
+
* values.
|
|
92
|
+
*
|
|
93
|
+
* Every identity tool's input is a closed, flat object whose properties are all strings, so the
|
|
94
|
+
* return type is narrowed to `Record<string, string>` — a caller reads `value.principalId` without
|
|
95
|
+
* re-checking the type, which is what keeps the handlers in `tool-registrations.ts` free of ad hoc
|
|
96
|
+
* `typeof` guards that could disagree with the schema.
|
|
97
|
+
*
|
|
98
|
+
* Rejection messages name the offending KEY and the rule it broke, and never echo the offending
|
|
99
|
+
* VALUE: a tool input carries operator content (a username, an email) and these messages travel
|
|
100
|
+
* back to the model and into the tool-attempt audit trail.
|
|
101
|
+
*
|
|
102
|
+
* @param required.schema - The tool's published `inputSchema`, used as the contract itself.
|
|
103
|
+
* @param required.input - The raw `ctx.input`, of any shape.
|
|
104
|
+
* @returns `{ok:true, value}` with each declared property present in `input` extracted, or
|
|
105
|
+
* `{ok:false, error}` naming the first violation found.
|
|
106
|
+
* @throws {Error} Only if the schema itself is uninterpretable — a developer error, distinct from
|
|
107
|
+
* a bad input, and therefore not folded into the `Result`.
|
|
108
|
+
* @complexity O(p + k + s) in declared properties, own input keys, and string code points.
|
|
109
|
+
* @overallScore 100
|
|
110
|
+
*/
|
|
111
|
+
export function parseIdentityToolInput(required) {
|
|
112
|
+
const schema = required.schema;
|
|
113
|
+
assertSchemaIsInterpretable(schema);
|
|
114
|
+
if (schema.type !== "object") {
|
|
115
|
+
throw new Error(`identity agent-tool schema must be an object schema, got type '${String(schema.type)}'`);
|
|
116
|
+
}
|
|
117
|
+
const { input } = required;
|
|
118
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
119
|
+
return { ok: false, error: new Error("input must be an object") };
|
|
120
|
+
}
|
|
121
|
+
const record = input;
|
|
122
|
+
const properties = (schema.properties ?? {});
|
|
123
|
+
if (schema.additionalProperties === false) {
|
|
124
|
+
for (const key of Object.keys(record)) {
|
|
125
|
+
if (!Object.hasOwn(properties, key)) {
|
|
126
|
+
return { ok: false, error: new Error(`'${key}' is not a recognized input key`) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const key of schema.required ?? []) {
|
|
131
|
+
if (!Object.hasOwn(record, key) || record[key] === undefined) {
|
|
132
|
+
return { ok: false, error: new Error(`'${key}' is required`) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const value = {};
|
|
136
|
+
for (const [key, sub] of Object.entries(properties)) {
|
|
137
|
+
if (!Object.hasOwn(record, key))
|
|
138
|
+
continue;
|
|
139
|
+
const candidate = record[key];
|
|
140
|
+
if (typeof candidate !== "string") {
|
|
141
|
+
return { ok: false, error: new Error(`'${key}' must be a string, received ${describeType(candidate)}`) };
|
|
142
|
+
}
|
|
143
|
+
if (typeof sub.minLength === "number" && Array.from(candidate).length < sub.minLength) {
|
|
144
|
+
return { ok: false, error: new Error(`'${key}' must contain at least ${sub.minLength} Unicode code point(s)`) };
|
|
145
|
+
}
|
|
146
|
+
Object.defineProperty(value, key, { value: candidate, enumerable: true, writable: true, configurable: true });
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, value };
|
|
149
|
+
}
|
|
150
|
+
/** Names a value's type for a rejection message WITHOUT echoing the value itself. */
|
|
151
|
+
function describeType(value) {
|
|
152
|
+
if (value === null)
|
|
153
|
+
return "null";
|
|
154
|
+
if (Array.isArray(value))
|
|
155
|
+
return "an array";
|
|
156
|
+
return `a ${typeof value}`;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=agent-tool-input.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Clock, IdGenerator } from "@jini-ai/core/primitives";
|
|
2
|
+
/**
|
|
3
|
+
* @file Identity's half of the agent-tool authorization wiring: maps `agent-tools.ts`'s fifteen catalog
|
|
4
|
+
* entries onto the users/roles/policies transitions `grant-service.ts`/`admin-crud-service.ts`
|
|
5
|
+
* export, plus the three repo-backed reads, as `ToolRegistration`s.
|
|
6
|
+
*
|
|
7
|
+
* `ToolPolicy.authorize` is a pass-through for all fifteen (see `buildDomainRegistrations`) for the
|
|
8
|
+
* "one evaluator" reason: the gate is the domain layer's, reached identically by every
|
|
9
|
+
* tool kind — mutations through their own service function, which opens with the check, and reads
|
|
10
|
+
* through {@link assertIdentityReadAllowed}, which calls the domain's own exported
|
|
11
|
+
* `assertCallerHasAnyPermission` rather than re-deriving an OR gate. A second check here would be a
|
|
12
|
+
* duplicate evaluator, and one any non-tool caller of the same service function would bypass. See
|
|
13
|
+
* `assistant/__tests__/tool-registrations.identity-authorization.test.ts`.
|
|
14
|
+
*/
|
|
15
|
+
import { type DerivedRiskByToolId, type ToolRegistration } from "@jini-ai/core";
|
|
16
|
+
import type { AuthServiceDeps } from "./auth-service.js";
|
|
17
|
+
import type { IdentityRepos, PasswordHasherPort, PolicyPermissionRepoPort, PolicyRepoPort, PrincipalPolicyRepoPort, PrincipalRepoPort, PrincipalRoleRepoPort, RolePolicyRepoPort, RoleRepoPort, SessionRepoPort, UserRepoPort } from "../core/index.js";
|
|
18
|
+
/**
|
|
19
|
+
* The exact slice of the route-deps bag Identity's tool handlers read. Declared structurally
|
|
20
|
+
* (rather than importing `server/routes/types`'s `RouteDeps`) so this module carries no back-edge
|
|
21
|
+
* into the composition root. `server/routes/*` satisfies this structurally by passing its existing
|
|
22
|
+
* `RouteDeps` (in practice a users-route subset) object after binding the shared transaction
|
|
23
|
+
* and session-token ports and using the kernel clock/ID contracts.
|
|
24
|
+
*/
|
|
25
|
+
export interface IdentityToolDeps {
|
|
26
|
+
workspaceId: string;
|
|
27
|
+
clock: Clock;
|
|
28
|
+
idGen: IdGenerator;
|
|
29
|
+
principalRepo: PrincipalRepoPort;
|
|
30
|
+
userRepo: UserRepoPort;
|
|
31
|
+
sessionRepo: SessionRepoPort;
|
|
32
|
+
roleRepo: RoleRepoPort;
|
|
33
|
+
policyRepo: PolicyRepoPort;
|
|
34
|
+
policyPermissionRepo: PolicyPermissionRepoPort;
|
|
35
|
+
rolePolicyRepo: RolePolicyRepoPort;
|
|
36
|
+
principalRoleRepo: PrincipalRoleRepoPort;
|
|
37
|
+
principalPolicyRepo: PrincipalPolicyRepoPort;
|
|
38
|
+
passwordHasher: PasswordHasherPort;
|
|
39
|
+
ownerPrincipalId: Promise<string>;
|
|
40
|
+
/** Bind the same transaction port as every grant/status writer. */
|
|
41
|
+
transactions: IdentityRepos["transactions"];
|
|
42
|
+
tokens: AuthServiceDeps["tokens"];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
46
|
+
* calls. See `DerivedRiskByToolId` in the kit for why it is independent of the catalog's own
|
|
47
|
+
* `sideEffects` declaration.
|
|
48
|
+
*/
|
|
49
|
+
export declare const identityDerivedRisk: DerivedRiskByToolId;
|
|
50
|
+
export declare function buildIdentityRegistrations(routeDeps: IdentityToolDeps): ToolRegistration[];
|
|
51
|
+
//# sourceMappingURL=agent-tool-registrations.d.ts.map
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Identity's half of the agent-tool authorization wiring: maps `agent-tools.ts`'s fifteen catalog
|
|
3
|
+
* entries onto the users/roles/policies transitions `grant-service.ts`/`admin-crud-service.ts`
|
|
4
|
+
* export, plus the three repo-backed reads, as `ToolRegistration`s.
|
|
5
|
+
*
|
|
6
|
+
* `ToolPolicy.authorize` is a pass-through for all fifteen (see `buildDomainRegistrations`) for the
|
|
7
|
+
* "one evaluator" reason: the gate is the domain layer's, reached identically by every
|
|
8
|
+
* tool kind — mutations through their own service function, which opens with the check, and reads
|
|
9
|
+
* through {@link assertIdentityReadAllowed}, which calls the domain's own exported
|
|
10
|
+
* `assertCallerHasAnyPermission` rather than re-deriving an OR gate. A second check here would be a
|
|
11
|
+
* duplicate evaluator, and one any non-tool caller of the same service function would bypass. See
|
|
12
|
+
* `assistant/__tests__/tool-registrations.identity-authorization.test.ts`.
|
|
13
|
+
*/
|
|
14
|
+
import { buildDomainRegistrations, indexCatalogById, } from "@jini-ai/core";
|
|
15
|
+
import { parseIdentityToolInput } from "./agent-tool-input.js";
|
|
16
|
+
import { identityAgentToolCatalog } from "./agent-tools.js";
|
|
17
|
+
import { assertCallerHasAnyPermission, assignRole, attachPolicy, createPolicy, createRole, createUser } from "./grant-service.js";
|
|
18
|
+
import { deletePolicy, deleteRole, disablePrincipal, enablePrincipal, updatePolicy, updateRole, updateUser } from "./admin-crud-service.js";
|
|
19
|
+
const CATALOG_BY_ID = indexCatalogById({ catalog: identityAgentToolCatalog });
|
|
20
|
+
/**
|
|
21
|
+
* Assembles `auth-service.ts`'s `IdentityRepos` bag from {@link IdentityToolDeps}'s flat fields — a
|
|
22
|
+
* local duplicate of `server/routes/admin/users/deps.ts`'s `identityReposFrom` rather than an import
|
|
23
|
+
* of it, for the same reason `features/settings/tool-registrations.ts`'s header gives for
|
|
24
|
+
* duplicating `toWriteServiceDeps`: importing from the HTTP admin layer would invert this codebase's
|
|
25
|
+
* ports/adapters direction. It is a field mapping, not an authorization decision. Host route bags must bind the same
|
|
26
|
+
* transaction and session-token ports so all paths share persistence semantics.
|
|
27
|
+
*/
|
|
28
|
+
function identityReposFrom(deps) {
|
|
29
|
+
return {
|
|
30
|
+
transactions: deps.transactions,
|
|
31
|
+
principals: deps.principalRepo,
|
|
32
|
+
users: deps.userRepo,
|
|
33
|
+
sessions: deps.sessionRepo,
|
|
34
|
+
roles: deps.roleRepo,
|
|
35
|
+
policies: deps.policyRepo,
|
|
36
|
+
policyPermissions: deps.policyPermissionRepo,
|
|
37
|
+
rolePolicies: deps.rolePolicyRepo,
|
|
38
|
+
principalRoles: deps.principalRoleRepo,
|
|
39
|
+
principalPolicies: deps.principalPolicyRepo,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Assembles `grant-service.ts`'s/`admin-crud-service.ts`'s `AuthServiceDeps` bag from
|
|
43
|
+
* {@link IdentityToolDeps} — the local twin of `identityReposFrom` just above. */
|
|
44
|
+
function identityServiceDepsFrom(deps) {
|
|
45
|
+
return {
|
|
46
|
+
repos: identityReposFrom(deps),
|
|
47
|
+
hasher: deps.passwordHasher,
|
|
48
|
+
clock: deps.clock,
|
|
49
|
+
idGen: deps.idGen,
|
|
50
|
+
tokens: deps.tokens,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
55
|
+
* calls. See `DerivedRiskByToolId` in the kit for why it is independent of the catalog's own
|
|
56
|
+
* `sideEffects` declaration.
|
|
57
|
+
*/
|
|
58
|
+
export const identityDerivedRisk = new Map([
|
|
59
|
+
// -> authorize() + repo reads only; no save on any path.
|
|
60
|
+
["identity_user_list", "none"],
|
|
61
|
+
["identity_role_list", "none"],
|
|
62
|
+
["identity_policy_list", "none"],
|
|
63
|
+
// -> createUser (grant-service.ts): saves a principal row AND a users row (credential).
|
|
64
|
+
["identity_user_create", "mutates-durable-state"],
|
|
65
|
+
// -> updateUser (admin-crud-service.ts): users.save, email field only.
|
|
66
|
+
["identity_user_update_email", "mutates-durable-state"],
|
|
67
|
+
// -> disablePrincipal (admin-crud-service.ts): status flip, guarded by the owner floor. See docs/decisions/DR-007-workspace-and-owner-floors.md.
|
|
68
|
+
["identity_user_disable", "mutates-durable-state"],
|
|
69
|
+
// -> enablePrincipal (admin-crud-service.ts): status flip back to active.
|
|
70
|
+
["identity_user_enable", "mutates-durable-state"],
|
|
71
|
+
// -> createRole (grant-service.ts): roles.save, always isBuiltin=false.
|
|
72
|
+
["identity_role_create", "mutates-durable-state"],
|
|
73
|
+
// -> assignRole (grant-service.ts): principalRoles.save behind the grant clamp. The one. See docs/decisions/DR-007-workspace-and-owner-floors.md.
|
|
74
|
+
// identity tool that confers permissions, hence the heaviest of these.
|
|
75
|
+
["identity_role_assign", "mutates-durable-state"],
|
|
76
|
+
// -> updateRole (admin-crud-service.ts): roles.save, refuses built-ins.
|
|
77
|
+
["identity_role_rename", "mutates-durable-state"],
|
|
78
|
+
// -> deleteRole (admin-crud-service.ts): roles.delete, refuses built-ins and referenced roles.
|
|
79
|
+
["identity_role_delete", "mutates-durable-state"],
|
|
80
|
+
// -> createPolicy (grant-service.ts): policies.save, always isBuiltin=false/isFrozen=false.
|
|
81
|
+
["identity_policy_create", "mutates-durable-state"],
|
|
82
|
+
// -> updatePolicy (admin-crud-service.ts): policies.save, refuses built-in/frozen.
|
|
83
|
+
["identity_policy_update", "mutates-durable-state"],
|
|
84
|
+
// -> deletePolicy (admin-crud-service.ts): policyPermissions.deleteByPolicyId + policies.delete,
|
|
85
|
+
// refuses built-in/frozen and still-referenced policies.
|
|
86
|
+
["identity_policy_delete", "mutates-durable-state"],
|
|
87
|
+
// -> attachPolicy (grant-service.ts): principalPolicies.save behind the grant clamp — the. See docs/decisions/DR-007-workspace-and-owner-floors.md.
|
|
88
|
+
// direct-grant counterpart to identity_role_assign, same weight.
|
|
89
|
+
["identity_policy_attach", "mutates-durable-state"],
|
|
90
|
+
]);
|
|
91
|
+
/**
|
|
92
|
+
* Upper bound on the roster `identity_user_list` will fan out over.
|
|
93
|
+
*
|
|
94
|
+
* `routes/admin/users/list.ts` does the same O(n) per-user fan-out uncapped, on the documented
|
|
95
|
+
* "operator-managed roster, not member/content scale" assumption. That assumption is fine for a
|
|
96
|
+
* human screen and NOT fine here for a different reason than scale: this result is spent as model
|
|
97
|
+
* context, so an unexpectedly large roster would silently consume the run's context window. The
|
|
98
|
+
* cap truncates and SAYS SO in the payload rather than failing, so a caller that hits it can still
|
|
99
|
+
* work with what it got and knows not to treat the list as complete.
|
|
100
|
+
*/
|
|
101
|
+
const IDENTITY_USER_LIST_MAX = 200;
|
|
102
|
+
function requireIdentityCatalogEntry(toolId) {
|
|
103
|
+
const entry = CATALOG_BY_ID.get(toolId);
|
|
104
|
+
if (!entry)
|
|
105
|
+
throw new Error(`agent-tool-registrations: catalog has no entry named '${toolId}' — @jini-ai/user-management/server catalog drifted`);
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
function identityInput(toolId, input) {
|
|
109
|
+
const entry = requireIdentityCatalogEntry(toolId);
|
|
110
|
+
const parsed = parseIdentityToolInput({ schema: entry.inputSchema, input });
|
|
111
|
+
if (!parsed.ok) {
|
|
112
|
+
const recovery = "Fix the input and retry — this will not resolve on retry without an input change.";
|
|
113
|
+
throw new Error(`${parsed.error.message}. ${recovery} Schema for '${toolId}': ${JSON.stringify(entry.inputSchema)}`);
|
|
114
|
+
}
|
|
115
|
+
const value = parsed.value;
|
|
116
|
+
return {
|
|
117
|
+
req(field) {
|
|
118
|
+
const found = value[field];
|
|
119
|
+
if (found === undefined) {
|
|
120
|
+
throw new Error(`identity tool '${toolId}': the handler reads '${field}' as required, but the published inputSchema does not mark it required — add it to that schema's 'required' list, or read it with opt() and handle its absence`);
|
|
121
|
+
}
|
|
122
|
+
return found;
|
|
123
|
+
},
|
|
124
|
+
opt(field) {
|
|
125
|
+
return value[field];
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** The permission set a tool's catalog entry declares — both halves when the gate is an OR. */
|
|
130
|
+
function identityPermissionsFor(toolId) {
|
|
131
|
+
const { authorization } = requireIdentityCatalogEntry(toolId);
|
|
132
|
+
return authorization.orPermission ? [authorization.permission, authorization.orPermission] : [authorization.permission];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The gate for the two READ tools.
|
|
136
|
+
*
|
|
137
|
+
* The mutating tools need no equivalent: their domain function opens with this same
|
|
138
|
+
* `assertCallerHasAnyPermission` call itself. Reads have no service-layer function to inherit it
|
|
139
|
+
* from — `identity` exports grant-writing transitions, not read wrappers, which is why
|
|
140
|
+
* `routes/admin/users/list.ts` also gates in the route and then reads the repo ports directly.
|
|
141
|
+
* This calls the domain's own exported helper rather than re-deriving an OR gate (and rather than
|
|
142
|
+
* the kit's generic `requireToolPermission`, which cannot express an OR), so the single
|
|
143
|
+
* evaluator is reached by an identical path from both tool kinds.
|
|
144
|
+
*/
|
|
145
|
+
async function assertIdentityReadAllowed(routeDeps, toolId, callerPrincipalId) {
|
|
146
|
+
await assertCallerHasAnyPermission({
|
|
147
|
+
deps: identityServiceDepsFrom(routeDeps),
|
|
148
|
+
workspaceId: routeDeps.workspaceId,
|
|
149
|
+
callerPrincipalId,
|
|
150
|
+
permissions: identityPermissionsFor(toolId),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Projects a principal + its credential row into the explicit model-facing shape.
|
|
155
|
+
*
|
|
156
|
+
* The load-bearing omission is `UserRecord.passwordHash`: keeps hashed secrets server-side,
|
|
157
|
+
* and this view has no field for one, so there is nothing to forward by accident even if a future
|
|
158
|
+
* `UserRecord` field is added. `workspaceId` is dropped for the same reason content-types'
|
|
159
|
+
* `toContentTypeView` drops it — the agent is scoped to one workspace it cannot change, so echoing
|
|
160
|
+
* the id spends model attention on a value that can never inform a decision.
|
|
161
|
+
*
|
|
162
|
+
* @param record.roleIds - The principal's role assignments, passed in rather than resolved here so
|
|
163
|
+
* the projection stays pure and the caller decides whether the extra repo read is worth it.
|
|
164
|
+
* @returns The model-facing view. `email` is present only when set, so a user without one carries
|
|
165
|
+
* no always-undefined key.
|
|
166
|
+
* @complexity O(r) in the role count (the array is copied so the caller cannot alias domain state).
|
|
167
|
+
* @overallScore 100
|
|
168
|
+
* See docs/decisions/DR-007-workspace-and-owner-floors.md.
|
|
169
|
+
*/
|
|
170
|
+
function toIdentityUserView(record) {
|
|
171
|
+
const view = {
|
|
172
|
+
principalId: record.principal.id,
|
|
173
|
+
username: record.user.username,
|
|
174
|
+
status: record.principal.status,
|
|
175
|
+
roleIds: [...record.roleIds],
|
|
176
|
+
};
|
|
177
|
+
if (record.user.email)
|
|
178
|
+
view.email = record.user.email;
|
|
179
|
+
return view;
|
|
180
|
+
}
|
|
181
|
+
/** What an identity role tool returns to the model. `isBuiltin` is kept because it is what predicts a rename/delete refusal. */
|
|
182
|
+
function toIdentityRoleView(role) {
|
|
183
|
+
return { id: role.id, name: role.name, isBuiltin: role.isBuiltin };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* What an identity policy tool returns to the model. `isBuiltin`/`isFrozen` are both kept because
|
|
187
|
+
* together they are what predicts an update/delete/write-permission refusal (mirrors
|
|
188
|
+
* `toIdentityRoleView`'s `isBuiltin`, extended for the second immutability flag policies carry that
|
|
189
|
+
* roles do not). `description` is present only when set, matching `toIdentityUserView`'s `email`.
|
|
190
|
+
*/
|
|
191
|
+
function toIdentityPolicyView(policy) {
|
|
192
|
+
const view = {
|
|
193
|
+
id: policy.id,
|
|
194
|
+
name: policy.name,
|
|
195
|
+
isBuiltin: policy.isBuiltin,
|
|
196
|
+
isFrozen: policy.isFrozen,
|
|
197
|
+
};
|
|
198
|
+
if (policy.description)
|
|
199
|
+
view.description = policy.description;
|
|
200
|
+
return view;
|
|
201
|
+
}
|
|
202
|
+
/** Read a principal's role-assignment ids. Shared by every tool that returns a user view. */
|
|
203
|
+
async function roleIdsFor(routeDeps, principalId) {
|
|
204
|
+
const links = await routeDeps.principalRoleRepo.listByPrincipalId({ workspaceId: routeDeps.workspaceId, principalId });
|
|
205
|
+
return links.map((link) => link.roleId);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Load the `UserRecord` paired with a principal the domain just returned.
|
|
209
|
+
*
|
|
210
|
+
* Every `kind='user'` principal has one by construction (CREATE_USER's atomicity guarantee), so
|
|
211
|
+
* the miss is defensive rather than an expected path — the same reasoning `routes/admin/users/
|
|
212
|
+
* disable.ts` records for its identical second lookup.
|
|
213
|
+
*/
|
|
214
|
+
async function requireUserRecord(routeDeps, principalId) {
|
|
215
|
+
const user = await routeDeps.userRepo.findByPrincipalId({ workspaceId: routeDeps.workspaceId, principalId });
|
|
216
|
+
if (!user)
|
|
217
|
+
throw new Error(`user '${principalId}' was not found`);
|
|
218
|
+
return user;
|
|
219
|
+
}
|
|
220
|
+
export function buildIdentityRegistrations(routeDeps) {
|
|
221
|
+
/** Assembled per call, not per handler, because every mutating handler needs the identical bag. */
|
|
222
|
+
const serviceDeps = () => identityServiceDepsFrom(routeDeps);
|
|
223
|
+
const handlers = {
|
|
224
|
+
identity_user_list: async (ctx) => {
|
|
225
|
+
identityInput("identity_user_list", ctx.input);
|
|
226
|
+
await assertIdentityReadAllowed(routeDeps, "identity_user_list", ctx.principal.id);
|
|
227
|
+
const principals = await routeDeps.principalRepo.list({ workspaceId: routeDeps.workspaceId });
|
|
228
|
+
// Humans only — `system`/`agent`/`api_key` principals (including the disabled `user-local`
|
|
229
|
+
// seed row) are not what "users" means on this surface, matching `routes/.../list.ts`.
|
|
230
|
+
const humans = principals.filter((principal) => principal.kind === "user");
|
|
231
|
+
const page = humans.slice(0, IDENTITY_USER_LIST_MAX);
|
|
232
|
+
const users = await Promise.all(page.map(async (principal) => {
|
|
233
|
+
const user = await routeDeps.userRepo.findByPrincipalId({ workspaceId: routeDeps.workspaceId, principalId: principal.id });
|
|
234
|
+
if (!user)
|
|
235
|
+
return null;
|
|
236
|
+
return toIdentityUserView({ principal, user, roleIds: await roleIdsFor(routeDeps, principal.id) });
|
|
237
|
+
}));
|
|
238
|
+
const listed = users.filter((user) => user !== null);
|
|
239
|
+
return humans.length > page.length
|
|
240
|
+
? { users: listed, truncated: true, totalCount: humans.length }
|
|
241
|
+
: { users: listed };
|
|
242
|
+
},
|
|
243
|
+
identity_role_list: async (ctx) => {
|
|
244
|
+
identityInput("identity_role_list", ctx.input);
|
|
245
|
+
await assertIdentityReadAllowed(routeDeps, "identity_role_list", ctx.principal.id);
|
|
246
|
+
const roles = await routeDeps.roleRepo.list({ workspaceId: routeDeps.workspaceId });
|
|
247
|
+
return { roles: roles.map(toIdentityRoleView) };
|
|
248
|
+
},
|
|
249
|
+
identity_policy_list: async (ctx) => {
|
|
250
|
+
identityInput("identity_policy_list", ctx.input);
|
|
251
|
+
await assertIdentityReadAllowed(routeDeps, "identity_policy_list", ctx.principal.id);
|
|
252
|
+
const policies = await routeDeps.policyRepo.list({ workspaceId: routeDeps.workspaceId });
|
|
253
|
+
return { policies: policies.map(toIdentityPolicyView) };
|
|
254
|
+
},
|
|
255
|
+
identity_user_create: async (ctx) => {
|
|
256
|
+
const input = identityInput("identity_user_create", ctx.input);
|
|
257
|
+
const { principal, user } = await createUser({
|
|
258
|
+
deps: serviceDeps(),
|
|
259
|
+
input: {
|
|
260
|
+
workspaceId: routeDeps.workspaceId,
|
|
261
|
+
callerPrincipalId: ctx.principal.id,
|
|
262
|
+
username: input.req("username"),
|
|
263
|
+
password: input.req("password"),
|
|
264
|
+
},
|
|
265
|
+
}, { email: input.opt("email") });
|
|
266
|
+
// A brand-new principal has no assignments yet, so this is [] by construction, not a read.
|
|
267
|
+
return { user: toIdentityUserView({ principal, user, roleIds: [] }) };
|
|
268
|
+
},
|
|
269
|
+
identity_user_update_email: async (ctx) => {
|
|
270
|
+
const input = identityInput("identity_user_update_email", ctx.input);
|
|
271
|
+
const { user } = await updateUser({
|
|
272
|
+
deps: serviceDeps(),
|
|
273
|
+
input: {
|
|
274
|
+
workspaceId: routeDeps.workspaceId,
|
|
275
|
+
callerPrincipalId: ctx.principal.id,
|
|
276
|
+
principalId: input.req("principalId"),
|
|
277
|
+
// Absent stays absent: `updateUser` treats a falsy email as "clear", which is exactly
|
|
278
|
+
// what this tool's description tells the model omitting it does.
|
|
279
|
+
},
|
|
280
|
+
}, { email: input.opt("email") });
|
|
281
|
+
const principal = await routeDeps.principalRepo.findById({ workspaceId: routeDeps.workspaceId, id: input.req("principalId") });
|
|
282
|
+
if (!principal)
|
|
283
|
+
throw new Error(`principal '${input.req("principalId")}' was not found`);
|
|
284
|
+
return { user: toIdentityUserView({ principal, user, roleIds: await roleIdsFor(routeDeps, principal.id) }) };
|
|
285
|
+
},
|
|
286
|
+
identity_user_disable: async (ctx) => {
|
|
287
|
+
const input = identityInput("identity_user_disable", ctx.input);
|
|
288
|
+
const { principal } = await disablePrincipal({
|
|
289
|
+
deps: serviceDeps(),
|
|
290
|
+
input: {
|
|
291
|
+
workspaceId: routeDeps.workspaceId,
|
|
292
|
+
callerPrincipalId: ctx.principal.id,
|
|
293
|
+
principalId: input.req("principalId"),
|
|
294
|
+
// Resolved by the caller, per `disablePrincipal`'s own contract — the same
|
|
295
|
+
// `await deps.ownerPrincipalId` the human disable route performs.
|
|
296
|
+
seededOwnerPrincipalId: await routeDeps.ownerPrincipalId,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
const user = await requireUserRecord(routeDeps, principal.id);
|
|
300
|
+
return { user: toIdentityUserView({ principal, user, roleIds: await roleIdsFor(routeDeps, principal.id) }) };
|
|
301
|
+
},
|
|
302
|
+
identity_user_enable: async (ctx) => {
|
|
303
|
+
const input = identityInput("identity_user_enable", ctx.input);
|
|
304
|
+
const { principal } = await enablePrincipal({
|
|
305
|
+
deps: serviceDeps(),
|
|
306
|
+
input: { workspaceId: routeDeps.workspaceId, callerPrincipalId: ctx.principal.id, principalId: input.req("principalId") },
|
|
307
|
+
});
|
|
308
|
+
const user = await requireUserRecord(routeDeps, principal.id);
|
|
309
|
+
return { user: toIdentityUserView({ principal, user, roleIds: await roleIdsFor(routeDeps, principal.id) }) };
|
|
310
|
+
},
|
|
311
|
+
identity_role_create: async (ctx) => {
|
|
312
|
+
const input = identityInput("identity_role_create", ctx.input);
|
|
313
|
+
const { role } = await createRole({
|
|
314
|
+
deps: serviceDeps(),
|
|
315
|
+
input: { workspaceId: routeDeps.workspaceId, callerPrincipalId: ctx.principal.id, name: input.req("name") },
|
|
316
|
+
});
|
|
317
|
+
return { role: toIdentityRoleView(role) };
|
|
318
|
+
},
|
|
319
|
+
identity_role_assign: async (ctx) => {
|
|
320
|
+
const input = identityInput("identity_role_assign", ctx.input);
|
|
321
|
+
const { assignment } = await assignRole({
|
|
322
|
+
deps: serviceDeps(),
|
|
323
|
+
input: {
|
|
324
|
+
workspaceId: routeDeps.workspaceId,
|
|
325
|
+
callerPrincipalId: ctx.principal.id,
|
|
326
|
+
principalId: input.req("principalId"),
|
|
327
|
+
roleId: input.req("roleId"),
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
// The join row's own id is not addressable by any other tool, so it is dropped; what the
|
|
331
|
+
// model needs back is confirmation of WHICH pair is now linked.
|
|
332
|
+
return { assigned: { principalId: assignment.principalId, roleId: assignment.roleId } };
|
|
333
|
+
},
|
|
334
|
+
identity_role_rename: async (ctx) => {
|
|
335
|
+
const input = identityInput("identity_role_rename", ctx.input);
|
|
336
|
+
const { role } = await updateRole({
|
|
337
|
+
deps: serviceDeps(),
|
|
338
|
+
input: { workspaceId: routeDeps.workspaceId, callerPrincipalId: ctx.principal.id, roleId: input.req("roleId"), name: input.req("name") },
|
|
339
|
+
});
|
|
340
|
+
return { role: toIdentityRoleView(role) };
|
|
341
|
+
},
|
|
342
|
+
identity_role_delete: async (ctx) => {
|
|
343
|
+
const input = identityInput("identity_role_delete", ctx.input);
|
|
344
|
+
await deleteRole({
|
|
345
|
+
deps: serviceDeps(),
|
|
346
|
+
input: { workspaceId: routeDeps.workspaceId, callerPrincipalId: ctx.principal.id, roleId: input.req("roleId") },
|
|
347
|
+
});
|
|
348
|
+
// `deleteRole` resolves void; an empty tool result would read to the model as "nothing
|
|
349
|
+
// happened", so the deleted id is echoed as the acknowledgement.
|
|
350
|
+
return { deleted: { roleId: input.req("roleId") } };
|
|
351
|
+
},
|
|
352
|
+
identity_policy_create: async (ctx) => {
|
|
353
|
+
const input = identityInput("identity_policy_create", ctx.input);
|
|
354
|
+
const { policy } = await createPolicy({
|
|
355
|
+
deps: serviceDeps(),
|
|
356
|
+
input: {
|
|
357
|
+
workspaceId: routeDeps.workspaceId,
|
|
358
|
+
callerPrincipalId: ctx.principal.id,
|
|
359
|
+
name: input.req("name"),
|
|
360
|
+
// Absent stays absent: `input.description` is only present when the caller supplied it
|
|
361
|
+
// (`parseIdentityToolInput` omits absent optional keys entirely), so this passes through
|
|
362
|
+
// unchanged rather than coercing a caller-supplied empty string to "no description".
|
|
363
|
+
},
|
|
364
|
+
}, { description: input.opt("description") });
|
|
365
|
+
return { policy: toIdentityPolicyView(policy) };
|
|
366
|
+
},
|
|
367
|
+
identity_policy_update: async (ctx) => {
|
|
368
|
+
const input = identityInput("identity_policy_update", ctx.input);
|
|
369
|
+
const { policy } = await updatePolicy({
|
|
370
|
+
deps: serviceDeps(),
|
|
371
|
+
input: {
|
|
372
|
+
workspaceId: routeDeps.workspaceId,
|
|
373
|
+
callerPrincipalId: ctx.principal.id,
|
|
374
|
+
policyId: input.req("policyId"),
|
|
375
|
+
// Same absent-stays-absent reasoning as identity_policy_create — `updatePolicy` itself
|
|
376
|
+
// distinguishes undefined ("leave unchanged") from an explicit empty string.
|
|
377
|
+
},
|
|
378
|
+
}, { name: input.opt("name"), description: input.opt("description") });
|
|
379
|
+
return { policy: toIdentityPolicyView(policy) };
|
|
380
|
+
},
|
|
381
|
+
identity_policy_delete: async (ctx) => {
|
|
382
|
+
const input = identityInput("identity_policy_delete", ctx.input);
|
|
383
|
+
await deletePolicy({
|
|
384
|
+
deps: serviceDeps(),
|
|
385
|
+
input: { workspaceId: routeDeps.workspaceId, callerPrincipalId: ctx.principal.id, policyId: input.req("policyId") },
|
|
386
|
+
});
|
|
387
|
+
// `deletePolicy` resolves void; an empty tool result would read to the model as "nothing
|
|
388
|
+
// happened", so the deleted id is echoed as the acknowledgement (mirrors identity_role_delete).
|
|
389
|
+
return { deleted: { policyId: input.req("policyId") } };
|
|
390
|
+
},
|
|
391
|
+
identity_policy_attach: async (ctx) => {
|
|
392
|
+
const input = identityInput("identity_policy_attach", ctx.input);
|
|
393
|
+
const { attachment } = await attachPolicy({
|
|
394
|
+
deps: serviceDeps(),
|
|
395
|
+
input: {
|
|
396
|
+
workspaceId: routeDeps.workspaceId,
|
|
397
|
+
callerPrincipalId: ctx.principal.id,
|
|
398
|
+
principalId: input.req("principalId"),
|
|
399
|
+
policyId: input.req("policyId"),
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
// The join row's own id is not addressable by any other tool, so it is dropped; what the
|
|
403
|
+
// model needs back is confirmation of WHICH pair is now linked (mirrors identity_role_assign).
|
|
404
|
+
return { attached: { principalId: attachment.principalId, policyId: attachment.policyId } };
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
// No `unwiredToolIds`: identity wires its ENTIRE catalog (which is why its own
|
|
408
|
+
// `AgentToolDefinition` makes `inputSchema` required rather than optional).
|
|
409
|
+
return buildDomainRegistrations({
|
|
410
|
+
domain: "identity",
|
|
411
|
+
catalogModule: "./index.js",
|
|
412
|
+
catalog: CATALOG_BY_ID,
|
|
413
|
+
handlers,
|
|
414
|
+
derivedRisk: identityDerivedRisk,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=agent-tool-registrations.js.map
|