@voyant-travel/auth 0.129.0 → 0.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +14 -0
  2. package/dist/cloud-broker/members.d.ts +9 -0
  3. package/dist/cloud-broker/members.js +10 -0
  4. package/dist/cloud-broker/members.js.map +1 -1
  5. package/dist/cloud-broker.d.ts +1 -1
  6. package/dist/cloud-broker.js +1 -1
  7. package/dist/cloud-broker.js.map +1 -1
  8. package/dist/identity-access-graph-runtime.js +2 -1
  9. package/dist/identity-access-graph-runtime.js.map +1 -1
  10. package/dist/invitations-routes.js +4 -0
  11. package/dist/invitations-routes.js.map +1 -1
  12. package/dist/local-member-access.d.ts +15 -0
  13. package/dist/local-member-access.js +119 -0
  14. package/dist/local-member-access.js.map +1 -0
  15. package/dist/mcp-runtime.d.ts +9 -0
  16. package/dist/mcp-runtime.js +38 -0
  17. package/dist/mcp-runtime.js.map +1 -0
  18. package/dist/runtime-contributor.js +18 -1
  19. package/dist/runtime-contributor.js.map +1 -1
  20. package/dist/server.d.ts +10 -6
  21. package/dist/server.js +6 -3
  22. package/dist/server.js.map +1 -1
  23. package/dist/team-management-cloud-adapter.d.ts +7 -0
  24. package/dist/team-management-cloud-adapter.js +146 -0
  25. package/dist/team-management-cloud-adapter.js.map +1 -0
  26. package/dist/team-management-local-adapter.d.ts +8 -0
  27. package/dist/team-management-local-adapter.js +285 -0
  28. package/dist/team-management-local-adapter.js.map +1 -0
  29. package/dist/team-management-policy.d.ts +27 -0
  30. package/dist/team-management-policy.js +109 -0
  31. package/dist/team-management-policy.js.map +1 -0
  32. package/dist/team-management-runtime-port.d.ts +60 -0
  33. package/dist/team-management-runtime-port.js +25 -0
  34. package/dist/team-management-runtime-port.js.map +1 -0
  35. package/dist/team-routes.d.ts +3 -3
  36. package/dist/team-routes.js +89 -138
  37. package/dist/team-routes.js.map +1 -1
  38. package/dist/tools.d.ts +58 -0
  39. package/dist/tools.js +226 -0
  40. package/dist/tools.js.map +1 -0
  41. package/dist/voyant.js +262 -7
  42. package/dist/voyant.js.map +1 -1
  43. package/openapi/admin/team.json +42 -74
  44. package/package.json +15 -4
@@ -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":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACpD,OAAO,EACL,WAAW,EACX,WAAW,EACX,WAAW,EACX,QAAQ,EACR,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAA;AACjE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAG3C,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AAC5F,OAAO,EAA8B,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAW7F,MAAM,WAAW,GAAkB;IACjC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,2CAA2C,EAAE;IACxF,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,4CAA4C,EAAE;IACzF,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrF,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;CAC5E,CAAA;AAED,MAAM,WAAW,GAA2B,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AAe5F,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AAEnE,SAAS,QAAQ,CAAC,MAAc;IAC9B,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,IAAI,IAAI,MAAM,CAAA;AACvE,CAAC;AAED,SAAS,UAAU,CAAC,IAAuB,EAAE,KAAwB;IACnE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACrE,CAAC;AAED,SAAS,gBAAgB,CAAC,YAAqB,EAAE,WAA4B;IAC3E,IAAI,YAAY;QAAE,OAAO,OAAO,CAAA;IAChC,IAAI,WAAW,KAAK,IAAI,IAAI,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAA;IACrE,IAAI,UAAU,CAAC,WAAW,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAAE,OAAO,QAAQ,CAAA;IAC3E,IAAI,UAAU,CAAC,WAAW,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAAE,OAAO,QAAQ,CAAA;IAC3E,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAc;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,mBAAmB,CAAC,gBAAgB,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAA;IACzF,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;AACpB,CAAC;AAED,SAAS,cAAc,CAAC,QAAwC;IAC9D,MAAM,MAAM,GAAG,QAAQ,EAAE,MAAM,CAAA;IAC/B,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC5F,CAAC;AAED,SAAS,gBAAgB,CAAC,KAGzB;IACC,IAAI,KAAK,CAAC,UAAU;QAAE,OAAO,UAAU,CAAA;IACvC,IAAI,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,SAAS,CAAA;IAC7D,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa;IACzC,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;IAC9B,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,KAAK,MAAM,IAAI,IAAI,MAAM;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAChF,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,KAAa;IACpC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IACrF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC,CAAA;AACb,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,OAAgC;IAC9D,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACzC,OAAO,CAAC,EAAE;aACP,MAAM,CAAC;YACN,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,QAAQ,EAAE,QAAQ,CAAC,IAAI;YACvB,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,SAAS,EAAE,iBAAiB,CAAC,SAAS;YACtC,QAAQ,EAAE,iBAAiB,CAAC,QAAQ;YACpC,YAAY,EAAE,iBAAiB,CAAC,YAAY;YAC5C,WAAW,EAAE,iBAAiB,CAAC,WAAW;YAC1C,YAAY,EAAE,iBAAiB,CAAC,YAAY;SAC7C,CAAC;aACD,IAAI,CAAC,QAAQ,CAAC;aACd,QAAQ,CAAC,iBAAiB,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrE,OAAO,CAAC,EAAE;aACP,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,CAAC,UAAU,EAAE,CAAC;aAC1E,IAAI,CAAC,WAAW,CAAC;KACrB,CAAC,CAAA;IACF,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,QAAQ;SACL,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,uBAAuB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;SACjE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CACpC,CAAA;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACtB,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,YAAY,IAAI,KAAK,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAA;QACnF,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;QAClF,OAAO;YACL,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,IAAI,EAAE,WAAW,IAAI,GAAG,CAAC,QAAQ,IAAI,IAAI;YACzC,MAAM;YACN,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC;YAC1B,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa;YAC5D,QAAQ,EAAE,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI;YAC9C,cAAc,EAAE,GAAG,CAAC,YAAY,EAAE,WAAW,EAAE,IAAI,IAAI;SACxD,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,OAAwB,EAAE,QAAgB;IAC/D,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAA;IACrE,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,wBAAwB,EAAE,GAAG,CAAC,CAAA;IAC7F,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,wBAAwB,CAAC,OAAwB,EAAE,MAAqB;IAC/E,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO;QAAE,OAAM;IACnE,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CACrC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,CACpE,CAAC,MAAM,CAAA;IACR,IAAI,gBAAgB,IAAI,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,mBAAmB,CAC3B,YAAY,EACZ,yDAAyD,EACzD,GAAG,CACJ,CAAA;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,OAAqC,EACrC,SAAsE;IAEtE,OAAO,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE;QAClD,uGAAuG;QACvG,MAAM,WAAW,CAAC,OAAO,CACvB,GAAG,CAAA,UAAU,QAAQ,CAAC,EAAE,SAAS,QAAQ,aAAa,QAAQ,CAAC,EAAE,aAAa,CAC/E,CAAA;QACD,OAAO,SAAS,CAAC,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,cAA6C;IAE7C,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,OAAO;YACpB,MAAM,KAAK,GAAG,CAAC,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;YAC9F,IAAI,KAAK,EAAE,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC/B,MAAM,IAAI,mBAAmB,CAAC,WAAW,EAAE,2CAA2C,CAAC,CAAA;YACzF,CAAC;YACD,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAA;QACrD,CAAC;QACD,KAAK,CAAC,eAAe,CAAC,QAAQ,EAAE,KAAK;YACnC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,KAAK,OAAO,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,CAAA;YACtE,OAAO;gBACL,UAAU,EAAE,IAAI;gBAChB,aAAa,EAAE,SAAS;gBACxB,WAAW,EAAE,SAAS;gBACtB,eAAe,EAAE,SAAS;gBAC1B,iBAAiB,EAAE,SAAS;gBAC5B,iBAAiB,EAAE,SAAS;aAC7B,CAAA;QACH,CAAC;QACD,WAAW,EAAE,gBAAgB;QAC7B,KAAK,CAAC,SAAS;YACb,OAAO,WAAW,CAAA;QACpB,CAAC;QACD,KAAK,CAAC,eAAe,CAAC,OAAO;YAC3B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,EAAE;iBAC1B,MAAM,CAAC;gBACN,EAAE,EAAE,oBAAoB,CAAC,EAAE;gBAC3B,KAAK,EAAE,oBAAoB,CAAC,KAAK;gBACjC,QAAQ,EAAE,oBAAoB,CAAC,QAAQ;gBACvC,SAAS,EAAE,oBAAoB,CAAC,SAAS;gBACzC,UAAU,EAAE,oBAAoB,CAAC,UAAU;gBAC3C,SAAS,EAAE,oBAAoB,CAAC,SAAS;aAC1C,CAAC;iBACD,IAAI,CAAC,oBAAoB,CAAC;iBAC1B,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC;iBAC7C,KAAK,CAAC,GAAG,CAAC,CAAA;YACb,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBACtB,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;gBAC3C,OAAO;oBACL,EAAE,EAAE,GAAG,CAAC,EAAE;oBACV,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,MAAM;oBACN,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC;oBAC1B,MAAM,EAAE,gBAAgB,CAAC,GAAG,CAAC;oBAC7B,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;oBACtC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;iBACvC,CAAA;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,KAA4B;YACtD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;YAC9C,MAAM,CAAC,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;iBACpC,MAAM,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;iBAC3B,IAAI,CAAC,QAAQ,CAAC;iBACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;iBAChC,KAAK,CAAC,CAAC,CAAC,CAAA;YACX,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,mBAAmB,CAC3B,WAAW,EACX,+CAA+C,EAC/C,GAAG,CACJ,CAAA;YACH,CAAC;YAED,MAAM,QAAQ,GAAG,oBAAoB,CAAC,EAAE,CAAC,CAAA;YACzC,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,IAAI,CAAC,CAAA;YAC9C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,GAAG,UAAU,CAAC,CAAA;YACnE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;YACtB,MAAM,EAAE,GAAG,KAAK,CAAC,kBAAkB,CAAC,CAAA;YACpC,MAAM,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,MAAM,CAAC;gBACnD,EAAE;gBACF,KAAK;gBACL,SAAS,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC;gBACpC,SAAS;gBACT,SAAS,EAAE,OAAO,CAAC,MAAM;gBACzB,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE;gBAClC,SAAS,EAAE,GAAG;aACf,CAAC,CAAA;YAEF,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YACrE,MAAM,SAAS,GAAG,GAAG,MAAM,wBAAwB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAA;YACjF,MAAM,cAAc,CAAC,mBAAmB,CAAC,OAAO,CAAC,QAAQ,EAAE;gBACzD,SAAS;gBACT,cAAc,EAAE,aAAa,GAAG,EAAE;gBAClC,EAAE,EAAE,KAAK;aACV,CAAC,CAAA;YACF,OAAO;gBACL,EAAE;gBACF,KAAK;gBACL,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;gBAChC,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;gBAC5B,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;gBAClC,SAAS;aACV,CAAA;QACH,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY;YAC1C,MAAM,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAA;QAChG,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM;YAC9C,OAAO,mBAAmB,CAAC,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE;gBAC/D,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;gBAC1D,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;gBAC/C,IAAI,MAAM,KAAK,OAAO;oBAAE,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;gBAEjE,MAAM,kBAAkB,CAAC,EAAE;qBACxB,MAAM,CAAC,iBAAiB,CAAC;qBACzB,GAAG,CAAC;oBACH,YAAY,EAAE,MAAM,KAAK,OAAO;oBAChC,WAAW,EAAE,kBAAkB,CAAC,MAAM,CAAC;oBACvC,SAAS,EAAE,IAAI,IAAI,EAAE;iBACtB,CAAC;qBACD,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAA;gBAC5C,OAAO,aAAa,CAAC,MAAM,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,CAAA;YAC5E,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,QAAQ;YACtC,OAAO,mBAAmB,CAAC,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE;gBAC/D,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;gBAC1D,wBAAwB,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;gBAEnE,MAAM,kBAAkB,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;gBACvF,MAAM,kBAAkB,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAA;gBAC5F,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,EAAE;qBACzC,MAAM,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,EAAE,WAAW,CAAC,UAAU,EAAE,CAAC;qBAClE,IAAI,CAAC,WAAW,CAAC;qBACjB,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;gBAC1C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAC/B,MAAM,kBAAkB,CAAC,EAAE;yBACxB,MAAM,CAAC,WAAW,CAAC;yBACnB,GAAG,CAAC,EAAE,UAAU,EAAE,wBAAwB,CAAC,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,EAAE,CAAC;yBAChF,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC1C,CAAC;gBACD,OAAO,aAAa,CAAC,MAAM,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,CAAA;YAC5E,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ;YACpC,OAAO,mBAAmB,CAAC,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE;gBAC/D,aAAa,CAAC,MAAM,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,CAAA;gBACnE,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,EAAE;qBACzC,MAAM,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,EAAE,WAAW,CAAC,UAAU,EAAE,CAAC;qBAClE,IAAI,CAAC,WAAW,CAAC;qBACjB,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;gBAC1C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAC/B,MAAM,kBAAkB,CAAC,EAAE;yBACxB,MAAM,CAAC,WAAW,CAAC;yBACnB,GAAG,CAAC,EAAE,UAAU,EAAE,wBAAwB,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;yBAC3E,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC1C,CAAC;gBACD,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,CAAA;gBAClF,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;oBAC/B,MAAM,IAAI,mBAAmB,CAC3B,wBAAwB,EACxB,yDAAyD,EACzD,GAAG,CACJ,CAAA;gBACH,CAAC;gBACD,OAAO,MAAM,CAAA;YACf,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,SAAS,CAAC,MAAM;YACd,OAAO,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjC,CAAC;QACD,WAAW,CAAC,MAAM;YAChB,OAAO,MAAM,KAAK,OAAO,CAAA;QAC3B,CAAC;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,QAAwC;IAIrF,MAAM,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAA;IACvC,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,EAAE,WAAW,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAA;AACtF,CAAC","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;
@@ -0,0 +1,109 @@
1
+ export class TeamManagementError extends Error {
2
+ code;
3
+ status;
4
+ constructor(code, message, status = 403) {
5
+ super(message);
6
+ this.code = code;
7
+ this.status = status;
8
+ this.name = "TeamManagementError";
9
+ }
10
+ }
11
+ function requireCapability(capabilities, capability) {
12
+ if (!capabilities[capability]) {
13
+ throw new TeamManagementError("forbidden", "This team-management action is not allowed.");
14
+ }
15
+ }
16
+ async function authorize(adapter, context, capability) {
17
+ const actor = await adapter.getActor(context);
18
+ const capabilities = await adapter.getCapabilities(context, actor);
19
+ requireCapability(capabilities, capability);
20
+ return { actor, capabilities };
21
+ }
22
+ async function mutationState(adapter, context, capability, memberId) {
23
+ const { actor } = await authorize(adapter, context, capability);
24
+ const members = await adapter.listMembers(context);
25
+ const target = members.find((member) => member.id === memberId);
26
+ if (!target) {
27
+ throw new TeamManagementError("member_not_found", "Team member not found.", 404);
28
+ }
29
+ if (target.id === actor.memberId) {
30
+ throw new TeamManagementError("self_change_forbidden", "You cannot change your own role or access.", 409);
31
+ }
32
+ if (adapter.roleLevel(target.roleId) > adapter.roleLevel(actor.roleId)) {
33
+ throw new TeamManagementError("privilege_escalation", "You cannot manage a member with a more privileged role.");
34
+ }
35
+ return { actor, members, target };
36
+ }
37
+ function assertAssignableRole(adapter, actor, roles, roleId) {
38
+ if (!roles.some((role) => role.id === roleId)) {
39
+ throw new TeamManagementError("role_not_found", "Team role not found.", 404);
40
+ }
41
+ if (adapter.roleLevel(roleId) > adapter.roleLevel(actor.roleId)) {
42
+ throw new TeamManagementError("privilege_escalation", "You cannot assign a role above your own privilege level.");
43
+ }
44
+ }
45
+ function assertOwnerRemains(adapter, members, target) {
46
+ if (!adapter.isOwnerRole(target.roleId))
47
+ return;
48
+ const activeOwners = members.filter((member) => member.status === "active" && adapter.isOwnerRole(member.roleId));
49
+ if (activeOwners.length <= 1) {
50
+ throw new TeamManagementError("last_owner", "The last active owner cannot be demoted or deactivated.", 409);
51
+ }
52
+ }
53
+ /** Applies provider-neutral authorization and invariants before invoking an adapter. */
54
+ export function createGuardedTeamManagementProvider(resolveAdapter) {
55
+ return {
56
+ async getCapabilities(context) {
57
+ const adapter = resolveAdapter(context);
58
+ const actor = await adapter.getActor(context);
59
+ return adapter.getCapabilities(context, actor);
60
+ },
61
+ async listMembers(context) {
62
+ const adapter = resolveAdapter(context);
63
+ await authorize(adapter, context, "viewRoster");
64
+ return adapter.listMembers(context);
65
+ },
66
+ async listRoles(context) {
67
+ const adapter = resolveAdapter(context);
68
+ await authorize(adapter, context, "viewRoster");
69
+ return adapter.listRoles(context);
70
+ },
71
+ async listInvitations(context) {
72
+ const adapter = resolveAdapter(context);
73
+ await authorize(adapter, context, "viewRoster");
74
+ return adapter.listInvitations(context);
75
+ },
76
+ async inviteMember(context, input) {
77
+ const adapter = resolveAdapter(context);
78
+ const { actor } = await authorize(adapter, context, "inviteMembers");
79
+ assertAssignableRole(adapter, actor, await adapter.listRoles(context), input.roleId);
80
+ return adapter.inviteMember(context, input);
81
+ },
82
+ async revokeInvitation(context, invitationId) {
83
+ const adapter = resolveAdapter(context);
84
+ await authorize(adapter, context, "revokeInvitations");
85
+ return adapter.revokeInvitation(context, invitationId);
86
+ },
87
+ async updateMemberRole(context, memberId, roleId) {
88
+ const adapter = resolveAdapter(context);
89
+ const { actor, members, target } = await mutationState(adapter, context, "manageRoles", memberId);
90
+ assertAssignableRole(adapter, actor, await adapter.listRoles(context), roleId);
91
+ if (adapter.isOwnerRole(target.roleId) && !adapter.isOwnerRole(roleId)) {
92
+ assertOwnerRemains(adapter, members, target);
93
+ }
94
+ return adapter.updateMemberRole(context, memberId, roleId);
95
+ },
96
+ async deactivateMember(context, memberId) {
97
+ const adapter = resolveAdapter(context);
98
+ const { members, target } = await mutationState(adapter, context, "deactivateMembers", memberId);
99
+ assertOwnerRemains(adapter, members, target);
100
+ return adapter.deactivateMember(context, memberId);
101
+ },
102
+ async activateMember(context, memberId) {
103
+ const adapter = resolveAdapter(context);
104
+ await mutationState(adapter, context, "activateMembers", memberId);
105
+ return adapter.activateMember(context, memberId);
106
+ },
107
+ };
108
+ }
109
+ //# sourceMappingURL=team-management-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-management-policy.js","sourceRoot":"","sources":["../src/team-management-policy.ts"],"names":[],"mappings":"AAqBA,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAEjC;IAEA;IAHX,YACW,IAA6B,EACtC,OAAe,EACN,SAAsC,GAAG;QAElD,KAAK,CAAC,OAAO,CAAC,CAAA;QAJL,SAAI,GAAJ,IAAI,CAAyB;QAE7B,WAAM,GAAN,MAAM,CAAmC;QAGlD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF;AAkCD,SAAS,iBAAiB,CAAC,YAA2C,EAAE,UAAsB;IAC5F,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,mBAAmB,CAAC,WAAW,EAAE,6CAA6C,CAAC,CAAA;IAC3F,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS,CACtB,OAA8B,EAC9B,OAAqC,EACrC,UAAsB;IAEtB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAClE,iBAAiB,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAC3C,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAA;AAChC,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,OAA8B,EAC9B,OAAqC,EACrC,UAAsB,EACtB,QAAgB;IAEhB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAA;IAC/D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAA;IAC/D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,wBAAwB,EAAE,GAAG,CAAC,CAAA;IAClF,CAAC;IACD,IAAI,MAAM,CAAC,EAAE,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,mBAAmB,CAC3B,uBAAuB,EACvB,4CAA4C,EAC5C,GAAG,CACJ,CAAA;IACH,CAAC;IACD,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,mBAAmB,CAC3B,sBAAsB,EACtB,yDAAyD,CAC1D,CAAA;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AACnC,CAAC;AAED,SAAS,oBAAoB,CAC3B,OAA8B,EAC9B,KAA0B,EAC1B,KAAoB,EACpB,MAAc;IAEd,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,mBAAmB,CAAC,gBAAgB,EAAE,sBAAsB,EAAE,GAAG,CAAC,CAAA;IAC9E,CAAC;IACD,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,mBAAmB,CAC3B,sBAAsB,EACtB,0DAA0D,CAC3D,CAAA;IACH,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CACzB,OAA8B,EAC9B,OAAwB,EACxB,MAAqB;IAErB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;QAAE,OAAM;IAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CACjC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAC7E,CAAA;IACD,IAAI,YAAY,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,mBAAmB,CAC3B,YAAY,EACZ,yDAAyD,EACzD,GAAG,CACJ,CAAA;IACH,CAAC;AACH,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,mCAAmC,CACjD,cAAgF;IAEhF,OAAO;QACL,KAAK,CAAC,eAAe,CAAC,OAAO;YAC3B,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;YAC7C,OAAO,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAChD,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,OAAO;YACvB,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;YAC/C,OAAO,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QACrC,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;YAC/C,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;QACnC,CAAC;QACD,KAAK,CAAC,eAAe,CAAC,OAAO;YAC3B,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;YAC/C,OAAO,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;QACzC,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK;YAC/B,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,eAAe,CAAC,CAAA;YACpE,oBAAoB,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;YACpF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAC7C,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY;YAC1C,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAA;YACtD,OAAO,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QACxD,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM;YAC9C,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CACpD,OAAO,EACP,OAAO,EACP,aAAa,EACb,QAAQ,CACT,CAAA;YACD,oBAAoB,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAA;YAC9E,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvE,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;YAC9C,CAAC;YACD,OAAO,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;QAC5D,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,QAAQ;YACtC,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAC7C,OAAO,EACP,OAAO,EACP,mBAAmB,EACnB,QAAQ,CACT,CAAA;YACD,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;YAC5C,OAAO,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QACpD,CAAC;QACD,KAAK,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ;YACpC,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,QAAQ,CAAC,CAAA;YAClE,OAAO,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QAClD,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type {\n CreatedTeamInvitationDto,\n InviteTeamMemberInput,\n TeamInvitationDto,\n TeamManagementCapabilitiesDto,\n TeamManagementRequestContext,\n TeamManagementRuntimeProvider,\n TeamMemberDto,\n TeamRoleDto,\n} from \"./team-management-runtime-port.js\"\n\nexport type TeamManagementErrorCode =\n | \"forbidden\"\n | \"member_not_found\"\n | \"role_not_found\"\n | \"self_change_forbidden\"\n | \"privilege_escalation\"\n | \"last_owner\"\n | \"activation_unavailable\"\n | \"not_configured\"\n\nexport class TeamManagementError extends Error {\n constructor(\n readonly code: TeamManagementErrorCode,\n message: string,\n readonly status: 400 | 403 | 404 | 409 | 501 = 403,\n ) {\n super(message)\n this.name = \"TeamManagementError\"\n }\n}\n\nexport interface TeamManagementActor {\n memberId: string\n roleId: string\n}\n\nexport interface TeamManagementAdapter {\n getActor(context: TeamManagementRequestContext): Promise<TeamManagementActor>\n getCapabilities(\n context: TeamManagementRequestContext,\n actor: TeamManagementActor,\n ): Promise<TeamManagementCapabilitiesDto>\n listMembers(context: TeamManagementRequestContext): Promise<TeamMemberDto[]>\n listRoles(context: TeamManagementRequestContext): Promise<TeamRoleDto[]>\n listInvitations(context: TeamManagementRequestContext): Promise<TeamInvitationDto[]>\n inviteMember(\n context: TeamManagementRequestContext,\n input: InviteTeamMemberInput,\n ): Promise<CreatedTeamInvitationDto>\n revokeInvitation(context: TeamManagementRequestContext, invitationId: string): Promise<void>\n updateMemberRole(\n context: TeamManagementRequestContext,\n memberId: string,\n roleId: string,\n ): Promise<TeamMemberDto>\n activateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>\n deactivateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>\n roleLevel(roleId: string): number\n isOwnerRole(roleId: string): boolean\n}\n\ntype Capability = keyof TeamManagementCapabilitiesDto\n\nfunction requireCapability(capabilities: TeamManagementCapabilitiesDto, capability: Capability) {\n if (!capabilities[capability]) {\n throw new TeamManagementError(\"forbidden\", \"This team-management action is not allowed.\")\n }\n}\n\nasync function authorize(\n adapter: TeamManagementAdapter,\n context: TeamManagementRequestContext,\n capability: Capability,\n) {\n const actor = await adapter.getActor(context)\n const capabilities = await adapter.getCapabilities(context, actor)\n requireCapability(capabilities, capability)\n return { actor, capabilities }\n}\n\nasync function mutationState(\n adapter: TeamManagementAdapter,\n context: TeamManagementRequestContext,\n capability: Capability,\n memberId: string,\n) {\n const { actor } = await authorize(adapter, context, capability)\n const members = await adapter.listMembers(context)\n const target = members.find((member) => member.id === memberId)\n if (!target) {\n throw new TeamManagementError(\"member_not_found\", \"Team member not found.\", 404)\n }\n if (target.id === actor.memberId) {\n throw new TeamManagementError(\n \"self_change_forbidden\",\n \"You cannot change your own role or access.\",\n 409,\n )\n }\n if (adapter.roleLevel(target.roleId) > adapter.roleLevel(actor.roleId)) {\n throw new TeamManagementError(\n \"privilege_escalation\",\n \"You cannot manage a member with a more privileged role.\",\n )\n }\n return { actor, members, target }\n}\n\nfunction assertAssignableRole(\n adapter: TeamManagementAdapter,\n actor: TeamManagementActor,\n roles: TeamRoleDto[],\n roleId: string,\n) {\n if (!roles.some((role) => role.id === roleId)) {\n throw new TeamManagementError(\"role_not_found\", \"Team role not found.\", 404)\n }\n if (adapter.roleLevel(roleId) > adapter.roleLevel(actor.roleId)) {\n throw new TeamManagementError(\n \"privilege_escalation\",\n \"You cannot assign a role above your own privilege level.\",\n )\n }\n}\n\nfunction assertOwnerRemains(\n adapter: TeamManagementAdapter,\n members: TeamMemberDto[],\n target: TeamMemberDto,\n) {\n if (!adapter.isOwnerRole(target.roleId)) return\n const activeOwners = members.filter(\n (member) => member.status === \"active\" && adapter.isOwnerRole(member.roleId),\n )\n if (activeOwners.length <= 1) {\n throw new TeamManagementError(\n \"last_owner\",\n \"The last active owner cannot be demoted or deactivated.\",\n 409,\n )\n }\n}\n\n/** Applies provider-neutral authorization and invariants before invoking an adapter. */\nexport function createGuardedTeamManagementProvider(\n resolveAdapter: (context: TeamManagementRequestContext) => TeamManagementAdapter,\n): TeamManagementRuntimeProvider {\n return {\n async getCapabilities(context) {\n const adapter = resolveAdapter(context)\n const actor = await adapter.getActor(context)\n return adapter.getCapabilities(context, actor)\n },\n async listMembers(context) {\n const adapter = resolveAdapter(context)\n await authorize(adapter, context, \"viewRoster\")\n return adapter.listMembers(context)\n },\n async listRoles(context) {\n const adapter = resolveAdapter(context)\n await authorize(adapter, context, \"viewRoster\")\n return adapter.listRoles(context)\n },\n async listInvitations(context) {\n const adapter = resolveAdapter(context)\n await authorize(adapter, context, \"viewRoster\")\n return adapter.listInvitations(context)\n },\n async inviteMember(context, input) {\n const adapter = resolveAdapter(context)\n const { actor } = await authorize(adapter, context, \"inviteMembers\")\n assertAssignableRole(adapter, actor, await adapter.listRoles(context), input.roleId)\n return adapter.inviteMember(context, input)\n },\n async revokeInvitation(context, invitationId) {\n const adapter = resolveAdapter(context)\n await authorize(adapter, context, \"revokeInvitations\")\n return adapter.revokeInvitation(context, invitationId)\n },\n async updateMemberRole(context, memberId, roleId) {\n const adapter = resolveAdapter(context)\n const { actor, members, target } = await mutationState(\n adapter,\n context,\n \"manageRoles\",\n memberId,\n )\n assertAssignableRole(adapter, actor, await adapter.listRoles(context), roleId)\n if (adapter.isOwnerRole(target.roleId) && !adapter.isOwnerRole(roleId)) {\n assertOwnerRemains(adapter, members, target)\n }\n return adapter.updateMemberRole(context, memberId, roleId)\n },\n async deactivateMember(context, memberId) {\n const adapter = resolveAdapter(context)\n const { members, target } = await mutationState(\n adapter,\n context,\n \"deactivateMembers\",\n memberId,\n )\n assertOwnerRemains(adapter, members, target)\n return adapter.deactivateMember(context, memberId)\n },\n async activateMember(context, memberId) {\n const adapter = resolveAdapter(context)\n await mutationState(adapter, context, \"activateMembers\", memberId)\n return adapter.activateMember(context, memberId)\n },\n }\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { VoyantDb } from "@voyant-travel/hono";
2
+ export type TeamMemberStatus = "active" | "deactivated";
3
+ export type TeamInvitationStatus = "pending" | "accepted" | "expired" | "revoked";
4
+ export interface TeamMemberDto {
5
+ id: string;
6
+ email: string | null;
7
+ name: string | null;
8
+ roleId: string;
9
+ roleName: string;
10
+ status: TeamMemberStatus;
11
+ joinedAt: string | null;
12
+ lastActivityAt: string | null;
13
+ }
14
+ export interface TeamRoleDto {
15
+ id: string;
16
+ name: string;
17
+ description: string | null;
18
+ }
19
+ export interface TeamInvitationDto {
20
+ id: string;
21
+ email: string;
22
+ roleId: string;
23
+ roleName: string;
24
+ status: TeamInvitationStatus;
25
+ createdAt: string;
26
+ expiresAt: string;
27
+ }
28
+ export interface CreatedTeamInvitationDto extends TeamInvitationDto {
29
+ acceptUrl: string | null;
30
+ }
31
+ export interface TeamManagementCapabilitiesDto {
32
+ viewRoster: boolean;
33
+ inviteMembers: boolean;
34
+ manageRoles: boolean;
35
+ activateMembers: boolean;
36
+ deactivateMembers: boolean;
37
+ revokeInvitations: boolean;
38
+ }
39
+ export interface TeamManagementRequestContext {
40
+ bindings: Record<string, unknown>;
41
+ db: VoyantDb;
42
+ userId: string;
43
+ }
44
+ export interface InviteTeamMemberInput {
45
+ email: string;
46
+ roleId: string;
47
+ expiresInDays?: number;
48
+ }
49
+ export interface TeamManagementRuntimeProvider {
50
+ getCapabilities(context: TeamManagementRequestContext): Promise<TeamManagementCapabilitiesDto>;
51
+ listMembers(context: TeamManagementRequestContext): Promise<TeamMemberDto[]>;
52
+ listRoles(context: TeamManagementRequestContext): Promise<TeamRoleDto[]>;
53
+ listInvitations(context: TeamManagementRequestContext): Promise<TeamInvitationDto[]>;
54
+ inviteMember(context: TeamManagementRequestContext, input: InviteTeamMemberInput): Promise<CreatedTeamInvitationDto>;
55
+ revokeInvitation(context: TeamManagementRequestContext, invitationId: string): Promise<void>;
56
+ updateMemberRole(context: TeamManagementRequestContext, memberId: string, roleId: string): Promise<TeamMemberDto>;
57
+ activateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>;
58
+ deactivateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>;
59
+ }
60
+ export declare const teamManagementRuntimePort: import("@voyant-travel/core/project").VoyantPort<TeamManagementRuntimeProvider>;
@@ -0,0 +1,25 @@
1
+ import { definePort } from "@voyant-travel/core/project";
2
+ export const teamManagementRuntimePort = definePort({
3
+ id: "auth.team-management-runtime",
4
+ test(provider) {
5
+ if (provider === null || typeof provider !== "object") {
6
+ throw new Error("auth.team-management-runtime provider must be an object.");
7
+ }
8
+ for (const method of [
9
+ "getCapabilities",
10
+ "listMembers",
11
+ "listRoles",
12
+ "listInvitations",
13
+ "inviteMember",
14
+ "revokeInvitation",
15
+ "updateMemberRole",
16
+ "activateMember",
17
+ "deactivateMember",
18
+ ]) {
19
+ if (typeof provider[method] !== "function") {
20
+ throw new Error(`auth.team-management-runtime provider must implement ${method}().`);
21
+ }
22
+ }
23
+ },
24
+ });
25
+ //# sourceMappingURL=team-management-runtime-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-management-runtime-port.js","sourceRoot":"","sources":["../src/team-management-runtime-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AA6ExD,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAgC;IACjF,EAAE,EAAE,8BAA8B;IAClC,IAAI,CAAC,QAAQ;QACX,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;QAC7E,CAAC;QACD,KAAK,MAAM,MAAM,IAAI;YACnB,iBAAiB;YACjB,aAAa;YACb,WAAW;YACX,iBAAiB;YACjB,cAAc;YACd,kBAAkB;YAClB,kBAAkB;YAClB,gBAAgB;YAChB,kBAAkB;SACV,EAAE,CAAC;YACX,IAAI,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC;gBAC3C,MAAM,IAAI,KAAK,CAAC,wDAAwD,MAAM,KAAK,CAAC,CAAA;YACtF,CAAC;QACH,CAAC;IACH,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { definePort } from \"@voyant-travel/core/project\"\nimport type { VoyantDb } from \"@voyant-travel/hono\"\n\nexport type TeamMemberStatus = \"active\" | \"deactivated\"\nexport type TeamInvitationStatus = \"pending\" | \"accepted\" | \"expired\" | \"revoked\"\n\nexport interface TeamMemberDto {\n id: string\n email: string | null\n name: string | null\n roleId: string\n roleName: string\n status: TeamMemberStatus\n joinedAt: string | null\n lastActivityAt: string | null\n}\n\nexport interface TeamRoleDto {\n id: string\n name: string\n description: string | null\n}\n\nexport interface TeamInvitationDto {\n id: string\n email: string\n roleId: string\n roleName: string\n status: TeamInvitationStatus\n createdAt: string\n expiresAt: string\n}\n\nexport interface CreatedTeamInvitationDto extends TeamInvitationDto {\n acceptUrl: string | null\n}\n\nexport interface TeamManagementCapabilitiesDto {\n viewRoster: boolean\n inviteMembers: boolean\n manageRoles: boolean\n activateMembers: boolean\n deactivateMembers: boolean\n revokeInvitations: boolean\n}\n\nexport interface TeamManagementRequestContext {\n bindings: Record<string, unknown>\n db: VoyantDb\n userId: string\n}\n\nexport interface InviteTeamMemberInput {\n email: string\n roleId: string\n expiresInDays?: number\n}\n\nexport interface TeamManagementRuntimeProvider {\n getCapabilities(context: TeamManagementRequestContext): Promise<TeamManagementCapabilitiesDto>\n listMembers(context: TeamManagementRequestContext): Promise<TeamMemberDto[]>\n listRoles(context: TeamManagementRequestContext): Promise<TeamRoleDto[]>\n listInvitations(context: TeamManagementRequestContext): Promise<TeamInvitationDto[]>\n inviteMember(\n context: TeamManagementRequestContext,\n input: InviteTeamMemberInput,\n ): Promise<CreatedTeamInvitationDto>\n revokeInvitation(context: TeamManagementRequestContext, invitationId: string): Promise<void>\n updateMemberRole(\n context: TeamManagementRequestContext,\n memberId: string,\n roleId: string,\n ): Promise<TeamMemberDto>\n activateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>\n deactivateMember(context: TeamManagementRequestContext, memberId: string): Promise<TeamMemberDto>\n}\n\nexport const teamManagementRuntimePort = definePort<TeamManagementRuntimeProvider>({\n id: \"auth.team-management-runtime\",\n test(provider) {\n if (provider === null || typeof provider !== \"object\") {\n throw new Error(\"auth.team-management-runtime provider must be an object.\")\n }\n for (const method of [\n \"getCapabilities\",\n \"listMembers\",\n \"listRoles\",\n \"listInvitations\",\n \"inviteMember\",\n \"revokeInvitation\",\n \"updateMemberRole\",\n \"activateMember\",\n \"deactivateMember\",\n ] as const) {\n if (typeof provider[method] !== \"function\") {\n throw new Error(`auth.team-management-runtime provider must implement ${method}().`)\n }\n }\n },\n})\n"]}
@@ -1,12 +1,12 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import { type VoyantDb } from "@voyant-travel/hono";
3
- import type { IdentityAccessRuntimeProvider } from "./identity-access-runtime-port.js";
4
- type IdentityAccessEnv = {
3
+ import type { TeamManagementRuntimeProvider } from "./team-management-runtime-port.js";
4
+ type TeamEnv = {
5
5
  Bindings: Record<string, unknown>;
6
6
  Variables: {
7
7
  userId?: string;
8
8
  db: VoyantDb;
9
9
  };
10
10
  };
11
- export declare function createTeamAdminRoutes(runtime: IdentityAccessRuntimeProvider): OpenAPIHono<IdentityAccessEnv, {}, "/">;
11
+ export declare function createTeamAdminRoutes(runtime: TeamManagementRuntimeProvider): OpenAPIHono<TeamEnv, {}, "/">;
12
12
  export {};