@jini-ai/user-management 0.3.1 → 0.3.3
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 +4 -0
- package/dist/core/runtime-ports.d.ts +20 -0
- package/dist/core/types.d.ts +4 -1
- package/dist/core/types.js +5 -1
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +2 -0
- package/dist/react/members/Members.d.ts +1 -1
- package/dist/react/members/Members.js +2 -2
- package/dist/react/members/hooks/use-members.hooks.d.ts +3 -1
- package/dist/react/members/hooks/use-members.hooks.js +69 -69
- package/dist/react/models.d.ts +2 -0
- package/dist/react/ports.d.ts +2 -0
- package/dist/react/testing.js +1 -1
- package/dist/react/users/ChangeUsernameDialog.d.ts +8 -0
- package/dist/react/users/ChangeUsernameDialog.js +8 -0
- package/dist/react/users/Users.d.ts +2 -1
- package/dist/react/users/Users.js +6 -3
- package/dist/react/users/hooks/use-change-username.hooks.d.ts +27 -0
- package/dist/react/users/hooks/use-change-username.hooks.js +53 -0
- package/dist/react/users/hooks/use-users.hooks.d.ts +9 -0
- package/dist/react/users/hooks/use-users.hooks.js +31 -0
- package/dist/react/users/rules.d.ts +1 -0
- package/dist/react/users/rules.js +3 -0
- package/dist/server/admin-crud-service.d.ts +6 -4
- package/dist/server/admin-crud-service.js +49 -6
- package/dist/server/agent-tool-registrations.d.ts +1 -0
- package/dist/server/agent-tool-registrations.js +14 -0
- package/dist/server/agent-tools.js +11 -0
- package/dist/server/auth-service.d.ts +3 -1
- package/dist/server/seed.d.ts +7 -4
- package/dist/server/seed.js +15 -6
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,4 +5,24 @@ export interface SessionTokenPort {
|
|
|
5
5
|
rawToken: string;
|
|
6
6
|
}): string;
|
|
7
7
|
}
|
|
8
|
+
/** Host lifecycle for renames. Audit writes must join the identity transaction. */
|
|
9
|
+
export interface UsernameChangePort {
|
|
10
|
+
/** A host can refuse a rename that its bootstrap identity cannot safely survive. */
|
|
11
|
+
assertSupported?: ((required: {
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
principalId: string;
|
|
14
|
+
}, optional?: Record<string, never>) => Promise<void>) | undefined;
|
|
15
|
+
isInTrash(required: {
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
principalId: string;
|
|
18
|
+
}, optional?: Record<string, never>): Promise<boolean>;
|
|
19
|
+
record(required: {
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
principalId: string;
|
|
22
|
+
callerPrincipalId: string;
|
|
23
|
+
previousUsername: string;
|
|
24
|
+
username: string;
|
|
25
|
+
occurredAt: string;
|
|
26
|
+
}, optional?: Record<string, never>): Promise<void>;
|
|
27
|
+
}
|
|
8
28
|
//# sourceMappingURL=runtime-ports.d.ts.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -156,8 +156,11 @@ export declare class IdentityNotFoundError extends Error {
|
|
|
156
156
|
* See docs/decisions/DR-001-identity-and-session-boundary.md.
|
|
157
157
|
*/
|
|
158
158
|
export declare class IdentityConflictError extends Error {
|
|
159
|
-
|
|
159
|
+
/** Rename conflicts carry a precise reason; legacy conflicts retain their existing contract. */
|
|
160
|
+
readonly code?: "RESOURCE_CONFLICT" | "USER_IN_TRASH" | "USERNAME_IN_TRASH";
|
|
161
|
+
constructor({ message, code }: {
|
|
160
162
|
message: string;
|
|
163
|
+
code?: "RESOURCE_CONFLICT" | "USER_IN_TRASH" | "USERNAME_IN_TRASH";
|
|
161
164
|
}, optional?: ErrorOptions);
|
|
162
165
|
}
|
|
163
166
|
/** Raised by `login` on bad credentials, a disabled principal, or a blank field. See docs/decisions/DR-001-identity-and-session-boundary.md. */
|
package/dist/core/types.js
CHANGED
|
@@ -18,8 +18,12 @@ export class IdentityNotFoundError extends Error {
|
|
|
18
18
|
* See docs/decisions/DR-001-identity-and-session-boundary.md.
|
|
19
19
|
*/
|
|
20
20
|
export class IdentityConflictError extends Error {
|
|
21
|
-
|
|
21
|
+
/** Rename conflicts carry a precise reason; legacy conflicts retain their existing contract. */
|
|
22
|
+
code;
|
|
23
|
+
constructor({ message, code }, optional = {}) {
|
|
22
24
|
super(message, optional);
|
|
25
|
+
if (code !== undefined)
|
|
26
|
+
this.code = code;
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
/** Raised by `login` on bad credentials, a disabled principal, or a blank field. See docs/decisions/DR-001-identity-and-session-boundary.md. */
|
package/dist/react/index.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type { MembersProps } from './members/Members.js';
|
|
|
15
15
|
export { useMembers } from './members/hooks/use-members.hooks.js';
|
|
16
16
|
export type { MembersController, MembersDependencies, MembersOptions } from './members/hooks/use-members.hooks.js';
|
|
17
17
|
export { describeIdentityError } from './errors.js';
|
|
18
|
+
export { useChangeUsername } from './users/hooks/use-change-username.hooks.js';
|
|
19
|
+
export type { ChangeUsernameController } from './users/hooks/use-change-username.hooks.js';
|
|
20
|
+
export { ChangeUsernameDialog } from './users/ChangeUsernameDialog.js';
|
|
18
21
|
export { userAccountCapabilities, isOwnerAccount, grantOptionLabel, formatGrantLabel, userRowMenuItems, describeUsersError } from './users/rules.js';
|
|
19
22
|
export type { UserAccountCapabilities, UserRowMenuHandlers } from './users/rules.js';
|
|
20
23
|
export { emptyRowState, memberRowMenuItems, describeMembersError } from './members/rules.js';
|
package/dist/react/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export { useResetPasswordFields } from './users/hooks/use-reset-password-fields.
|
|
|
6
6
|
export { Members } from './members/Members.js';
|
|
7
7
|
export { useMembers } from './members/hooks/use-members.hooks.js';
|
|
8
8
|
export { describeIdentityError } from './errors.js';
|
|
9
|
+
export { useChangeUsername } from './users/hooks/use-change-username.hooks.js';
|
|
10
|
+
export { ChangeUsernameDialog } from './users/ChangeUsernameDialog.js';
|
|
9
11
|
export { userAccountCapabilities, isOwnerAccount, grantOptionLabel, formatGrantLabel, userRowMenuItems, describeUsersError } from './users/rules.js';
|
|
10
12
|
export { emptyRowState, memberRowMenuItems, describeMembersError } from './members/rules.js';
|
|
11
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -29,5 +29,5 @@ export interface MembersProps extends MembersDependencies, MembersOptions {
|
|
|
29
29
|
value: string;
|
|
30
30
|
}) => ReactNode) | undefined;
|
|
31
31
|
}
|
|
32
|
-
export declare function Members({ useMembersHook, port, translate: injectedTranslate, refresh, description, emptyDescription, renderServerLabel }: MembersProps): import("react").JSX.Element;
|
|
32
|
+
export declare function Members({ useMembersHook, port, translate: injectedTranslate, queryScope, refresh, description, emptyDescription, renderServerLabel }: MembersProps): import("react").JSX.Element;
|
|
33
33
|
//# sourceMappingURL=Members.d.ts.map
|
|
@@ -28,8 +28,8 @@ function MemberRow({ member, rowState, isExpanded, detail, detailError, detailLo
|
|
|
28
28
|
onRequestDisable: setConfirmingDisable,
|
|
29
29
|
}, translate }) }), rowState.error ? (_jsx("div", { className: "notice error", role: "alert", children: rowState.error })) : null, rowState.notice ? _jsx("div", { className: "notice", children: rowState.notice }) : null] })] }), isExpanded ? (_jsx("tr", { children: _jsx("td", { colSpan: 5, children: _jsx(MemberDetailPanel, { memberId: member.id, detailLoadingId: detailLoadingId, detailError: detailError, detail: detail, t: t }) }) })) : null] }, member.id));
|
|
30
30
|
}
|
|
31
|
-
export function Members({ useMembersHook = useMembers, port, translate: injectedTranslate, refresh, description = "People who have registered an account — review status, resend a sign-in link, or disable access.", emptyDescription = "Registered members will show up here.", renderServerLabel }) {
|
|
32
|
-
const { members, error, stateFor, onResendSignInLink, expandedId, detailById, detailError, detailLoadingId, onToggleDetail, confirmingDisable, setConfirmingDisable, confirmDisable, t, translate, } = useMembersHook({ port, translate: injectedTranslate }, { refresh });
|
|
31
|
+
export function Members({ useMembersHook = useMembers, port, translate: injectedTranslate, queryScope, refresh, description = "People who have registered an account — review status, resend a sign-in link, or disable access.", emptyDescription = "Registered members will show up here.", renderServerLabel }) {
|
|
32
|
+
const { members, error, stateFor, onResendSignInLink, expandedId, detailById, detailError, detailLoadingId, onToggleDetail, confirmingDisable, setConfirmingDisable, confirmDisable, t, translate, } = useMembersHook({ port, translate: injectedTranslate, ...(queryScope === undefined ? {} : { queryScope }) }, { refresh });
|
|
33
33
|
if (error)
|
|
34
34
|
return _jsx("div", { className: "notice error", children: error });
|
|
35
35
|
if (!members)
|
|
@@ -34,6 +34,8 @@ import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
|
34
34
|
*/
|
|
35
35
|
export interface MembersDependencies {
|
|
36
36
|
port: MembersPort;
|
|
37
|
+
/** Same session/workspace scope as Users; isolated providers retain legacy composition. */
|
|
38
|
+
queryScope?: string;
|
|
37
39
|
translate: Translate;
|
|
38
40
|
}
|
|
39
41
|
export interface MembersController {
|
|
@@ -70,5 +72,5 @@ export interface MembersController {
|
|
|
70
72
|
export interface MembersOptions {
|
|
71
73
|
refresh?: IdentityRefreshPort | undefined;
|
|
72
74
|
}
|
|
73
|
-
export declare function useMembers({ port, translate }: MembersDependencies, { refresh }?: MembersOptions): MembersController;
|
|
75
|
+
export declare function useMembers({ port, translate, queryScope }: MembersDependencies, { refresh }?: MembersOptions): MembersController;
|
|
74
76
|
//# sourceMappingURL=use-members.hooks.d.ts.map
|
|
@@ -1,60 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { describeApiError, emptyRowState } from "../rules.js";
|
|
3
|
-
import {
|
|
4
|
-
export function useMembers({ port, translate }, { refresh } = {}) {
|
|
3
|
+
import { useCachedLoader, useFetchMutation, useFetchQuery, useInvalidate } from "@jini-ai/ui/fetch-query";
|
|
4
|
+
export function useMembers({ port, translate, queryScope = "members" }, { refresh } = {}) {
|
|
5
|
+
const scopeRef = useRef(queryScope);
|
|
6
|
+
scopeRef.current = queryScope;
|
|
5
7
|
const boundT = translate;
|
|
6
8
|
const describeError = (error, fallback) => describeApiError({ error, fallback, translate });
|
|
7
|
-
const [members,
|
|
8
|
-
const [
|
|
9
|
+
const listKey = [queryScope, 'members', 'list'];
|
|
10
|
+
const detailRoot = [queryScope, 'members', 'detail'];
|
|
11
|
+
const invalidate = useInvalidate();
|
|
12
|
+
// Mount reads and assistant refreshes can overlap; the shared cache owns list settlement
|
|
13
|
+
// order so a slow earlier success or failure cannot replace a newer roster.
|
|
14
|
+
const list = useFetchQuery({ key: listKey, fetch: () => port.listMembers({}) });
|
|
15
|
+
const members = list.data?.members ?? null;
|
|
16
|
+
const error = list.error ? describeError(list.error, translate("failed to load members")) : null;
|
|
17
|
+
const listLoader = useCachedLoader({ key: listKey, fetch: () => port.listMembers({}) }, { staleTime: 0 });
|
|
18
|
+
const disable = useFetchMutation({ run: ({ input: id }) => port.disableMember({ id }) }, { invalidates: [listKey, detailRoot] });
|
|
19
|
+
// Sending a link changes no list/detail fields; do not refetch the roster on success.
|
|
20
|
+
const resend = useFetchMutation({ run: ({ input: email }) => port.requestMemberMagicLink({ email }) });
|
|
9
21
|
const [rowState, setRowState] = useState({});
|
|
10
22
|
const [expandedId, setExpandedId] = useState(null);
|
|
11
23
|
const [detailById, setDetailById] = useState({});
|
|
12
|
-
const [detailError, setDetailError] = useState(null);
|
|
13
|
-
const [detailLoadingId, setDetailLoadingId] = useState(null);
|
|
14
24
|
// Disable now confirms via a `RowMenu` item -> `ConfirmDialog` modal (replacing the in-place
|
|
15
25
|
// two-click `ConfirmButton`, which has no menu-item equivalent — same migration Posts.tsx/
|
|
16
26
|
// Redirects.tsx/Users.tsx already made). `null` when the dialog is closed.
|
|
17
27
|
const [confirmingDisable, setConfirmingDisable] = useState(null);
|
|
18
|
-
const settlement = useSettlementGeneration();
|
|
19
28
|
// Which row's detail panel is actually open (C7, plan-access.md §8, N2) — a ref, not just
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// expanding row A (slow, then failing), then row B, lets A's `finally` blank B's loading indicator
|
|
23
|
-
// and lets A's failure paint onto B's now-open panel — the same "entity-scoped panel, unkeyed
|
|
24
|
-
// settle" shape `use-roles.hooks.ts`'s `permissionPolicyIdRef` fixes for `loadPermissions`.
|
|
29
|
+
// expandedId state, because onToggleDetail reads it after await, when a closure can be stale.
|
|
30
|
+
// Expanding slow/failing A then B must not let A's failure or finally blank B's panel/spinner.
|
|
25
31
|
const expandedIdRef = useRef(null);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const generation = settlement.next();
|
|
36
|
-
port
|
|
37
|
-
.listMembers({})
|
|
38
|
-
.then((r) => {
|
|
39
|
-
if (!settlement.isCurrent({ generation }))
|
|
40
|
-
return;
|
|
41
|
-
setMembers(r.members);
|
|
42
|
-
setError(null);
|
|
43
|
-
})
|
|
44
|
-
.catch((e) => {
|
|
45
|
-
if (!settlement.isCurrent({ generation }))
|
|
46
|
-
return;
|
|
47
|
-
setError(e instanceof Error ? e.message : translate("failed to load members"));
|
|
48
|
-
});
|
|
49
|
-
// `port`/`locale`/`settlement` are added — see `use-page-editor.hooks.ts`'s identical note:
|
|
50
|
-
// function-scoped values ESLint's exhaustive-deps rule can see, referentially stable in
|
|
51
|
-
// production, so this changes nothing about when this callback's identity changes.
|
|
52
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
-
}, [port, settlement, translate]);
|
|
32
|
+
const detailGenerations = useRef({});
|
|
33
|
+
const [detailGeneration, setDetailGeneration] = useState(0);
|
|
34
|
+
// Entity keys own settlement: a late A response cannot paint B's error/spinner. Detail
|
|
35
|
+
// data remains cached on collapse, while successful disables invalidate both resources.
|
|
36
|
+
const detail = useFetchQuery({
|
|
37
|
+
key: [...detailRoot, expandedId ?? '', detailGeneration],
|
|
38
|
+
fetch: () => port.getMember({ id: expandedId }),
|
|
39
|
+
}, { enabled: expandedId !== null, staleTime: Infinity });
|
|
40
|
+
const detailLoader = useCachedLoader({ key: [...detailRoot, expandedId ?? '', detailGeneration], fetch: () => port.getMember({ id: expandedId }) }, { staleTime: Infinity });
|
|
54
41
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
expandedIdRef.current = null;
|
|
43
|
+
detailGenerations.current = {};
|
|
44
|
+
setExpandedId(null);
|
|
45
|
+
setDetailGeneration(0);
|
|
46
|
+
setDetailById({});
|
|
47
|
+
setRowState({});
|
|
48
|
+
setConfirmingDisable(null);
|
|
49
|
+
}, [queryScope]);
|
|
50
|
+
// Only paint the failure onto the panel that is still expanded: the observer's entity key
|
|
51
|
+
// selects the current row, so a late rejection for a collapsed row cannot surface here.
|
|
52
|
+
const detailError = detail.error ? describeError(detail.error, translate("Failed to load member detail.")) : null;
|
|
53
|
+
// A superseded load leaves the spinner to its successor. Derive it from the current keyed
|
|
54
|
+
// observer instead of letting an old request's finally clear the new row's loading state.
|
|
55
|
+
const detailLoadingId = expandedId !== null && !detail.data && detail.isFetching ? expandedId : null;
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (expandedId && detail.data)
|
|
58
|
+
setDetailById(current => ({ ...current, [expandedId]: detail.data.member }));
|
|
59
|
+
}, [expandedId, detail.data]);
|
|
60
|
+
useEffect(() => refresh?.subscribe({ onRefresh: () => {
|
|
61
|
+
invalidate({ key: listKey });
|
|
62
|
+
invalidate({ key: detailRoot });
|
|
63
|
+
} }), [refresh, invalidate, queryScope]);
|
|
58
64
|
function stateFor({ id }) {
|
|
59
65
|
return rowState[id] ?? emptyRowState({});
|
|
60
66
|
}
|
|
@@ -66,8 +72,9 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
66
72
|
return;
|
|
67
73
|
patchRowState(member.id, { disabling: true, error: null, notice: null });
|
|
68
74
|
try {
|
|
69
|
-
const result = await
|
|
70
|
-
|
|
75
|
+
const result = await disable.mutate({ input: member.id });
|
|
76
|
+
await listLoader.load().catch(() => { }); // Await roster settlement; list.error owns a failed read, not the accepted disable.
|
|
77
|
+
setDetailById(current => current[member.id] ? { ...current, [member.id]: result.member } : current);
|
|
71
78
|
patchRowState(member.id, { disabling: false, notice: translate("Member disabled.") });
|
|
72
79
|
}
|
|
73
80
|
catch (e) {
|
|
@@ -79,7 +86,7 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
79
86
|
return;
|
|
80
87
|
patchRowState(member.id, { resending: true, error: null, notice: null });
|
|
81
88
|
try {
|
|
82
|
-
await
|
|
89
|
+
await resend.mutate({ input: member.email });
|
|
83
90
|
patchRowState(member.id, { resending: false, notice: translate("Sign-in link sent.") });
|
|
84
91
|
}
|
|
85
92
|
catch (e) {
|
|
@@ -103,30 +110,23 @@ export function useMembers({ port, translate }, { refresh } = {}) {
|
|
|
103
110
|
}
|
|
104
111
|
expandedIdRef.current = member.id;
|
|
105
112
|
setExpandedId(member.id);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
// A collapsed unfinished request cannot own a reopened panel. A primitive generation
|
|
114
|
+
// creates a new read fence for that member, preserving the prior latest-wins contract;
|
|
115
|
+
// successful details keep their current generation so reopen reuses the cache.
|
|
116
|
+
// Claimed before await: even two loads of the SAME row must be distinguishable, since a
|
|
117
|
+
// row-id key alone cannot fence an unfinished request after collapse/reopen.
|
|
118
|
+
const generation = detailById[member.id] ? detailGenerations.current[member.id] ?? 1 : (detailGenerations.current[member.id] ?? 0) + 1;
|
|
119
|
+
detailGenerations.current[member.id] = generation;
|
|
120
|
+
setDetailGeneration(generation);
|
|
121
|
+
// Start through the same cache synchronously so imperative callers still await the
|
|
122
|
+
// detail read; the reactive observer joins it when React commits the selected identity.
|
|
123
|
+
const requestedScope = queryScope;
|
|
114
124
|
try {
|
|
115
|
-
const result = await port.getMember({ id: member.id });
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
catch (e) {
|
|
119
|
-
// Only paint the failure onto the panel this load was actually for — a different row may
|
|
120
|
-
// already be open by the time this settles, or a newer load of this row may be running.
|
|
121
|
-
if (detailSettlement.isCurrent({ generation }) && expandedIdRef.current === member.id) {
|
|
122
|
-
setDetailError(describeError(e, translate("Failed to load member detail.")));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
// A superseded load leaves the spinner to the load that superseded it.
|
|
127
|
-
if (detailSettlement.isCurrent({ generation }))
|
|
128
|
-
setDetailLoadingId(null);
|
|
125
|
+
const result = await detailLoader.load({ key: [...detailRoot, member.id, generation], fetch: () => port.getMember({ id: member.id }) });
|
|
126
|
+
if (scopeRef.current === requestedScope && detailGenerations.current[member.id] === generation)
|
|
127
|
+
setDetailById(current => ({ ...current, [member.id]: result.member }));
|
|
129
128
|
}
|
|
129
|
+
catch { /* The keyed observer owns the failure for the panel that is still expanded. */ }
|
|
130
130
|
}
|
|
131
131
|
return {
|
|
132
132
|
members,
|
package/dist/react/models.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface AdminIdentityUser {
|
|
|
14
14
|
/** Server classification covers ownership granted by policy, not only named roles. */
|
|
15
15
|
isOwner?: boolean | undefined;
|
|
16
16
|
isProtectedAccount?: boolean | undefined;
|
|
17
|
+
/** Host refusal for accounts whose bootstrap identity cannot safely follow a rename yet. */
|
|
18
|
+
canRename?: boolean | undefined;
|
|
17
19
|
roleIds: string[];
|
|
18
20
|
policyIds: string[];
|
|
19
21
|
}
|
package/dist/react/ports.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface UsersPort {
|
|
|
16
16
|
id: string;
|
|
17
17
|
};
|
|
18
18
|
canManageUserTrash: boolean;
|
|
19
|
+
effectivePermissions?: string[] | undefined;
|
|
19
20
|
}>;
|
|
20
21
|
listRoles(required: Record<string, never>): Promise<{
|
|
21
22
|
roles: AdminRole[];
|
|
@@ -35,6 +36,7 @@ export interface UsersPort {
|
|
|
35
36
|
principalId: string;
|
|
36
37
|
}, options?: {
|
|
37
38
|
email?: string | undefined;
|
|
39
|
+
username?: string | undefined;
|
|
38
40
|
}): Promise<{
|
|
39
41
|
user: AdminIdentityUser;
|
|
40
42
|
}>;
|
package/dist/react/testing.js
CHANGED
|
@@ -67,7 +67,7 @@ export function createFakeUsersPort(_required, options = {}) {
|
|
|
67
67
|
if (options.updateUserError)
|
|
68
68
|
throw options.updateUserError;
|
|
69
69
|
const current = requireUser(target.principalId);
|
|
70
|
-
return { user: replaceUser({ ...current, ...opts }) };
|
|
70
|
+
return { user: replaceUser({ ...current, ...opts, username: opts?.username ?? current.username }) };
|
|
71
71
|
},
|
|
72
72
|
async disableUser({ principalId }) {
|
|
73
73
|
if (options.toggleStatusError)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
2
|
+
import type { useChangeUsername } from "./hooks/use-change-username.hooks.js";
|
|
3
|
+
/** Markup shared by row actions and the signed-in user's profile; state lives in the hook. */
|
|
4
|
+
export declare function ChangeUsernameDialog({ controller, translate: t }: {
|
|
5
|
+
controller: ReturnType<typeof useChangeUsername>;
|
|
6
|
+
translate: Translate;
|
|
7
|
+
}, _optional?: Record<string, never>): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=ChangeUsernameDialog.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ConfirmDialog } from "@jini-ai/admin/react";
|
|
3
|
+
import { agentHandle } from "@jini-ai/agentic";
|
|
4
|
+
/** Markup shared by row actions and the signed-in user's profile; state lives in the hook. */
|
|
5
|
+
export function ChangeUsernameDialog({ controller, translate: t }, _optional = {}) {
|
|
6
|
+
return _jsx(ConfirmDialog, { open: controller.target !== null, agentHandle: "users-change-username", title: t("Change username"), cancelLabel: t("Cancel"), confirmLabel: t("Change username"), pending: controller.pending, onConfirm: () => void controller.confirm({}), onCancel: () => controller.cancel({}), body: _jsxs(_Fragment, { children: [_jsx("p", { children: controller.target?.username }), _jsxs("label", { children: [t("New username"), _jsx("input", { value: controller.username, autoComplete: "off", disabled: controller.pending, onChange: event => controller.setUsername(event.target.value), ...agentHandle({ handle: "users-change-username-input" }, { role: "field", label: "New login username" }) })] }), _jsx("p", { children: t("Existing sessions stay signed in. Use the new username the next time you sign in.") }), controller.error ? _jsx("p", { role: "alert", className: "save-error", children: controller.error }) : null] }) });
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=ChangeUsernameDialog.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Dispatch, type SetStateAction } from "react";
|
|
2
2
|
import type { AdminIdentityUser, AdminPolicy, AdminRole } from "../models.js";
|
|
3
3
|
import { type UserAccountCapabilities } from "./rules.js";
|
|
4
|
-
import { useUsers, type UsersDependencies, type UsersOptions } from "./hooks/use-users.hooks.js";
|
|
4
|
+
import { useUsers, type UsersController, type UsersDependencies, type UsersOptions } from "./hooks/use-users.hooks.js";
|
|
5
5
|
import type { ReactNode } from "react";
|
|
6
6
|
import type { Translate } from "@jini-ai/ui/panel-kit";
|
|
7
7
|
/**
|
|
@@ -130,6 +130,7 @@ export interface UserRowActionsController {
|
|
|
130
130
|
requestDisable: (user: AdminIdentityUser) => void;
|
|
131
131
|
toggleStatus: (user: AdminIdentityUser) => Promise<void>;
|
|
132
132
|
openResetPassword: (user: AdminIdentityUser) => void;
|
|
133
|
+
renameUserHandler?: UsersController["renameUserHandler"];
|
|
133
134
|
/** Delete-user plan v2 (2026-09-24): whether the row's `RowMenu` should carry a "Delete" item at
|
|
134
135
|
* all — see `use-users.hooks.ts`'s `canManageUserTrash` doc comment for where this comes from. */
|
|
135
136
|
canDelete: boolean;
|
|
@@ -7,6 +7,7 @@ import { formatGrantLabel, grantOptionLabel, userRowMenuItems, isOwnerAccount }
|
|
|
7
7
|
import { useUsers } from "./hooks/use-users.hooks.js";
|
|
8
8
|
import { InfoTip } from "@jini-ai/ui/admin-widgets";
|
|
9
9
|
import { useResetPasswordFields } from "./hooks/use-reset-password-fields.hooks.js";
|
|
10
|
+
import { ChangeUsernameDialog } from "./ChangeUsernameDialog.js";
|
|
10
11
|
/** The "New user" form — extracted from `Users` verbatim; own scope for `formError`/`saving`. */
|
|
11
12
|
function NewUserForm({ username, setUsername, email, setEmail, password, setPassword, saving, formError, onCreate, t }) {
|
|
12
13
|
return (_jsxs("form", { onSubmit: onCreate, className: "notice integrations-form form-measure", children: [formError ? _jsx("span", { className: "save-error", children: formError }) : null, _jsxs("label", { children: [t("Username"), _jsx("input", { value: username, onChange: (e) => setUsername(e.target.value), required: true, ...agentHandle({ handle: "users-new-username" }, { role: "field", label: "The new user's username" }) })] }), _jsxs("label", { children: [t("Email (optional)"), _jsx("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), ...agentHandle({ handle: "users-new-email" }, { role: "field", label: "The new user's email address" }) })] }), _jsxs("label", { children: [t("Password"), _jsx("input", { type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), required: true, minLength: 1, ...agentHandle({ handle: "users-new-password" }, {
|
|
@@ -43,6 +44,7 @@ function UserRow({ user, roleById, policyById, showGrants, actions, manage, agen
|
|
|
43
44
|
onEnable: (u) => void actions.toggleStatus(u),
|
|
44
45
|
onManage: actions.toggleExpanded,
|
|
45
46
|
onResetPassword: actions.openResetPassword,
|
|
47
|
+
onChangeUsername: actions.renameUserHandler?.({ user }),
|
|
46
48
|
onRequestDelete: actions.requestDelete,
|
|
47
49
|
}, translate, canDelete: actions.canDelete,
|
|
48
50
|
capabilities: actions.rowCapabilities?.(user) ?? actions.canDelete,
|
|
@@ -159,20 +161,21 @@ function UsersNotices({ toggleError, notice }) {
|
|
|
159
161
|
return (_jsxs(_Fragment, { children: [toggleError ? _jsx("div", { className: "notice error", children: toggleError }) : null, notice ? _jsx("div", { className: "notice", children: notice }) : null] }));
|
|
160
162
|
}
|
|
161
163
|
export function Users({ useUsersHook = useUsers, port, translate: injectedTranslate, queryScope, openOwnPasswordReset, onOwnPasswordResetClosed, passwordResetNotice, renderServerLabel }) {
|
|
162
|
-
const { users, roles, policies, canGrant, error, formOpen, setFormOpen, username, setUsername, email, setEmail, password, setPassword, saving, formError, onCreate, expandedId, toggleExpanded, pendingRoleId, setPendingRoleId, pendingPolicyId, setPendingPolicyId, grantSaving, grantError, onAssignRole, onAttachPolicy, editEmail, setEditEmail, emailSaving, onSaveEmail, toggleSavingId, toggleError, notice, confirmingDisable, setConfirmingDisable, requestDisable, confirmDisable, onToggleStatus, resetPasswordFor, setResetPasswordFor, newPassword, setNewPassword, passwordSaving, passwordError, setPasswordError, openResetPassword, confirmResetPassword, canManageUserTrash, rowCapabilities, confirmingDelete, setConfirmingDelete, requestDelete, deleteSaving, confirmDelete, t, translate, } = useUsersHook({ port, translate: injectedTranslate, queryScope }, {
|
|
164
|
+
const { users, roles, policies, canGrant, error, formOpen, setFormOpen, username, setUsername, email, setEmail, password, setPassword, saving, formError, onCreate, expandedId, toggleExpanded, pendingRoleId, setPendingRoleId, pendingPolicyId, setPendingPolicyId, grantSaving, grantError, onAssignRole, onAttachPolicy, editEmail, setEditEmail, emailSaving, onSaveEmail, toggleSavingId, toggleError, notice, confirmingDisable, setConfirmingDisable, requestDisable, confirmDisable, onToggleStatus, resetPasswordFor, setResetPasswordFor, newPassword, setNewPassword, passwordSaving, passwordError, setPasswordError, openResetPassword, confirmResetPassword, canManageUserTrash, rowCapabilities, ownProfile, canRenameOwnProfile, openOwnRename, renameUserHandler, changeUsername, confirmingDelete, setConfirmingDelete, requestDelete, deleteSaving, confirmDelete, t, translate, } = useUsersHook({ port, translate: injectedTranslate, queryScope }, {
|
|
163
165
|
openOwnPasswordReset, onOwnPasswordResetClosed, passwordResetNotice,
|
|
164
166
|
});
|
|
165
167
|
if (error)
|
|
166
168
|
return _jsx("div", { className: "notice error", children: error });
|
|
167
169
|
if (!users || !roles || !policies)
|
|
168
170
|
return _jsx("div", { className: "notice", children: t("Loading users…") });
|
|
169
|
-
return (_jsxs("div", { className: "page", children: [_jsx(UsersPageHeader, { formOpen: formOpen, setFormOpen: setFormOpen, t: t }), formOpen ? (_jsx(NewUserForm, { username: username, setUsername: setUsername, email: email, setEmail: setEmail, password: password, setPassword: setPassword, saving: saving, formError: formError, onCreate: onCreate, t: t })) : null, _jsx(UsersNotices, { toggleError: toggleError, notice: notice }), _jsx(UsersTable, { users: users, roles: roles, policies: policies, showGrants: canGrant, actions: {
|
|
171
|
+
return (_jsxs("div", { className: "page", children: [_jsx(UsersPageHeader, { formOpen: formOpen, setFormOpen: setFormOpen, t: t }), ownProfile ? _jsxs("section", { className: "notice", "aria-label": t("My profile"), children: [_jsx("h2", { children: t("My profile") }), _jsx("p", { children: ownProfile.username }), canRenameOwnProfile ? _jsx("button", { type: "button", onClick: openOwnRename, ...agentHandle({ handle: "users-profile-change-username" }, { role: "button", label: "Change my login username" }), children: t("Change username") }) : null] }) : null, formOpen ? (_jsx(NewUserForm, { username: username, setUsername: setUsername, email: email, setEmail: setEmail, password: password, setPassword: setPassword, saving: saving, formError: formError, onCreate: onCreate, t: t })) : null, _jsx(UsersNotices, { toggleError: toggleError, notice: notice }), _jsx(UsersTable, { users: users, roles: roles, policies: policies, showGrants: canGrant, actions: {
|
|
170
172
|
expandedId,
|
|
171
173
|
toggleExpanded,
|
|
172
174
|
savingId: toggleSavingId,
|
|
173
175
|
requestDisable,
|
|
174
176
|
toggleStatus: onToggleStatus,
|
|
175
177
|
openResetPassword,
|
|
178
|
+
renameUserHandler,
|
|
176
179
|
canDelete: canManageUserTrash,
|
|
177
180
|
rowCapabilities,
|
|
178
181
|
requestDelete,
|
|
@@ -186,6 +189,6 @@ export function Users({ useUsersHook = useUsers, port, translate: injectedTransl
|
|
|
186
189
|
policyGrant: canGrant
|
|
187
190
|
? { options: policies, pendingId: pendingPolicyId, setPendingId: setPendingPolicyId, submit: onAttachPolicy }
|
|
188
191
|
: null,
|
|
189
|
-
}, t: t, translate: translate, renderServerLabel: renderServerLabel }), _jsx(UserDisableDialog, { confirmingDisable: confirmingDisable, setConfirmingDisable: setConfirmingDisable, toggleSavingId: toggleSavingId, confirmDisable: confirmDisable, t: t }), _jsx(UserDeleteDialog, { confirmingDelete: confirmingDelete, setConfirmingDelete: setConfirmingDelete, deleteSaving: deleteSaving, confirmDelete: confirmDelete, t: t }), _jsx(UserResetPasswordDialog, { resetPasswordFor: resetPasswordFor, setResetPasswordFor: setResetPasswordFor, newPassword: newPassword, setNewPassword: setNewPassword, passwordError: passwordError, setPasswordError: setPasswordError, passwordSaving: passwordSaving, confirmResetPassword: confirmResetPassword, t: t })] }));
|
|
192
|
+
}, t: t, translate: translate, renderServerLabel: renderServerLabel }), _jsx(UserDisableDialog, { confirmingDisable: confirmingDisable, setConfirmingDisable: setConfirmingDisable, toggleSavingId: toggleSavingId, confirmDisable: confirmDisable, t: t }), _jsx(UserDeleteDialog, { confirmingDelete: confirmingDelete, setConfirmingDelete: setConfirmingDelete, deleteSaving: deleteSaving, confirmDelete: confirmDelete, t: t }), _jsx(UserResetPasswordDialog, { resetPasswordFor: resetPasswordFor, setResetPasswordFor: setResetPasswordFor, newPassword: newPassword, setNewPassword: setNewPassword, passwordError: passwordError, setPasswordError: setPasswordError, passwordSaving: passwordSaving, confirmResetPassword: confirmResetPassword, t: t }), changeUsername ? _jsx(ChangeUsernameDialog, { controller: changeUsername, translate: t }) : null] }));
|
|
190
193
|
}
|
|
191
194
|
//# sourceMappingURL=Users.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
import { type Translate } from "@jini-ai/ui/panel-kit";
|
|
3
|
+
import type { AdminIdentityUser } from "../../models.js";
|
|
4
|
+
import type { UsersPort } from "../../ports.js";
|
|
5
|
+
export interface ChangeUsernameController {
|
|
6
|
+
target: AdminIdentityUser | null;
|
|
7
|
+
username: string;
|
|
8
|
+
setUsername: Dispatch<SetStateAction<string>>;
|
|
9
|
+
error: string | null;
|
|
10
|
+
pending: boolean;
|
|
11
|
+
open: (required: {
|
|
12
|
+
user: AdminIdentityUser;
|
|
13
|
+
}, optional?: Record<string, never>) => void;
|
|
14
|
+
cancel: (required?: Record<string, never>, optional?: Record<string, never>) => void;
|
|
15
|
+
confirm: (required?: Record<string, never>, optional?: Record<string, never>) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Shared rename dialog lifecycle: guard, write through the existing user port, invalidate list.
|
|
18
|
+
* Failures retain the target/draft and the server's actual reason; duplicate submits are refused.
|
|
19
|
+
* @complexity O(1) client state and one PATCH per submission. No credential/session effects.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useChangeUsername(required: {
|
|
22
|
+
port: Pick<UsersPort, "updateUser">;
|
|
23
|
+
queryScope: string;
|
|
24
|
+
translate: Translate;
|
|
25
|
+
canRename: (user: AdminIdentityUser) => boolean;
|
|
26
|
+
}, _optional?: Record<string, never>): ChangeUsernameController;
|
|
27
|
+
//# sourceMappingURL=use-change-username.hooks.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { useFetchMutation } from "@jini-ai/ui/panel-kit";
|
|
3
|
+
import { describeApiError, KEYS } from "../rules.js";
|
|
4
|
+
/** Shared rename dialog lifecycle: guard, write through the existing user port, invalidate list.
|
|
5
|
+
* Failures retain the target/draft and the server's actual reason; duplicate submits are refused.
|
|
6
|
+
* @complexity O(1) client state and one PATCH per submission. No credential/session effects.
|
|
7
|
+
*/
|
|
8
|
+
export function useChangeUsername(required, _optional = {}) {
|
|
9
|
+
const [target, setTarget] = useState(null);
|
|
10
|
+
const [username, setUsername] = useState("");
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const busy = useRef(false);
|
|
13
|
+
const mutation = useFetchMutation({
|
|
14
|
+
run: ({ input }) => required.port.updateUser({ principalId: input.principalId }, { username: input.username }),
|
|
15
|
+
}, { invalidates: [[required.queryScope, ...KEYS.list]] });
|
|
16
|
+
function open({ user }, _options = {}) {
|
|
17
|
+
if (busy.current || !required.canRename(user))
|
|
18
|
+
return;
|
|
19
|
+
setTarget(user);
|
|
20
|
+
setUsername(user.username);
|
|
21
|
+
setError(null);
|
|
22
|
+
}
|
|
23
|
+
function cancel(_input = {}, _options = {}) {
|
|
24
|
+
if (busy.current)
|
|
25
|
+
return;
|
|
26
|
+
setTarget(null);
|
|
27
|
+
setUsername("");
|
|
28
|
+
setError(null);
|
|
29
|
+
}
|
|
30
|
+
async function confirm(_input = {}, _options = {}) {
|
|
31
|
+
if (!target || busy.current || !required.canRename(target))
|
|
32
|
+
return;
|
|
33
|
+
if (!username.trim()) {
|
|
34
|
+
setError(required.translate("username must not be blank"));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
busy.current = true;
|
|
38
|
+
setError(null);
|
|
39
|
+
try {
|
|
40
|
+
await mutation.mutate({ input: { principalId: target.principalId, username } });
|
|
41
|
+
setTarget(null);
|
|
42
|
+
setUsername("");
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
setError(describeApiError({ error: cause, fallback: required.translate("failed to change username"), translate: required.translate }));
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
busy.current = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { target, username, setUsername, error, pending: mutation.status === "pending", open, cancel, confirm };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=use-change-username.hooks.js.map
|
|
@@ -3,6 +3,7 @@ import type { AdminIdentityUser, AdminPolicy, AdminRole } from "../../models.js"
|
|
|
3
3
|
import { type Translate } from "@jini-ai/ui/panel-kit";
|
|
4
4
|
import { type UserAccountCapabilities } from "../rules.js";
|
|
5
5
|
import type { UsersPort } from "../../ports.js";
|
|
6
|
+
import { useChangeUsername } from "./use-change-username.hooks.js";
|
|
6
7
|
/**
|
|
7
8
|
* @file Everything the Users screen does, so `Users.tsx` is only markup.
|
|
8
9
|
*
|
|
@@ -143,6 +144,14 @@ export interface UsersController {
|
|
|
143
144
|
* the real boundary is server-side, not this flag. */
|
|
144
145
|
canManageUserTrash: boolean;
|
|
145
146
|
rowCapabilities: (user: AdminIdentityUser) => UserAccountCapabilities;
|
|
147
|
+
ownProfile?: AdminIdentityUser | null;
|
|
148
|
+
canRenameUser?: ((user: AdminIdentityUser) => boolean) | undefined;
|
|
149
|
+
canRenameOwnProfile?: boolean | undefined;
|
|
150
|
+
openOwnRename?: (() => void) | undefined;
|
|
151
|
+
renameUserHandler?: ((required: {
|
|
152
|
+
user: AdminIdentityUser;
|
|
153
|
+
}) => ((user: AdminIdentityUser) => void) | undefined) | undefined;
|
|
154
|
+
changeUsername?: ReturnType<typeof useChangeUsername> | undefined;
|
|
146
155
|
/** The user a `RowMenu` "Delete" selection is asking to confirm; `null` when the dialog is shut.
|
|
147
156
|
* Mirrors `confirmingDisable` exactly (own state, own request/confirm pair) rather than sharing
|
|
148
157
|
* it — Delete and Disable are two independent confirms an operator could otherwise not tell apart
|
|
@@ -3,6 +3,7 @@ import { interpolate } from "@jini-ai/ui/panel-kit";
|
|
|
3
3
|
import { useFetchMutation, useFetchQuery } from "@jini-ai/ui/panel-kit";
|
|
4
4
|
import { useAsyncAction } from "@jini-ai/ui/panel-kit";
|
|
5
5
|
import { describeApiError, KEYS, isOwnerAccount, userAccountCapabilities } from "../rules.js";
|
|
6
|
+
import { useChangeUsername } from "./use-change-username.hooks.js";
|
|
6
7
|
/** The shape `onAssignRole`/`onAttachPolicy` both repeat: set the shared `grantSaving`/`grantError`
|
|
7
8
|
* pair, run one mutation, do a success-only side effect, and clear saving in a `finally` — the
|
|
8
9
|
* "whole-hook" complexity view (brief §2) rolls every closure inside a hook into one score, so two
|
|
@@ -164,6 +165,7 @@ export function useUsers({ port, translate, queryScope }, { openOwnPasswordReset
|
|
|
164
165
|
* one opened via `/users/change-password`. One extra `me()` call per screen load is cheaper than
|
|
165
166
|
* threading two different notice code paths for what is, to the operator, the same event. */
|
|
166
167
|
const [ownPrincipalId, setOwnPrincipalId] = useState(null);
|
|
168
|
+
const [effectivePermissions, setEffectivePermissions] = useState([]);
|
|
167
169
|
// `canManageUserTrash` (delete-user plan v2, Slice 4): resolved by this SAME one-shot `me()` call
|
|
168
170
|
// rather than a second request — see `UsersController.canManageUserTrash`'s own doc comment.
|
|
169
171
|
const [canManageUserTrash, setCanManageUserTrash] = useState(false);
|
|
@@ -174,6 +176,7 @@ export function useUsers({ port, translate, queryScope }, { openOwnPasswordReset
|
|
|
174
176
|
.then((res) => {
|
|
175
177
|
if (!cancelled) {
|
|
176
178
|
setOwnPrincipalId(res.user.id);
|
|
179
|
+
setEffectivePermissions(res.effectivePermissions ?? []);
|
|
177
180
|
setCanManageUserTrash(res.canManageUserTrash);
|
|
178
181
|
}
|
|
179
182
|
})
|
|
@@ -199,6 +202,28 @@ export function useUsers({ port, translate, queryScope }, { openOwnPasswordReset
|
|
|
199
202
|
callerIsOwner: ownRow ? isOwnerAccount({ user: ownRow, roles: roleMap }) : false,
|
|
200
203
|
canManageUserTrash, roles: roleMap });
|
|
201
204
|
}
|
|
205
|
+
const ownProfile = users?.find(user => user.principalId === ownPrincipalId) ?? null;
|
|
206
|
+
function canRenameUser(user) {
|
|
207
|
+
if (user.canRename === false)
|
|
208
|
+
return false;
|
|
209
|
+
if (!ownPrincipalId || (!effectivePermissions.includes("*") && !effectivePermissions.includes("user.manage")))
|
|
210
|
+
return false;
|
|
211
|
+
const roleMap = new Map((roles ?? []).map(role => [role.id, role]));
|
|
212
|
+
const callerIsOwner = ownProfile ? isOwnerAccount({ user: ownProfile, roles: roleMap }) : false;
|
|
213
|
+
// Server-supplied owner flags remain authoritative when role.manage is unavailable.
|
|
214
|
+
const targetIsOwner = user.isOwner ?? (user.roleIds.some(id => !roleMap.has(id)) || isOwnerAccount({ user, roles: roleMap }));
|
|
215
|
+
return !targetIsOwner || callerIsOwner;
|
|
216
|
+
}
|
|
217
|
+
const changeUsername = useChangeUsername({ port, queryScope, translate, canRename: canRenameUser });
|
|
218
|
+
const canRenameOwnProfile = ownProfile !== null && canRenameUser(ownProfile);
|
|
219
|
+
/** Profile and row actions enter the same guarded dialog; the view only renders affordances. */
|
|
220
|
+
function openOwnRename() {
|
|
221
|
+
if (ownProfile)
|
|
222
|
+
changeUsername.open({ user: ownProfile });
|
|
223
|
+
}
|
|
224
|
+
function renameUserHandler({ user }, _options = {}) {
|
|
225
|
+
return canRenameUser(user) ? (target) => changeUsername.open({ user: target }) : undefined;
|
|
226
|
+
}
|
|
202
227
|
const deepLinkAttemptedRef = useRef(false);
|
|
203
228
|
/** Set true exactly when this effect opens the dialog, cleared by the close-effect below the first
|
|
204
229
|
* time it observes `resetPasswordFor` go back to `null`. Read there to decide whether to navigate
|
|
@@ -443,6 +468,12 @@ export function useUsers({ port, translate, queryScope }, { openOwnPasswordReset
|
|
|
443
468
|
confirmResetPassword,
|
|
444
469
|
canManageUserTrash,
|
|
445
470
|
rowCapabilities,
|
|
471
|
+
ownProfile,
|
|
472
|
+
canRenameUser,
|
|
473
|
+
canRenameOwnProfile,
|
|
474
|
+
openOwnRename,
|
|
475
|
+
renameUserHandler,
|
|
476
|
+
changeUsername,
|
|
446
477
|
confirmingDelete,
|
|
447
478
|
setConfirmingDelete,
|
|
448
479
|
requestDelete,
|
|
@@ -47,6 +47,7 @@ export interface UserRowMenuHandlers {
|
|
|
47
47
|
onEnable: (user: AdminIdentityUser) => void;
|
|
48
48
|
onManage: (user: AdminIdentityUser) => void;
|
|
49
49
|
onResetPassword: (user: AdminIdentityUser) => void;
|
|
50
|
+
onChangeUsername?: ((user: AdminIdentityUser) => void) | undefined;
|
|
50
51
|
/** Delete-user plan v2 (2026-09-24) — always opens the confirm dialog, never fires immediately:
|
|
51
52
|
* unlike Enable, "Delete" moves the user to the Trash, a destructive-looking action even though
|
|
52
53
|
* it is recoverable there for 60 days. Only asked for when `userRowMenuItems`'s own `canDelete`
|
|
@@ -127,6 +127,9 @@ export function userRowMenuItems({ user, toggleSaving, handlers, translate, canD
|
|
|
127
127
|
const permissions = typeof capabilities === "boolean"
|
|
128
128
|
? { canDelete: capabilities, canDisable: true, canEnable: true, canResetPassword: true }
|
|
129
129
|
: capabilities;
|
|
130
|
+
if (handlers.onChangeUsername) {
|
|
131
|
+
items.push({ key: "change-username", label: translate("Change username"), onSelect: () => handlers.onChangeUsername?.(user) });
|
|
132
|
+
}
|
|
130
133
|
if (permissions.canDelete) {
|
|
131
134
|
items.push({
|
|
132
135
|
key: "delete",
|
|
@@ -75,12 +75,13 @@ export declare function enablePrincipal(required: {
|
|
|
75
75
|
}>;
|
|
76
76
|
/**
|
|
77
77
|
* `UPDATE_USER` (feature.spec.md ). Gated by `user.manage`; existing self-profile edits also
|
|
78
|
-
* accept `member.manage`.
|
|
79
|
-
*
|
|
78
|
+
* accept `member.manage`. Username changes require `user.manage`, including self edits.
|
|
79
|
+
* Only owners may edit owner targets. Password is not this transition's concern.
|
|
80
|
+
* An absent or empty-string `email`
|
|
80
81
|
* clears the stored value to `undefined` ( — no distinct "blank but present" state).
|
|
81
82
|
*
|
|
82
|
-
* @complexity O(
|
|
83
|
-
*
|
|
83
|
+
* @complexity O(n) normalization for n username characters; fixed repo/Trash reads, one user save
|
|
84
|
+
* and one audit append for an actual rename, all inside the shared identity transaction.
|
|
84
85
|
* See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
|
|
85
86
|
*/
|
|
86
87
|
export declare function updateUser(required: {
|
|
@@ -92,6 +93,7 @@ export declare function updateUser(required: {
|
|
|
92
93
|
};
|
|
93
94
|
}, optional?: {
|
|
94
95
|
email?: string | undefined;
|
|
96
|
+
username?: string | undefined;
|
|
95
97
|
}): Promise<{
|
|
96
98
|
user: UserRecord;
|
|
97
99
|
}>;
|
|
@@ -2,6 +2,7 @@ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
|
2
2
|
import { inIdentityTransaction } from "./identity-transaction.js";
|
|
3
3
|
import { assertCallerHasAnyPermission, assertGrantClamp, assertOwnerTargetMayBeModified, principalHoldsOwnerWildcard } from "./grant-service.js";
|
|
4
4
|
import { validatePasswordPolicy } from "./password-policy.js";
|
|
5
|
+
import { normalizeUsername } from "./username.js";
|
|
5
6
|
import { isKnownPermission } from "../core/permissions.js";
|
|
6
7
|
import { principalKindMayExercisePermission } from "../core/principal-kind-policy.js";
|
|
7
8
|
import { IdentityConflictError, IdentityNotFoundError, IdentityValidationError, OwnerRequiredError, PermissionUnknownError, } from "../core/types.js";
|
|
@@ -138,12 +139,13 @@ export async function enablePrincipal(required) {
|
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
141
|
* `UPDATE_USER` (feature.spec.md ). Gated by `user.manage`; existing self-profile edits also
|
|
141
|
-
* accept `member.manage`.
|
|
142
|
-
*
|
|
142
|
+
* accept `member.manage`. Username changes require `user.manage`, including self edits.
|
|
143
|
+
* Only owners may edit owner targets. Password is not this transition's concern.
|
|
144
|
+
* An absent or empty-string `email`
|
|
143
145
|
* clears the stored value to `undefined` ( — no distinct "blank but present" state).
|
|
144
146
|
*
|
|
145
|
-
* @complexity O(
|
|
146
|
-
*
|
|
147
|
+
* @complexity O(n) normalization for n username characters; fixed repo/Trash reads, one user save
|
|
148
|
+
* and one audit append for an actual rename, all inside the shared identity transaction.
|
|
147
149
|
* See docs/decisions/DR-002-grant-authority-and-admin-safety.md.
|
|
148
150
|
*/
|
|
149
151
|
export async function updateUser(required, optional = {}) {
|
|
@@ -158,7 +160,7 @@ export async function updateUser(required, optional = {}) {
|
|
|
158
160
|
workspaceId: input.workspaceId,
|
|
159
161
|
callerPrincipalId: input.callerPrincipalId,
|
|
160
162
|
// Preserve existing self-profile access without letting member.manage edit other operators.
|
|
161
|
-
permissions: input.callerPrincipalId === input.principalId
|
|
163
|
+
permissions: input.username === undefined && input.callerPrincipalId === input.principalId
|
|
162
164
|
? ["user.manage", "member.manage"]
|
|
163
165
|
: ["user.manage"],
|
|
164
166
|
});
|
|
@@ -174,12 +176,53 @@ export async function updateUser(required, optional = {}) {
|
|
|
174
176
|
callerPrincipalId: input.callerPrincipalId,
|
|
175
177
|
principalId: input.principalId,
|
|
176
178
|
});
|
|
177
|
-
const
|
|
179
|
+
const username = input.username === undefined ? target.username : await resolveUpdatedUsername({ deps, input, target });
|
|
180
|
+
// Preserve the old email setter contract, while a username-only call retains email.
|
|
181
|
+
const email = input.username !== undefined && !Object.hasOwn(optional, "email")
|
|
182
|
+
? target.email : (input.email || undefined);
|
|
183
|
+
const updated = { ...target, username, email };
|
|
178
184
|
await deps.repos.users.save(updated);
|
|
185
|
+
if (username !== target.username) {
|
|
186
|
+
// Renaming never changes the principal id or credentials: existing sessions stay valid.
|
|
187
|
+
await deps.usernameChanges.record({ workspaceId: input.workspaceId, principalId: input.principalId,
|
|
188
|
+
callerPrincipalId: input.callerPrincipalId, previousUsername: target.username, username,
|
|
189
|
+
occurredAt: kernelNowIso({ clock: deps.clock }) });
|
|
190
|
+
}
|
|
179
191
|
return { user: updated };
|
|
180
192
|
},
|
|
181
193
|
});
|
|
182
194
|
}
|
|
195
|
+
/** Validate and reserve a normalized name inside the caller's identity transaction.
|
|
196
|
+
* Permission/owner gates run first, so conflicts never disclose accounts to unauthorized callers.
|
|
197
|
+
* @complexity O(n) normalization for n username characters; one indexed lookup and up to two Trash reads.
|
|
198
|
+
* Throws validation or conflict errors; performs no writes.
|
|
199
|
+
*/
|
|
200
|
+
async function resolveUpdatedUsername(required, _optional = {}) {
|
|
201
|
+
const { deps, input, target } = required;
|
|
202
|
+
if (typeof input.username !== "string")
|
|
203
|
+
throw new IdentityValidationError({ message: "username must be a string" });
|
|
204
|
+
const username = normalizeUsername({ raw: input.username });
|
|
205
|
+
if (!username)
|
|
206
|
+
throw new IdentityValidationError({ message: "username must not be blank" });
|
|
207
|
+
// Missing host ports are internal wiring faults, not user input errors (HTTP 500, not 400).
|
|
208
|
+
if (!deps.usernameChanges)
|
|
209
|
+
throw new Error("username changes are not configured");
|
|
210
|
+
if (await deps.usernameChanges.isInTrash({ workspaceId: input.workspaceId, principalId: input.principalId })) {
|
|
211
|
+
throw new IdentityConflictError({ message: "this user is in the Trash; restore them first", code: "USER_IN_TRASH" });
|
|
212
|
+
}
|
|
213
|
+
if (username !== target.username) {
|
|
214
|
+
await deps.usernameChanges.assertSupported?.({ workspaceId: input.workspaceId, principalId: input.principalId });
|
|
215
|
+
}
|
|
216
|
+
// Trash keeps credential rows until purge, so the unfiltered lookup reserves those names too.
|
|
217
|
+
const existing = await deps.repos.users.findByUsername({ workspaceId: input.workspaceId, username });
|
|
218
|
+
if (existing && existing.principalId !== target.principalId) {
|
|
219
|
+
if (await deps.usernameChanges.isInTrash({ workspaceId: input.workspaceId, principalId: existing.principalId })) {
|
|
220
|
+
throw new IdentityConflictError({ message: "a user with this username is in the Trash; restore or delete them permanently first", code: "USERNAME_IN_TRASH" });
|
|
221
|
+
}
|
|
222
|
+
throw new IdentityConflictError({ message: `username '${username}' is already in use`, code: "RESOURCE_CONFLICT" });
|
|
223
|
+
}
|
|
224
|
+
return username;
|
|
225
|
+
}
|
|
183
226
|
/**
|
|
184
227
|
* `RESET_USER_PASSWORD` (feature.spec.md ). Gated by **`user.manage`** only
|
|
185
228
|
* (resetting a credential is at least as sensitive as disabling the account, own reasoning). Hashes the new password identically to `CREATE_USER` (argon2id,
|
|
@@ -40,6 +40,7 @@ export interface IdentityToolDeps {
|
|
|
40
40
|
/** Bind the same transaction port as every grant/status writer. */
|
|
41
41
|
transactions: IdentityRepos["transactions"];
|
|
42
42
|
tokens: AuthServiceDeps["tokens"];
|
|
43
|
+
usernameChanges?: AuthServiceDeps["usernameChanges"];
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
45
46
|
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
@@ -48,6 +48,7 @@ function identityServiceDepsFrom(deps) {
|
|
|
48
48
|
clock: deps.clock,
|
|
49
49
|
idGen: deps.idGen,
|
|
50
50
|
tokens: deps.tokens,
|
|
51
|
+
usernameChanges: deps.usernameChanges,
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
@@ -64,6 +65,8 @@ export const identityDerivedRisk = new Map([
|
|
|
64
65
|
["identity_user_create", "mutates-durable-state"],
|
|
65
66
|
// -> updateUser (admin-crud-service.ts): users.save, email field only.
|
|
66
67
|
["identity_user_update_email", "mutates-durable-state"],
|
|
68
|
+
// -> updateUser (admin-crud-service.ts): users.save, username field only, plus the transactional audit.
|
|
69
|
+
["identity_user_change_username", "mutates-durable-state"],
|
|
67
70
|
// -> disablePrincipal (admin-crud-service.ts): status flip, guarded by the owner floor. See docs/decisions/DR-007-workspace-and-owner-floors.md.
|
|
68
71
|
["identity_user_disable", "mutates-durable-state"],
|
|
69
72
|
// -> enablePrincipal (admin-crud-service.ts): status flip back to active.
|
|
@@ -283,6 +286,17 @@ export function buildIdentityRegistrations(routeDeps) {
|
|
|
283
286
|
throw new Error(`principal '${input.req("principalId")}' was not found`);
|
|
284
287
|
return { user: toIdentityUserView({ principal, user, roleIds: await roleIdsFor(routeDeps, principal.id) }) };
|
|
285
288
|
},
|
|
289
|
+
identity_user_change_username: async (ctx) => {
|
|
290
|
+
const input = identityInput("identity_user_change_username", ctx.input);
|
|
291
|
+
const principalId = input.req("principalId");
|
|
292
|
+
const { user } = await updateUser({ deps: serviceDeps(), input: {
|
|
293
|
+
workspaceId: routeDeps.workspaceId, callerPrincipalId: ctx.principal.id, principalId,
|
|
294
|
+
} }, { username: input.req("username") });
|
|
295
|
+
const principal = await routeDeps.principalRepo.findById({ workspaceId: routeDeps.workspaceId, id: principalId });
|
|
296
|
+
if (!principal)
|
|
297
|
+
throw new Error(`principal '${principalId}' was not found`);
|
|
298
|
+
return { user: toIdentityUserView({ principal, user, roleIds: await roleIdsFor(routeDeps, principalId) }) };
|
|
299
|
+
},
|
|
286
300
|
identity_user_disable: async (ctx) => {
|
|
287
301
|
const input = identityInput("identity_user_disable", ctx.input);
|
|
288
302
|
const { principal } = await disablePrincipal({
|
|
@@ -106,6 +106,17 @@ export const identityAgentToolCatalog = [
|
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: "identity_user_change_username",
|
|
111
|
+
description: "Changes an operator user's login username. Call after finding their principalId with identity_user_list. Returns { user } with the normalized username; email, password and existing sessions stay unchanged. Requires user.manage even for your own profile; only owners may rename owner accounts. Fails for blank names, missing users, users in Trash, or names reserved by any user including Trash. Choose a different name or restore/purge the conflicting trashed user before retrying.",
|
|
112
|
+
sideEffects: "mutates-durable-state",
|
|
113
|
+
authorization: { permission: "user.manage" },
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object", additionalProperties: false, required: ["principalId", "username"],
|
|
116
|
+
properties: { principalId: PRINCIPAL_ID_SCHEMA, username: { type: "string", minLength: 1,
|
|
117
|
+
description: "New login name. Trimmed, lowercased and Unicode NFC normalized; unique per workspace, including users in Trash." } },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
109
120
|
{
|
|
110
121
|
name: "identity_user_disable",
|
|
111
122
|
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.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type UserManagementMessages } from "./messages.js";
|
|
2
|
-
import type { SessionTokenPort } from "../core/runtime-ports.js";
|
|
2
|
+
import type { SessionTokenPort, UsernameChangePort } from "../core/runtime-ports.js";
|
|
3
3
|
import type { Clock, IdGenerator, ISODateTime, UUID } from "@jini-ai/core/primitives";
|
|
4
4
|
import type { IdentityRepos, PasswordHasherPort } from "../core/ports.js";
|
|
5
5
|
import { type PrincipalRecord, type SessionRecord } from "../core/types.js";
|
|
@@ -9,6 +9,8 @@ export interface AuthServiceDeps {
|
|
|
9
9
|
clock: Clock;
|
|
10
10
|
idGen: IdGenerator;
|
|
11
11
|
tokens: SessionTokenPort;
|
|
12
|
+
/** Additive: legacy email updates need no rename lifecycle; renames require it. */
|
|
13
|
+
usernameChanges?: UsernameChangePort | undefined;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Mint and persist a session for a principal the caller has ALREADY identified by some means
|
package/dist/server/seed.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface SeedIdentityInput {
|
|
|
24
24
|
}
|
|
25
25
|
export interface SeedIdentityOptions {
|
|
26
26
|
ownerEmail?: string | undefined;
|
|
27
|
+
/** Stable bootstrap binding persisted by the host. Never derive this from a mutable login/title. */
|
|
28
|
+
seededOwnerPrincipalId?: UUID | undefined;
|
|
27
29
|
}
|
|
28
30
|
export interface SeedIdentityResult {
|
|
29
31
|
ownerPrincipalId: UUID;
|
|
@@ -33,8 +35,8 @@ export interface SeedIdentityResult {
|
|
|
33
35
|
* Seed first-boot identity data.
|
|
34
36
|
*
|
|
35
37
|
* Idempotent in two distinct senses, and both are load-bearing:
|
|
36
|
-
* - A seed that RAN TO COMPLETION is a no-op — the owner
|
|
37
|
-
*
|
|
38
|
+
* - A seed that RAN TO COMPLETION is a no-op — the owner lookup below early-returns after
|
|
39
|
+
* confirming its role binding (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
38
40
|
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
39
41
|
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
40
42
|
* `identityReady`.)
|
|
@@ -45,8 +47,9 @@ export interface SeedIdentityResult {
|
|
|
45
47
|
* previous attempt already wrote and fills in only what is missing. See the block comment above
|
|
46
48
|
* `ensureBuiltinRole` for why this function must survive being killed halfway.
|
|
47
49
|
*
|
|
48
|
-
* @complexity O(1)
|
|
49
|
-
*
|
|
50
|
+
* @complexity O(1) owner lookup plus the existing principal roster read; fresh seeds make a
|
|
51
|
+
* fixed small number of inserts (4 roles/policies, ~2 dozen policy_permissions,
|
|
52
|
+
* 3 principals, 1 user, 1 role assignment).
|
|
50
53
|
* @overallScore 100
|
|
51
54
|
*/
|
|
52
55
|
export declare function seedIdentity(required: {
|
package/dist/server/seed.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
2
2
|
import { normalizeUsername } from "./username.js";
|
|
3
|
+
import { IdentityNotFoundError } from "../core/types.js";
|
|
3
4
|
/**
|
|
4
5
|
* @file First-boot identity seed (state.spec `SEED_FIRST_BOOT`).
|
|
5
6
|
*
|
|
@@ -20,7 +21,7 @@ import { normalizeUsername } from "./username.js";
|
|
|
20
21
|
* Seed data lives in one place so both composition roots stay identical
|
|
21
22
|
* (mirrors `server/seed.ts`'s reasoning for workspace/post/presentation seed
|
|
22
23
|
* data). Idempotent: re-seeding an already-seeded workspace is a no-op
|
|
23
|
-
* (checked via the owner username).
|
|
24
|
+
* (checked via a host-supplied stable principal id, or the legacy owner username).
|
|
24
25
|
* See docs/decisions/DR-001-identity-and-session-boundary.md.
|
|
25
26
|
*/
|
|
26
27
|
/** : the literal id the legacy Article VI actor stamp resolves to. See docs/decisions/DR-001-identity-and-session-boundary.md. */
|
|
@@ -303,8 +304,8 @@ async function ensureOwnerRoleBinding(required) {
|
|
|
303
304
|
* Seed first-boot identity data.
|
|
304
305
|
*
|
|
305
306
|
* Idempotent in two distinct senses, and both are load-bearing:
|
|
306
|
-
* - A seed that RAN TO COMPLETION is a no-op — the owner
|
|
307
|
-
*
|
|
307
|
+
* - A seed that RAN TO COMPLETION is a no-op — the owner lookup below early-returns after
|
|
308
|
+
* confirming its role binding (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
308
309
|
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
309
310
|
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
310
311
|
* `identityReady`.)
|
|
@@ -315,8 +316,9 @@ async function ensureOwnerRoleBinding(required) {
|
|
|
315
316
|
* previous attempt already wrote and fills in only what is missing. See the block comment above
|
|
316
317
|
* `ensureBuiltinRole` for why this function must survive being killed halfway.
|
|
317
318
|
*
|
|
318
|
-
* @complexity O(1)
|
|
319
|
-
*
|
|
319
|
+
* @complexity O(1) owner lookup plus the existing principal roster read; fresh seeds make a
|
|
320
|
+
* fixed small number of inserts (4 roles/policies, ~2 dozen policy_permissions,
|
|
321
|
+
* 3 principals, 1 user, 1 role assignment).
|
|
320
322
|
* @overallScore 100
|
|
321
323
|
*/
|
|
322
324
|
export async function seedIdentity(required, optional = {}) {
|
|
@@ -326,7 +328,14 @@ export async function seedIdentity(required, optional = {}) {
|
|
|
326
328
|
const ownerUsername = normalizeUsername({ raw: input.ownerUsername });
|
|
327
329
|
const ownerPassword = input.ownerPassword;
|
|
328
330
|
const nowIso = kernelNowIso({ clock: deps.clock });
|
|
329
|
-
|
|
331
|
+
// An explicit binding is authoritative, even when somebody has reused its former login name.
|
|
332
|
+
// A missing bound row is an error, never permission to mint a replacement owner.
|
|
333
|
+
const seededOwnerUser = input.seededOwnerPrincipalId !== undefined
|
|
334
|
+
? await deps.repos.users.findByPrincipalId({ workspaceId, principalId: input.seededOwnerPrincipalId }) : null;
|
|
335
|
+
if (input.seededOwnerPrincipalId !== undefined && !seededOwnerUser) {
|
|
336
|
+
throw new IdentityNotFoundError({ message: "the bound seeded owner user was not found; restore the owner identity before booting" });
|
|
337
|
+
}
|
|
338
|
+
const existingOwnerUser = seededOwnerUser ?? await deps.repos.users.findByUsername({
|
|
330
339
|
workspaceId,
|
|
331
340
|
username: ownerUsername,
|
|
332
341
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/user-management",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Runtime-separated user management contracts and Node authentication, authorization, and administration services.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -87,16 +87,16 @@
|
|
|
87
87
|
"registry": "https://registry.npmjs.org"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@jini-ai/core": "^0.4.
|
|
90
|
+
"@jini-ai/core": "^0.4.4"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"argon2": "^0.44.0",
|
|
94
94
|
"react": "^18.3.0 || ^19.0.0",
|
|
95
95
|
"react-dom": "^18.3.0 || ^19.0.0",
|
|
96
|
-
"@jini-ai/ui": "^0.4.0",
|
|
97
|
-
"@jini-ai/admin": "^0.4.
|
|
98
|
-
"@jini-ai/agentic": "^0.4.
|
|
99
|
-
"@jini-ai/ui-kit": "^0.1.
|
|
96
|
+
"@jini-ai/ui": "^0.4.8 || ^0.5.0",
|
|
97
|
+
"@jini-ai/admin": "^0.4.6",
|
|
98
|
+
"@jini-ai/agentic": "^0.4.4",
|
|
99
|
+
"@jini-ai/ui-kit": "^0.1.3"
|
|
100
100
|
},
|
|
101
101
|
"peerDependenciesMeta": {
|
|
102
102
|
"argon2": {
|
|
@@ -134,12 +134,12 @@
|
|
|
134
134
|
"@testing-library/user-event": "^14.5.0",
|
|
135
135
|
"@testing-library/jest-dom": "^6.6.3",
|
|
136
136
|
"jsdom": "^25.0.1",
|
|
137
|
-
"@jini-ai/ui": "0.
|
|
138
|
-
"@jini-ai/
|
|
139
|
-
"@jini-ai/
|
|
140
|
-
"@jini-ai/
|
|
141
|
-
"@jini-ai/
|
|
142
|
-
"@jini-ai/
|
|
137
|
+
"@jini-ai/ui": "0.5.0",
|
|
138
|
+
"@jini-ai/ui-kit": "^0.1.3",
|
|
139
|
+
"@jini-ai/cms": "0.5.10",
|
|
140
|
+
"@jini-ai/admin": "0.4.7",
|
|
141
|
+
"@jini-ai/core": "^0.4.4",
|
|
142
|
+
"@jini-ai/agentic": "0.4.4"
|
|
143
143
|
},
|
|
144
144
|
"scripts": {
|
|
145
145
|
"build": "tsc -p tsconfig.json",
|