@voyant-travel/auth 0.128.3 → 0.130.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/README.md +14 -0
- package/dist/cloud-broker/members.d.ts +9 -0
- package/dist/cloud-broker/members.js +10 -0
- package/dist/cloud-broker/members.js.map +1 -1
- package/dist/cloud-broker.d.ts +1 -1
- package/dist/cloud-broker.js +1 -1
- package/dist/cloud-broker.js.map +1 -1
- package/dist/identity-access-graph-runtime.js +2 -1
- package/dist/identity-access-graph-runtime.js.map +1 -1
- package/dist/invitations-routes.js +4 -0
- package/dist/invitations-routes.js.map +1 -1
- package/dist/local-member-access.d.ts +15 -0
- package/dist/local-member-access.js +119 -0
- package/dist/local-member-access.js.map +1 -0
- package/dist/node-runtime.d.ts +1 -0
- package/dist/node-runtime.js +2 -7
- package/dist/node-runtime.js.map +1 -1
- package/dist/runtime-contributor.js +23 -2
- package/dist/runtime-contributor.js.map +1 -1
- package/dist/server.d.ts +10 -6
- package/dist/server.js +6 -3
- package/dist/server.js.map +1 -1
- package/dist/team-management-cloud-adapter.d.ts +7 -0
- package/dist/team-management-cloud-adapter.js +146 -0
- package/dist/team-management-cloud-adapter.js.map +1 -0
- package/dist/team-management-local-adapter.d.ts +8 -0
- package/dist/team-management-local-adapter.js +285 -0
- package/dist/team-management-local-adapter.js.map +1 -0
- package/dist/team-management-policy.d.ts +27 -0
- package/dist/team-management-policy.js +109 -0
- package/dist/team-management-policy.js.map +1 -0
- package/dist/team-management-runtime-port.d.ts +60 -0
- package/dist/team-management-runtime-port.js +25 -0
- package/dist/team-management-runtime-port.js.map +1 -0
- package/dist/team-routes.d.ts +3 -3
- package/dist/team-routes.js +89 -138
- package/dist/team-routes.js.map +1 -1
- package/dist/voyant.js +67 -3
- package/dist/voyant.js.map +1 -1
- package/openapi/admin/team.json +42 -74
- package/package.json +10 -5
- package/dist/tsconfig.typecheck.tsbuildinfo +0 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { cloudAuthUserLinks } from "@voyant-travel/db/schema/iam";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { inviteCloudAdminMember, listCloudAdminInvitations, listCloudAdminMemberRoles, listCloudAdminMembers, revokeCloudAdminInvitation, setCloudAdminMemberAccess, setCloudAdminMemberRole, } from "./cloud-broker.js";
|
|
4
|
+
import { TeamManagementError } from "./team-management-policy.js";
|
|
5
|
+
function roleLevel(roleId) {
|
|
6
|
+
switch (roleId.trim().toLowerCase()) {
|
|
7
|
+
case "owner":
|
|
8
|
+
return 40;
|
|
9
|
+
case "admin":
|
|
10
|
+
return 30;
|
|
11
|
+
case "editor":
|
|
12
|
+
case "member":
|
|
13
|
+
return 20;
|
|
14
|
+
case "viewer":
|
|
15
|
+
case "guest":
|
|
16
|
+
return 10;
|
|
17
|
+
default:
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function cloudTeamMemberDto(member) {
|
|
22
|
+
const roleId = member.roleSlug ?? "member";
|
|
23
|
+
return {
|
|
24
|
+
id: member.membershipId,
|
|
25
|
+
email: member.email,
|
|
26
|
+
name: member.name ?? null,
|
|
27
|
+
roleId,
|
|
28
|
+
roleName: member.roleName ?? roleId,
|
|
29
|
+
status: member.hasDeploymentAccess && member.status.toLowerCase() === "active"
|
|
30
|
+
? "active"
|
|
31
|
+
: "deactivated",
|
|
32
|
+
joinedAt: member.createdAt ?? null,
|
|
33
|
+
lastActivityAt: member.lastActivityAt ?? null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function cloudTeamInvitationDto(invitation) {
|
|
37
|
+
const roleId = invitation.roleSlug ?? "member";
|
|
38
|
+
return {
|
|
39
|
+
id: invitation.id,
|
|
40
|
+
email: invitation.email,
|
|
41
|
+
roleId,
|
|
42
|
+
roleName: invitation.roleName ?? roleId,
|
|
43
|
+
status: invitation.state,
|
|
44
|
+
createdAt: invitation.createdAt,
|
|
45
|
+
expiresAt: invitation.expiresAt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createdInvitationDto(invitation) {
|
|
49
|
+
return {
|
|
50
|
+
...cloudTeamInvitationDto(invitation),
|
|
51
|
+
acceptUrl: invitation.acceptInvitationUrl || null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function resolveCloudRequest(identityAccess, context) {
|
|
55
|
+
const deployment = identityAccess.resolveDeployment(context.bindings);
|
|
56
|
+
if (!deployment.cloudAdminMembers) {
|
|
57
|
+
throw new TeamManagementError("not_configured", "Voyant Cloud team management is not configured.", 501);
|
|
58
|
+
}
|
|
59
|
+
const [link] = await context.db
|
|
60
|
+
.select({ providerAccountId: cloudAuthUserLinks.providerAccountId })
|
|
61
|
+
.from(cloudAuthUserLinks)
|
|
62
|
+
.where(eq(cloudAuthUserLinks.userId, context.userId))
|
|
63
|
+
.limit(1);
|
|
64
|
+
if (!link?.providerAccountId) {
|
|
65
|
+
throw new TeamManagementError("forbidden", "No Cloud identity is linked to this session.");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
config: deployment.cloudAdminMembers,
|
|
69
|
+
actingWorkosUserId: link.providerAccountId,
|
|
70
|
+
actingExternalUserId: link.providerAccountId,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function createCloudTeamManagementAdapter(identityAccess) {
|
|
74
|
+
return {
|
|
75
|
+
async getActor(context) {
|
|
76
|
+
const request = await resolveCloudRequest(identityAccess, context);
|
|
77
|
+
const actor = (await listCloudAdminMembers(request)).find((member) => member.externalUserId === request.actingExternalUserId);
|
|
78
|
+
if (!actor?.hasDeploymentAccess) {
|
|
79
|
+
throw new TeamManagementError("forbidden", "The current user cannot manage this team.");
|
|
80
|
+
}
|
|
81
|
+
return { memberId: actor.membershipId, roleId: actor.roleSlug ?? "member" };
|
|
82
|
+
},
|
|
83
|
+
async getCapabilities(_context, actor) {
|
|
84
|
+
const canManage = actor.roleId === "owner" || actor.roleId === "admin";
|
|
85
|
+
return {
|
|
86
|
+
viewRoster: true,
|
|
87
|
+
inviteMembers: canManage,
|
|
88
|
+
manageRoles: canManage,
|
|
89
|
+
activateMembers: canManage,
|
|
90
|
+
deactivateMembers: canManage,
|
|
91
|
+
revokeInvitations: canManage,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
async listMembers(context) {
|
|
95
|
+
return (await listCloudAdminMembers(await resolveCloudRequest(identityAccess, context))).map(cloudTeamMemberDto);
|
|
96
|
+
},
|
|
97
|
+
async listRoles(context) {
|
|
98
|
+
return (await listCloudAdminMemberRoles(await resolveCloudRequest(identityAccess, context))).map((role) => ({ id: role.slug, name: role.name, description: role.description }));
|
|
99
|
+
},
|
|
100
|
+
async listInvitations(context) {
|
|
101
|
+
return (await listCloudAdminInvitations(await resolveCloudRequest(identityAccess, context))).map(cloudTeamInvitationDto);
|
|
102
|
+
},
|
|
103
|
+
async inviteMember(context, input) {
|
|
104
|
+
return createdInvitationDto(await inviteCloudAdminMember({
|
|
105
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
106
|
+
input: {
|
|
107
|
+
email: input.email,
|
|
108
|
+
roleSlug: input.roleId,
|
|
109
|
+
expiresInDays: input.expiresInDays,
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
},
|
|
113
|
+
async revokeInvitation(context, invitationId) {
|
|
114
|
+
await revokeCloudAdminInvitation({
|
|
115
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
116
|
+
invitationId,
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async updateMemberRole(context, memberId, roleId) {
|
|
120
|
+
return cloudTeamMemberDto(await setCloudAdminMemberRole({
|
|
121
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
122
|
+
membershipId: memberId,
|
|
123
|
+
roleSlug: roleId,
|
|
124
|
+
}));
|
|
125
|
+
},
|
|
126
|
+
async deactivateMember(context, memberId) {
|
|
127
|
+
return cloudTeamMemberDto(await setCloudAdminMemberAccess({
|
|
128
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
129
|
+
membershipId: memberId,
|
|
130
|
+
hasAccess: false,
|
|
131
|
+
}));
|
|
132
|
+
},
|
|
133
|
+
async activateMember(context, memberId) {
|
|
134
|
+
return cloudTeamMemberDto(await setCloudAdminMemberAccess({
|
|
135
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
136
|
+
membershipId: memberId,
|
|
137
|
+
hasAccess: true,
|
|
138
|
+
}));
|
|
139
|
+
},
|
|
140
|
+
roleLevel,
|
|
141
|
+
isOwnerRole(roleId) {
|
|
142
|
+
return roleId === "owner";
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=team-management-cloud-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-management-cloud-adapter.js","sourceRoot":"","sources":["../src/team-management-cloud-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { cloudAuthUserLinks } from \"@voyant-travel/db/schema/iam\"\nimport { eq } from \"drizzle-orm\"\n\nimport {\n type CloudAdminInvitation,\n type CloudAdminMember,\n type CloudAdminMembersRequest,\n inviteCloudAdminMember,\n listCloudAdminInvitations,\n listCloudAdminMemberRoles,\n listCloudAdminMembers,\n revokeCloudAdminInvitation,\n setCloudAdminMemberAccess,\n setCloudAdminMemberRole,\n} from \"./cloud-broker.js\"\nimport type { IdentityAccessRuntimeProvider } from \"./identity-access-runtime-port.js\"\nimport { type TeamManagementAdapter, TeamManagementError } from \"./team-management-policy.js\"\nimport type {\n CreatedTeamInvitationDto,\n TeamInvitationDto,\n TeamManagementCapabilitiesDto,\n TeamManagementRequestContext,\n TeamMemberDto,\n} from \"./team-management-runtime-port.js\"\n\nfunction roleLevel(roleId: string): number {\n switch (roleId.trim().toLowerCase()) {\n case \"owner\":\n return 40\n case \"admin\":\n return 30\n case \"editor\":\n case \"member\":\n return 20\n case \"viewer\":\n case \"guest\":\n return 10\n default:\n return 0\n }\n}\n\nexport function cloudTeamMemberDto(member: CloudAdminMember): TeamMemberDto {\n const roleId = member.roleSlug ?? \"member\"\n return {\n id: member.membershipId,\n email: member.email,\n name: member.name ?? null,\n roleId,\n roleName: member.roleName ?? roleId,\n status:\n member.hasDeploymentAccess && member.status.toLowerCase() === \"active\"\n ? \"active\"\n : \"deactivated\",\n joinedAt: member.createdAt ?? null,\n lastActivityAt: member.lastActivityAt ?? null,\n }\n}\n\nexport function cloudTeamInvitationDto(invitation: CloudAdminInvitation): TeamInvitationDto {\n const roleId = invitation.roleSlug ?? \"member\"\n return {\n id: invitation.id,\n email: invitation.email,\n roleId,\n roleName: invitation.roleName ?? roleId,\n status: invitation.state,\n createdAt: invitation.createdAt,\n expiresAt: invitation.expiresAt,\n }\n}\n\nfunction createdInvitationDto(invitation: CloudAdminInvitation): CreatedTeamInvitationDto {\n return {\n ...cloudTeamInvitationDto(invitation),\n acceptUrl: invitation.acceptInvitationUrl || null,\n }\n}\n\nasync function resolveCloudRequest(\n identityAccess: IdentityAccessRuntimeProvider,\n context: TeamManagementRequestContext,\n): Promise<CloudAdminMembersRequest & { actingExternalUserId: string }> {\n const deployment = identityAccess.resolveDeployment(context.bindings)\n if (!deployment.cloudAdminMembers) {\n throw new TeamManagementError(\n \"not_configured\",\n \"Voyant Cloud team management is not configured.\",\n 501,\n )\n }\n const [link] = await context.db\n .select({ providerAccountId: cloudAuthUserLinks.providerAccountId })\n .from(cloudAuthUserLinks)\n .where(eq(cloudAuthUserLinks.userId, context.userId))\n .limit(1)\n if (!link?.providerAccountId) {\n throw new TeamManagementError(\"forbidden\", \"No Cloud identity is linked to this session.\")\n }\n return {\n config: deployment.cloudAdminMembers,\n actingWorkosUserId: link.providerAccountId,\n actingExternalUserId: link.providerAccountId,\n }\n}\n\nexport function createCloudTeamManagementAdapter(\n identityAccess: IdentityAccessRuntimeProvider,\n): TeamManagementAdapter {\n return {\n async getActor(context) {\n const request = await resolveCloudRequest(identityAccess, context)\n const actor = (await listCloudAdminMembers(request)).find(\n (member) => member.externalUserId === request.actingExternalUserId,\n )\n if (!actor?.hasDeploymentAccess) {\n throw new TeamManagementError(\"forbidden\", \"The current user cannot manage this team.\")\n }\n return { memberId: actor.membershipId, roleId: actor.roleSlug ?? \"member\" }\n },\n async getCapabilities(_context, actor): Promise<TeamManagementCapabilitiesDto> {\n const canManage = actor.roleId === \"owner\" || actor.roleId === \"admin\"\n return {\n viewRoster: true,\n inviteMembers: canManage,\n manageRoles: canManage,\n activateMembers: canManage,\n deactivateMembers: canManage,\n revokeInvitations: canManage,\n }\n },\n async listMembers(context) {\n return (await listCloudAdminMembers(await resolveCloudRequest(identityAccess, context))).map(\n cloudTeamMemberDto,\n )\n },\n async listRoles(context) {\n return (\n await listCloudAdminMemberRoles(await resolveCloudRequest(identityAccess, context))\n ).map((role) => ({ id: role.slug, name: role.name, description: role.description }))\n },\n async listInvitations(context) {\n return (\n await listCloudAdminInvitations(await resolveCloudRequest(identityAccess, context))\n ).map(cloudTeamInvitationDto)\n },\n async inviteMember(context, input) {\n return createdInvitationDto(\n await inviteCloudAdminMember({\n ...(await resolveCloudRequest(identityAccess, context)),\n input: {\n email: input.email,\n roleSlug: input.roleId,\n expiresInDays: input.expiresInDays,\n },\n }),\n )\n },\n async revokeInvitation(context, invitationId) {\n await revokeCloudAdminInvitation({\n ...(await resolveCloudRequest(identityAccess, context)),\n invitationId,\n })\n },\n async updateMemberRole(context, memberId, roleId) {\n return cloudTeamMemberDto(\n await setCloudAdminMemberRole({\n ...(await resolveCloudRequest(identityAccess, context)),\n membershipId: memberId,\n roleSlug: roleId,\n }),\n )\n },\n async deactivateMember(context, memberId) {\n return cloudTeamMemberDto(\n await setCloudAdminMemberAccess({\n ...(await resolveCloudRequest(identityAccess, context)),\n membershipId: memberId,\n hasAccess: false,\n }),\n )\n },\n async activateMember(context, memberId) {\n return cloudTeamMemberDto(\n await setCloudAdminMemberAccess({\n ...(await resolveCloudRequest(identityAccess, context)),\n membershipId: memberId,\n hasAccess: true,\n }),\n )\n },\n roleLevel,\n isOwnerRole(roleId) {\n return roleId === \"owner\"\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IdentityAccessRuntimeProvider } from "./identity-access-runtime-port.js";
|
|
2
|
+
import { type TeamManagementAdapter } from "./team-management-policy.js";
|
|
3
|
+
export { localProviderIdForStatus } from "./local-member-access.js";
|
|
4
|
+
export declare function createLocalTeamManagementAdapter(identityAccess: IdentityAccessRuntimeProvider): TeamManagementAdapter;
|
|
5
|
+
export declare function localRoleForRedeemedInvitation(metadata: Record<string, unknown> | null): {
|
|
6
|
+
isSuperAdmin: boolean;
|
|
7
|
+
permissions: string[];
|
|
8
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { newId } from "@voyant-travel/db/lib/typeid";
|
|
2
|
+
import { apikeyTable, authAccount, authSession, authUser, userInvitationsTable, userProfilesTable, } from "@voyant-travel/db/schema/iam";
|
|
3
|
+
import { scopesForRole } from "@voyant-travel/types/member-roles";
|
|
4
|
+
import { desc, eq, sql } from "drizzle-orm";
|
|
5
|
+
import { isDeactivatedProviderId, localProviderIdForStatus } from "./local-member-access.js";
|
|
6
|
+
import { TeamManagementError } from "./team-management-policy.js";
|
|
7
|
+
const LOCAL_ROLES = [
|
|
8
|
+
{ id: "owner", name: "Owner", description: "Full access, including ownership changes." },
|
|
9
|
+
{ id: "admin", name: "Admin", description: "Full access to team and operator settings." },
|
|
10
|
+
{ id: "editor", name: "Editor", description: "Can manage day-to-day operator data." },
|
|
11
|
+
{ id: "viewer", name: "Viewer", description: "Read-only operator access." },
|
|
12
|
+
];
|
|
13
|
+
const ROLE_LEVELS = { owner: 40, admin: 30, editor: 20, viewer: 10 };
|
|
14
|
+
export { localProviderIdForStatus } from "./local-member-access.js";
|
|
15
|
+
function roleName(roleId) {
|
|
16
|
+
return LOCAL_ROLES.find((role) => role.id === roleId)?.name ?? roleId;
|
|
17
|
+
}
|
|
18
|
+
function sameScopes(left, right) {
|
|
19
|
+
return [...left].sort().join("\0") === [...right].sort().join("\0");
|
|
20
|
+
}
|
|
21
|
+
function resolveLocalRole(isSuperAdmin, permissions) {
|
|
22
|
+
if (isSuperAdmin)
|
|
23
|
+
return "owner";
|
|
24
|
+
if (permissions === null || permissions.includes("*"))
|
|
25
|
+
return "admin";
|
|
26
|
+
if (sameScopes(permissions, scopesForRole("editor") ?? []))
|
|
27
|
+
return "editor";
|
|
28
|
+
if (sameScopes(permissions, scopesForRole("viewer") ?? []))
|
|
29
|
+
return "viewer";
|
|
30
|
+
return "custom";
|
|
31
|
+
}
|
|
32
|
+
function permissionsForRole(roleId) {
|
|
33
|
+
const scopes = scopesForRole(roleId);
|
|
34
|
+
if (!scopes)
|
|
35
|
+
throw new TeamManagementError("role_not_found", "Team role not found.", 404);
|
|
36
|
+
return [...scopes];
|
|
37
|
+
}
|
|
38
|
+
function invitationRole(metadata) {
|
|
39
|
+
const roleId = metadata?.roleId;
|
|
40
|
+
return typeof roleId === "string" && ROLE_LEVELS[roleId] !== undefined ? roleId : "editor";
|
|
41
|
+
}
|
|
42
|
+
function invitationStatus(input) {
|
|
43
|
+
if (input.redeemedAt)
|
|
44
|
+
return "accepted";
|
|
45
|
+
if (input.expiresAt.getTime() <= Date.now())
|
|
46
|
+
return "expired";
|
|
47
|
+
return "pending";
|
|
48
|
+
}
|
|
49
|
+
function randomTokenBase64Url(bytes) {
|
|
50
|
+
const buffer = new Uint8Array(bytes);
|
|
51
|
+
crypto.getRandomValues(buffer);
|
|
52
|
+
let binary = "";
|
|
53
|
+
for (const byte of buffer)
|
|
54
|
+
binary += String.fromCharCode(byte);
|
|
55
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
56
|
+
}
|
|
57
|
+
async function sha256Hex(input) {
|
|
58
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
59
|
+
return Array.from(new Uint8Array(digest))
|
|
60
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
61
|
+
.join("");
|
|
62
|
+
}
|
|
63
|
+
async function listLocalMembers(context) {
|
|
64
|
+
const [rows, accounts] = await Promise.all([
|
|
65
|
+
context.db
|
|
66
|
+
.select({
|
|
67
|
+
id: authUser.id,
|
|
68
|
+
email: authUser.email,
|
|
69
|
+
authName: authUser.name,
|
|
70
|
+
createdAt: authUser.createdAt,
|
|
71
|
+
firstName: userProfilesTable.firstName,
|
|
72
|
+
lastName: userProfilesTable.lastName,
|
|
73
|
+
isSuperAdmin: userProfilesTable.isSuperAdmin,
|
|
74
|
+
permissions: userProfilesTable.permissions,
|
|
75
|
+
lastActiveAt: userProfilesTable.lastActiveAt,
|
|
76
|
+
})
|
|
77
|
+
.from(authUser)
|
|
78
|
+
.leftJoin(userProfilesTable, eq(userProfilesTable.id, authUser.id)),
|
|
79
|
+
context.db
|
|
80
|
+
.select({ userId: authAccount.userId, providerId: authAccount.providerId })
|
|
81
|
+
.from(authAccount),
|
|
82
|
+
]);
|
|
83
|
+
const activeUserIds = new Set(accounts
|
|
84
|
+
.filter((account) => !isDeactivatedProviderId(account.providerId))
|
|
85
|
+
.map((account) => account.userId));
|
|
86
|
+
return rows.map((row) => {
|
|
87
|
+
const roleId = resolveLocalRole(row.isSuperAdmin ?? false, row.permissions ?? null);
|
|
88
|
+
const profileName = [row.firstName, row.lastName].filter(Boolean).join(" ").trim();
|
|
89
|
+
return {
|
|
90
|
+
id: row.id,
|
|
91
|
+
email: row.email,
|
|
92
|
+
name: profileName || row.authName || null,
|
|
93
|
+
roleId,
|
|
94
|
+
roleName: roleName(roleId),
|
|
95
|
+
status: activeUserIds.has(row.id) ? "active" : "deactivated",
|
|
96
|
+
joinedAt: row.createdAt?.toISOString() ?? null,
|
|
97
|
+
lastActivityAt: row.lastActiveAt?.toISOString() ?? null,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function memberOrThrow(members, memberId) {
|
|
102
|
+
const member = members.find((candidate) => candidate.id === memberId);
|
|
103
|
+
if (!member)
|
|
104
|
+
throw new TeamManagementError("member_not_found", "Team member not found.", 404);
|
|
105
|
+
return member;
|
|
106
|
+
}
|
|
107
|
+
function assertActiveOwnerRemains(members, target) {
|
|
108
|
+
if (target.status !== "active" || target.roleId !== "owner")
|
|
109
|
+
return;
|
|
110
|
+
const activeOwnerCount = members.filter((member) => member.status === "active" && member.roleId === "owner").length;
|
|
111
|
+
if (activeOwnerCount <= 1) {
|
|
112
|
+
throw new TeamManagementError("last_owner", "The last active owner cannot be demoted or deactivated.", 409);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function withLockedLocalTeam(context, operation) {
|
|
116
|
+
return context.db.transaction(async (transaction) => {
|
|
117
|
+
// agent-quality: raw-sql reviewed -- owner: auth; table identifiers are static Drizzle schema objects.
|
|
118
|
+
await transaction.execute(sql `SELECT ${authUser.id} FROM ${authUser} ORDER BY ${authUser.id} FOR UPDATE`);
|
|
119
|
+
return operation({ ...context, db: transaction });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function createLocalTeamManagementAdapter(identityAccess) {
|
|
123
|
+
return {
|
|
124
|
+
async getActor(context) {
|
|
125
|
+
const actor = (await listLocalMembers(context)).find((member) => member.id === context.userId);
|
|
126
|
+
if (actor?.status !== "active") {
|
|
127
|
+
throw new TeamManagementError("forbidden", "The current user cannot manage this team.");
|
|
128
|
+
}
|
|
129
|
+
return { memberId: actor.id, roleId: actor.roleId };
|
|
130
|
+
},
|
|
131
|
+
async getCapabilities(_context, actor) {
|
|
132
|
+
const canManage = actor.roleId === "owner" || actor.roleId === "admin";
|
|
133
|
+
return {
|
|
134
|
+
viewRoster: true,
|
|
135
|
+
inviteMembers: canManage,
|
|
136
|
+
manageRoles: canManage,
|
|
137
|
+
activateMembers: canManage,
|
|
138
|
+
deactivateMembers: canManage,
|
|
139
|
+
revokeInvitations: canManage,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
listMembers: listLocalMembers,
|
|
143
|
+
async listRoles() {
|
|
144
|
+
return LOCAL_ROLES;
|
|
145
|
+
},
|
|
146
|
+
async listInvitations(context) {
|
|
147
|
+
const rows = await context.db
|
|
148
|
+
.select({
|
|
149
|
+
id: userInvitationsTable.id,
|
|
150
|
+
email: userInvitationsTable.email,
|
|
151
|
+
metadata: userInvitationsTable.metadata,
|
|
152
|
+
expiresAt: userInvitationsTable.expiresAt,
|
|
153
|
+
redeemedAt: userInvitationsTable.redeemedAt,
|
|
154
|
+
createdAt: userInvitationsTable.createdAt,
|
|
155
|
+
})
|
|
156
|
+
.from(userInvitationsTable)
|
|
157
|
+
.orderBy(desc(userInvitationsTable.createdAt))
|
|
158
|
+
.limit(100);
|
|
159
|
+
return rows.map((row) => {
|
|
160
|
+
const roleId = invitationRole(row.metadata);
|
|
161
|
+
return {
|
|
162
|
+
id: row.id,
|
|
163
|
+
email: row.email,
|
|
164
|
+
roleId,
|
|
165
|
+
roleName: roleName(roleId),
|
|
166
|
+
status: invitationStatus(row),
|
|
167
|
+
createdAt: row.createdAt.toISOString(),
|
|
168
|
+
expiresAt: row.expiresAt.toISOString(),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
async inviteMember(context, input) {
|
|
173
|
+
const email = input.email.trim().toLowerCase();
|
|
174
|
+
const [existingUser] = await context.db
|
|
175
|
+
.select({ id: authUser.id })
|
|
176
|
+
.from(authUser)
|
|
177
|
+
.where(eq(authUser.email, email))
|
|
178
|
+
.limit(1);
|
|
179
|
+
if (existingUser) {
|
|
180
|
+
throw new TeamManagementError("forbidden", "A team member with this email already exists.", 409);
|
|
181
|
+
}
|
|
182
|
+
const rawToken = randomTokenBase64Url(32);
|
|
183
|
+
const expiresInDays = input.expiresInDays ?? 3;
|
|
184
|
+
const expiresAt = new Date(Date.now() + expiresInDays * 86_400_000);
|
|
185
|
+
const now = new Date();
|
|
186
|
+
const id = newId("user_invitations");
|
|
187
|
+
await context.db.insert(userInvitationsTable).values({
|
|
188
|
+
id,
|
|
189
|
+
email,
|
|
190
|
+
tokenHash: await sha256Hex(rawToken),
|
|
191
|
+
expiresAt,
|
|
192
|
+
createdBy: context.userId,
|
|
193
|
+
metadata: { roleId: input.roleId },
|
|
194
|
+
createdAt: now,
|
|
195
|
+
});
|
|
196
|
+
const { appUrl } = identityAccess.resolveDeployment(context.bindings);
|
|
197
|
+
const acceptUrl = `${appUrl}/accept-invite?token=${encodeURIComponent(rawToken)}`;
|
|
198
|
+
await identityAccess.sendInvitationEmail(context.bindings, {
|
|
199
|
+
acceptUrl,
|
|
200
|
+
expiresInHours: expiresInDays * 24,
|
|
201
|
+
to: email,
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
email,
|
|
206
|
+
roleId: input.roleId,
|
|
207
|
+
roleName: roleName(input.roleId),
|
|
208
|
+
status: "pending",
|
|
209
|
+
createdAt: now.toISOString(),
|
|
210
|
+
expiresAt: expiresAt.toISOString(),
|
|
211
|
+
acceptUrl,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
async revokeInvitation(context, invitationId) {
|
|
215
|
+
await context.db.delete(userInvitationsTable).where(eq(userInvitationsTable.id, invitationId));
|
|
216
|
+
},
|
|
217
|
+
async updateMemberRole(context, memberId, roleId) {
|
|
218
|
+
return withLockedLocalTeam(context, async (transactionContext) => {
|
|
219
|
+
const members = await listLocalMembers(transactionContext);
|
|
220
|
+
const target = memberOrThrow(members, memberId);
|
|
221
|
+
if (roleId !== "owner")
|
|
222
|
+
assertActiveOwnerRemains(members, target);
|
|
223
|
+
await transactionContext.db
|
|
224
|
+
.update(userProfilesTable)
|
|
225
|
+
.set({
|
|
226
|
+
isSuperAdmin: roleId === "owner",
|
|
227
|
+
permissions: permissionsForRole(roleId),
|
|
228
|
+
updatedAt: new Date(),
|
|
229
|
+
})
|
|
230
|
+
.where(eq(userProfilesTable.id, memberId));
|
|
231
|
+
return memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
async deactivateMember(context, memberId) {
|
|
235
|
+
return withLockedLocalTeam(context, async (transactionContext) => {
|
|
236
|
+
const members = await listLocalMembers(transactionContext);
|
|
237
|
+
assertActiveOwnerRemains(members, memberOrThrow(members, memberId));
|
|
238
|
+
await transactionContext.db.delete(authSession).where(eq(authSession.userId, memberId));
|
|
239
|
+
await transactionContext.db.delete(apikeyTable).where(eq(apikeyTable.referenceId, memberId));
|
|
240
|
+
const accounts = await transactionContext.db
|
|
241
|
+
.select({ id: authAccount.id, providerId: authAccount.providerId })
|
|
242
|
+
.from(authAccount)
|
|
243
|
+
.where(eq(authAccount.userId, memberId));
|
|
244
|
+
for (const account of accounts) {
|
|
245
|
+
await transactionContext.db
|
|
246
|
+
.update(authAccount)
|
|
247
|
+
.set({ providerId: localProviderIdForStatus(account.providerId, "deactivated") })
|
|
248
|
+
.where(eq(authAccount.id, account.id));
|
|
249
|
+
}
|
|
250
|
+
return memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
async activateMember(context, memberId) {
|
|
254
|
+
return withLockedLocalTeam(context, async (transactionContext) => {
|
|
255
|
+
memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
256
|
+
const accounts = await transactionContext.db
|
|
257
|
+
.select({ id: authAccount.id, providerId: authAccount.providerId })
|
|
258
|
+
.from(authAccount)
|
|
259
|
+
.where(eq(authAccount.userId, memberId));
|
|
260
|
+
for (const account of accounts) {
|
|
261
|
+
await transactionContext.db
|
|
262
|
+
.update(authAccount)
|
|
263
|
+
.set({ providerId: localProviderIdForStatus(account.providerId, "active") })
|
|
264
|
+
.where(eq(authAccount.id, account.id));
|
|
265
|
+
}
|
|
266
|
+
const member = memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
267
|
+
if (member.status !== "active") {
|
|
268
|
+
throw new TeamManagementError("activation_unavailable", "This local member has no sign-in account to reactivate.", 409);
|
|
269
|
+
}
|
|
270
|
+
return member;
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
roleLevel(roleId) {
|
|
274
|
+
return ROLE_LEVELS[roleId] ?? 0;
|
|
275
|
+
},
|
|
276
|
+
isOwnerRole(roleId) {
|
|
277
|
+
return roleId === "owner";
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function localRoleForRedeemedInvitation(metadata) {
|
|
282
|
+
const roleId = invitationRole(metadata);
|
|
283
|
+
return { isSuperAdmin: roleId === "owner", permissions: permissionsForRole(roleId) };
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=team-management-local-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-management-local-adapter.js","sourceRoot":"","sources":["../src/team-management-local-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { newId } from \"@voyant-travel/db/lib/typeid\"\nimport {\n apikeyTable,\n authAccount,\n authSession,\n authUser,\n userInvitationsTable,\n userProfilesTable,\n} from \"@voyant-travel/db/schema/iam\"\nimport { scopesForRole } from \"@voyant-travel/types/member-roles\"\nimport { desc, eq, sql } from \"drizzle-orm\"\n\nimport type { IdentityAccessRuntimeProvider } from \"./identity-access-runtime-port.js\"\nimport { isDeactivatedProviderId, localProviderIdForStatus } from \"./local-member-access.js\"\nimport { type TeamManagementAdapter, TeamManagementError } from \"./team-management-policy.js\"\nimport type {\n CreatedTeamInvitationDto,\n InviteTeamMemberInput,\n TeamInvitationDto,\n TeamManagementCapabilitiesDto,\n TeamManagementRequestContext,\n TeamMemberDto,\n TeamRoleDto,\n} from \"./team-management-runtime-port.js\"\n\nconst LOCAL_ROLES: TeamRoleDto[] = [\n { id: \"owner\", name: \"Owner\", description: \"Full access, including ownership changes.\" },\n { id: \"admin\", name: \"Admin\", description: \"Full access to team and operator settings.\" },\n { id: \"editor\", name: \"Editor\", description: \"Can manage day-to-day operator data.\" },\n { id: \"viewer\", name: \"Viewer\", description: \"Read-only operator access.\" },\n]\n\nconst ROLE_LEVELS: Record<string, number> = { owner: 40, admin: 30, editor: 20, viewer: 10 }\n\ntype TransactionDb<TDb> = TDb extends {\n transaction<TResult>(\n callback: (transaction: infer TTransaction) => Promise<TResult>,\n ): Promise<TResult>\n}\n ? TTransaction\n : never\n\ntype LocalTeamDb =\n | TeamManagementRequestContext[\"db\"]\n | TransactionDb<TeamManagementRequestContext[\"db\"]>\ntype LocalTeamRequestContext = Omit<TeamManagementRequestContext, \"db\"> & { db: LocalTeamDb }\n\nexport { localProviderIdForStatus } from \"./local-member-access.js\"\n\nfunction roleName(roleId: string): string {\n return LOCAL_ROLES.find((role) => role.id === roleId)?.name ?? roleId\n}\n\nfunction sameScopes(left: readonly string[], right: readonly string[]) {\n return [...left].sort().join(\"\\0\") === [...right].sort().join(\"\\0\")\n}\n\nfunction resolveLocalRole(isSuperAdmin: boolean, permissions: string[] | null): string {\n if (isSuperAdmin) return \"owner\"\n if (permissions === null || permissions.includes(\"*\")) return \"admin\"\n if (sameScopes(permissions, scopesForRole(\"editor\") ?? [])) return \"editor\"\n if (sameScopes(permissions, scopesForRole(\"viewer\") ?? [])) return \"viewer\"\n return \"custom\"\n}\n\nfunction permissionsForRole(roleId: string): string[] {\n const scopes = scopesForRole(roleId)\n if (!scopes) throw new TeamManagementError(\"role_not_found\", \"Team role not found.\", 404)\n return [...scopes]\n}\n\nfunction invitationRole(metadata: Record<string, unknown> | null): string {\n const roleId = metadata?.roleId\n return typeof roleId === \"string\" && ROLE_LEVELS[roleId] !== undefined ? roleId : \"editor\"\n}\n\nfunction invitationStatus(input: {\n redeemedAt: Date | null\n expiresAt: Date\n}): TeamInvitationDto[\"status\"] {\n if (input.redeemedAt) return \"accepted\"\n if (input.expiresAt.getTime() <= Date.now()) return \"expired\"\n return \"pending\"\n}\n\nfunction randomTokenBase64Url(bytes: number): string {\n const buffer = new Uint8Array(bytes)\n crypto.getRandomValues(buffer)\n let binary = \"\"\n for (const byte of buffer) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(input))\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\")\n}\n\nasync function listLocalMembers(context: LocalTeamRequestContext): Promise<TeamMemberDto[]> {\n const [rows, accounts] = await Promise.all([\n context.db\n .select({\n id: authUser.id,\n email: authUser.email,\n authName: authUser.name,\n createdAt: authUser.createdAt,\n firstName: userProfilesTable.firstName,\n lastName: userProfilesTable.lastName,\n isSuperAdmin: userProfilesTable.isSuperAdmin,\n permissions: userProfilesTable.permissions,\n lastActiveAt: userProfilesTable.lastActiveAt,\n })\n .from(authUser)\n .leftJoin(userProfilesTable, eq(userProfilesTable.id, authUser.id)),\n context.db\n .select({ userId: authAccount.userId, providerId: authAccount.providerId })\n .from(authAccount),\n ])\n const activeUserIds = new Set(\n accounts\n .filter((account) => !isDeactivatedProviderId(account.providerId))\n .map((account) => account.userId),\n )\n\n return rows.map((row) => {\n const roleId = resolveLocalRole(row.isSuperAdmin ?? false, row.permissions ?? null)\n const profileName = [row.firstName, row.lastName].filter(Boolean).join(\" \").trim()\n return {\n id: row.id,\n email: row.email,\n name: profileName || row.authName || null,\n roleId,\n roleName: roleName(roleId),\n status: activeUserIds.has(row.id) ? \"active\" : \"deactivated\",\n joinedAt: row.createdAt?.toISOString() ?? null,\n lastActivityAt: row.lastActiveAt?.toISOString() ?? null,\n }\n })\n}\n\nfunction memberOrThrow(members: TeamMemberDto[], memberId: string): TeamMemberDto {\n const member = members.find((candidate) => candidate.id === memberId)\n if (!member) throw new TeamManagementError(\"member_not_found\", \"Team member not found.\", 404)\n return member\n}\n\nfunction assertActiveOwnerRemains(members: TeamMemberDto[], target: TeamMemberDto): void {\n if (target.status !== \"active\" || target.roleId !== \"owner\") return\n const activeOwnerCount = members.filter(\n (member) => member.status === \"active\" && member.roleId === \"owner\",\n ).length\n if (activeOwnerCount <= 1) {\n throw new TeamManagementError(\n \"last_owner\",\n \"The last active owner cannot be demoted or deactivated.\",\n 409,\n )\n }\n}\n\nasync function withLockedLocalTeam<T>(\n context: TeamManagementRequestContext,\n operation: (transactionContext: LocalTeamRequestContext) => Promise<T>,\n): Promise<T> {\n return context.db.transaction(async (transaction) => {\n // agent-quality: raw-sql reviewed -- owner: auth; table identifiers are static Drizzle schema objects.\n await transaction.execute(\n sql`SELECT ${authUser.id} FROM ${authUser} ORDER BY ${authUser.id} FOR UPDATE`,\n )\n return operation({ ...context, db: transaction })\n })\n}\n\nexport function createLocalTeamManagementAdapter(\n identityAccess: IdentityAccessRuntimeProvider,\n): TeamManagementAdapter {\n return {\n async getActor(context) {\n const actor = (await listLocalMembers(context)).find((member) => member.id === context.userId)\n if (actor?.status !== \"active\") {\n throw new TeamManagementError(\"forbidden\", \"The current user cannot manage this team.\")\n }\n return { memberId: actor.id, roleId: actor.roleId }\n },\n async getCapabilities(_context, actor): Promise<TeamManagementCapabilitiesDto> {\n const canManage = actor.roleId === \"owner\" || actor.roleId === \"admin\"\n return {\n viewRoster: true,\n inviteMembers: canManage,\n manageRoles: canManage,\n activateMembers: canManage,\n deactivateMembers: canManage,\n revokeInvitations: canManage,\n }\n },\n listMembers: listLocalMembers,\n async listRoles() {\n return LOCAL_ROLES\n },\n async listInvitations(context) {\n const rows = await context.db\n .select({\n id: userInvitationsTable.id,\n email: userInvitationsTable.email,\n metadata: userInvitationsTable.metadata,\n expiresAt: userInvitationsTable.expiresAt,\n redeemedAt: userInvitationsTable.redeemedAt,\n createdAt: userInvitationsTable.createdAt,\n })\n .from(userInvitationsTable)\n .orderBy(desc(userInvitationsTable.createdAt))\n .limit(100)\n return rows.map((row) => {\n const roleId = invitationRole(row.metadata)\n return {\n id: row.id,\n email: row.email,\n roleId,\n roleName: roleName(roleId),\n status: invitationStatus(row),\n createdAt: row.createdAt.toISOString(),\n expiresAt: row.expiresAt.toISOString(),\n }\n })\n },\n async inviteMember(context, input: InviteTeamMemberInput): Promise<CreatedTeamInvitationDto> {\n const email = input.email.trim().toLowerCase()\n const [existingUser] = await context.db\n .select({ id: authUser.id })\n .from(authUser)\n .where(eq(authUser.email, email))\n .limit(1)\n if (existingUser) {\n throw new TeamManagementError(\n \"forbidden\",\n \"A team member with this email already exists.\",\n 409,\n )\n }\n\n const rawToken = randomTokenBase64Url(32)\n const expiresInDays = input.expiresInDays ?? 3\n const expiresAt = new Date(Date.now() + expiresInDays * 86_400_000)\n const now = new Date()\n const id = newId(\"user_invitations\")\n await context.db.insert(userInvitationsTable).values({\n id,\n email,\n tokenHash: await sha256Hex(rawToken),\n expiresAt,\n createdBy: context.userId,\n metadata: { roleId: input.roleId },\n createdAt: now,\n })\n\n const { appUrl } = identityAccess.resolveDeployment(context.bindings)\n const acceptUrl = `${appUrl}/accept-invite?token=${encodeURIComponent(rawToken)}`\n await identityAccess.sendInvitationEmail(context.bindings, {\n acceptUrl,\n expiresInHours: expiresInDays * 24,\n to: email,\n })\n return {\n id,\n email,\n roleId: input.roleId,\n roleName: roleName(input.roleId),\n status: \"pending\",\n createdAt: now.toISOString(),\n expiresAt: expiresAt.toISOString(),\n acceptUrl,\n }\n },\n async revokeInvitation(context, invitationId) {\n await context.db.delete(userInvitationsTable).where(eq(userInvitationsTable.id, invitationId))\n },\n async updateMemberRole(context, memberId, roleId) {\n return withLockedLocalTeam(context, async (transactionContext) => {\n const members = await listLocalMembers(transactionContext)\n const target = memberOrThrow(members, memberId)\n if (roleId !== \"owner\") assertActiveOwnerRemains(members, target)\n\n await transactionContext.db\n .update(userProfilesTable)\n .set({\n isSuperAdmin: roleId === \"owner\",\n permissions: permissionsForRole(roleId),\n updatedAt: new Date(),\n })\n .where(eq(userProfilesTable.id, memberId))\n return memberOrThrow(await listLocalMembers(transactionContext), memberId)\n })\n },\n async deactivateMember(context, memberId) {\n return withLockedLocalTeam(context, async (transactionContext) => {\n const members = await listLocalMembers(transactionContext)\n assertActiveOwnerRemains(members, memberOrThrow(members, memberId))\n\n await transactionContext.db.delete(authSession).where(eq(authSession.userId, memberId))\n await transactionContext.db.delete(apikeyTable).where(eq(apikeyTable.referenceId, memberId))\n const accounts = await transactionContext.db\n .select({ id: authAccount.id, providerId: authAccount.providerId })\n .from(authAccount)\n .where(eq(authAccount.userId, memberId))\n for (const account of accounts) {\n await transactionContext.db\n .update(authAccount)\n .set({ providerId: localProviderIdForStatus(account.providerId, \"deactivated\") })\n .where(eq(authAccount.id, account.id))\n }\n return memberOrThrow(await listLocalMembers(transactionContext), memberId)\n })\n },\n async activateMember(context, memberId) {\n return withLockedLocalTeam(context, async (transactionContext) => {\n memberOrThrow(await listLocalMembers(transactionContext), memberId)\n const accounts = await transactionContext.db\n .select({ id: authAccount.id, providerId: authAccount.providerId })\n .from(authAccount)\n .where(eq(authAccount.userId, memberId))\n for (const account of accounts) {\n await transactionContext.db\n .update(authAccount)\n .set({ providerId: localProviderIdForStatus(account.providerId, \"active\") })\n .where(eq(authAccount.id, account.id))\n }\n const member = memberOrThrow(await listLocalMembers(transactionContext), memberId)\n if (member.status !== \"active\") {\n throw new TeamManagementError(\n \"activation_unavailable\",\n \"This local member has no sign-in account to reactivate.\",\n 409,\n )\n }\n return member\n })\n },\n roleLevel(roleId) {\n return ROLE_LEVELS[roleId] ?? 0\n },\n isOwnerRole(roleId) {\n return roleId === \"owner\"\n },\n }\n}\n\nexport function localRoleForRedeemedInvitation(metadata: Record<string, unknown> | null): {\n isSuperAdmin: boolean\n permissions: string[]\n} {\n const roleId = invitationRole(metadata)\n return { isSuperAdmin: roleId === \"owner\", permissions: permissionsForRole(roleId) }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CreatedTeamInvitationDto, InviteTeamMemberInput, TeamInvitationDto, TeamManagementCapabilitiesDto, TeamManagementRequestContext, TeamManagementRuntimeProvider, TeamMemberDto, TeamRoleDto } from "./team-management-runtime-port.js";
|
|
2
|
+
export type TeamManagementErrorCode = "forbidden" | "member_not_found" | "role_not_found" | "self_change_forbidden" | "privilege_escalation" | "last_owner" | "activation_unavailable" | "not_configured";
|
|
3
|
+
export declare class TeamManagementError extends Error {
|
|
4
|
+
readonly code: TeamManagementErrorCode;
|
|
5
|
+
readonly status: 400 | 403 | 404 | 409 | 501;
|
|
6
|
+
constructor(code: TeamManagementErrorCode, message: string, status?: 400 | 403 | 404 | 409 | 501);
|
|
7
|
+
}
|
|
8
|
+
export interface TeamManagementActor {
|
|
9
|
+
memberId: string;
|
|
10
|
+
roleId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TeamManagementAdapter {
|
|
13
|
+
getActor(context: TeamManagementRequestContext): Promise<TeamManagementActor>;
|
|
14
|
+
getCapabilities(context: TeamManagementRequestContext, actor: TeamManagementActor): Promise<TeamManagementCapabilitiesDto>;
|
|
15
|
+
listMembers(context: TeamManagementRequestContext): Promise<TeamMemberDto[]>;
|
|
16
|
+
listRoles(context: TeamManagementRequestContext): Promise<TeamRoleDto[]>;
|
|
17
|
+
listInvitations(context: TeamManagementRequestContext): Promise<TeamInvitationDto[]>;
|
|
18
|
+
inviteMember(context: TeamManagementRequestContext, input: InviteTeamMemberInput): Promise<CreatedTeamInvitationDto>;
|
|
19
|
+
revokeInvitation(context: TeamManagementRequestContext, invitationId: string): Promise<void>;
|
|
20
|
+
updateMemberRole(context: TeamManagementRequestContext, memberId: string, roleId: string): Promise<TeamMemberDto>;
|
|
21
|
+
activateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>;
|
|
22
|
+
deactivateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>;
|
|
23
|
+
roleLevel(roleId: string): number;
|
|
24
|
+
isOwnerRole(roleId: string): boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Applies provider-neutral authorization and invariants before invoking an adapter. */
|
|
27
|
+
export declare function createGuardedTeamManagementProvider(resolveAdapter: (context: TeamManagementRequestContext) => TeamManagementAdapter): TeamManagementRuntimeProvider;
|