@checkstack/auth-backend 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.ts CHANGED
@@ -34,8 +34,8 @@ import {
34
34
  /**
35
35
  * Creates the auth router using contract-based implementation.
36
36
  *
37
- * Auth and permissions are automatically enforced via autoAuthMiddleware
38
- * based on the contract's meta.userType and meta.permissions.
37
+ * Auth and access rules are automatically enforced via autoAuthMiddleware
38
+ * based on the contract's meta.userType and meta.access.
39
39
  */
40
40
  const os = implement(authContract)
41
41
  .$context<RpcContext>()
@@ -117,12 +117,12 @@ export const createAuthRouter = (
117
117
  strategyRegistry: { getStrategies: () => AuthStrategy<unknown>[] },
118
118
  reloadAuthFn: () => Promise<void>,
119
119
  configService: ConfigService,
120
- permissionRegistry: {
121
- getPermissions: () => {
120
+ accessRuleRegistry: {
121
+ getAccessRules: () => {
122
122
  id: string;
123
123
  description?: string;
124
- isAuthenticatedDefault?: boolean;
125
- isPublicDefault?: boolean;
124
+ isDefault?: boolean;
125
+ isPublic?: boolean;
126
126
  }[];
127
127
  }
128
128
  ) => {
@@ -155,12 +155,12 @@ export const createAuthRouter = (
155
155
  return enabledStrategies.filter((s) => s.enabled);
156
156
  });
157
157
 
158
- const permissions = os.permissions.handler(async ({ context }) => {
158
+ const accessRulesHandler = os.accessRules.handler(async ({ context }) => {
159
159
  const user = context.user;
160
160
  if (!isRealUser(user)) {
161
- return { permissions: [] };
161
+ return { accessRules: [] };
162
162
  }
163
- return { permissions: user.permissions || [] };
163
+ return { accessRules: user.accessRules || [] };
164
164
  });
165
165
 
166
166
  const getUsers = os.getUsers.handler(async () => {
@@ -214,42 +214,42 @@ export const createAuthRouter = (
214
214
 
215
215
  const getRoles = os.getRoles.handler(async () => {
216
216
  const roles = await internalDb.select().from(schema.role);
217
- const rolePermissions = await internalDb
217
+ const roleAccessRules = await internalDb
218
218
  .select()
219
- .from(schema.rolePermission);
219
+ .from(schema.roleAccessRule);
220
220
 
221
221
  return roles.map((role) => ({
222
222
  id: role.id,
223
223
  name: role.name,
224
224
  description: role.description,
225
- permissions: rolePermissions
225
+ accessRules: roleAccessRules
226
226
  .filter((rp) => rp.roleId === role.id)
227
- .map((rp) => rp.permissionId),
227
+ .map((rp) => rp.accessRuleId),
228
228
  isSystem: role.isSystem || false,
229
229
  // Anonymous role cannot be assigned to users - it's for unauthenticated access
230
230
  isAssignable: role.id !== "anonymous",
231
231
  }));
232
232
  });
233
233
 
234
- const getPermissions = os.getPermissions.handler(async () => {
235
- // Return only currently active permissions (registered by loaded plugins)
236
- return permissionRegistry.getPermissions();
234
+ const getAccessRules = os.getAccessRules.handler(async () => {
235
+ // Return only currently active access rules (registered by loaded plugins)
236
+ return accessRuleRegistry.getAccessRules();
237
237
  });
238
238
 
239
239
  const createRole = os.createRole.handler(async ({ input }) => {
240
- const { name, description, permissions: inputPermissions } = input;
240
+ const { name, description, accessRules: inputAccessRules } = input;
241
241
 
242
242
  // Generate UUID for new role
243
243
  const id = crypto.randomUUID();
244
244
 
245
- // Get active permissions to filter input
246
- const activePermissions = new Set(
247
- permissionRegistry.getPermissions().map((p) => p.id)
245
+ // Get active access rules to filter input
246
+ const activeAccessRules = new Set(
247
+ accessRuleRegistry.getAccessRules().map((p) => p.id)
248
248
  );
249
249
 
250
- // Filter to only include active permissions
251
- const validPermissions = inputPermissions.filter((p) =>
252
- activePermissions.has(p)
250
+ // Filter to only include active access rules
251
+ const validAccessRules = inputAccessRules.filter((p) =>
252
+ activeAccessRules.has(p)
253
253
  );
254
254
 
255
255
  await internalDb.transaction(async (tx) => {
@@ -261,12 +261,12 @@ export const createAuthRouter = (
261
261
  isSystem: false,
262
262
  });
263
263
 
264
- // Create role-permission mappings
265
- if (validPermissions.length > 0) {
266
- await tx.insert(schema.rolePermission).values(
267
- validPermissions.map((permissionId) => ({
264
+ // Create role-access rule mappings
265
+ if (validAccessRules.length > 0) {
266
+ await tx.insert(schema.roleAccessRule).values(
267
+ validAccessRules.map((accessRuleId) => ({
268
268
  roleId: id,
269
- permissionId,
269
+ accessRuleId,
270
270
  }))
271
271
  );
272
272
  }
@@ -274,9 +274,9 @@ export const createAuthRouter = (
274
274
  });
275
275
 
276
276
  const updateRole = os.updateRole.handler(async ({ input, context }) => {
277
- const { id, name, description, permissions: inputPermissions } = input;
277
+ const { id, name, description, accessRules: inputAccessRules } = input;
278
278
 
279
- // Track if user has this role (for permission elevation prevention)
279
+ // Track if user has this role (for access elevation prevention)
280
280
  const userRoles = isRealUser(context.user) ? context.user.roles || [] : [];
281
281
  const isUserOwnRole = userRoles.includes(id);
282
282
 
@@ -296,87 +296,87 @@ export const createAuthRouter = (
296
296
  const isAdminRole = id === "admin";
297
297
 
298
298
  // System roles can have name/description edited, but not deleted
299
- // Admin role: permissions cannot be changed (wildcard permission)
300
- // Users role: permissions can be changed with default tracking
301
- // User's own role: permissions cannot be changed (prevent self-elevation)
299
+ // Admin role: access rules cannot be changed (wildcard access)
300
+ // Users role: access rules can be changed with default tracking
301
+ // User's own role: access rules cannot be changed (prevent access elevation)
302
302
 
303
- // Get active permissions to filter input
304
- const activePermissions = new Set(
305
- permissionRegistry.getPermissions().map((p) => p.id)
303
+ // Get active access rules to filter input
304
+ const activeAccessRules = new Set(
305
+ accessRuleRegistry.getAccessRules().map((p) => p.id)
306
306
  );
307
307
 
308
- // Filter to only include active permissions
309
- const validPermissions = inputPermissions.filter((p) =>
310
- activePermissions.has(p)
308
+ // Filter to only include active access rules
309
+ const validAccessRules = inputAccessRules.filter((p) =>
310
+ activeAccessRules.has(p)
311
311
  );
312
312
 
313
- // Track disabled authenticated default permissions for "users" role
313
+ // Track disabled authenticated default access rules for "users" role
314
314
  if (isUsersRole && !isUserOwnRole) {
315
- const allPerms = permissionRegistry.getPermissions();
315
+ const allPerms = accessRuleRegistry.getAccessRules();
316
316
  const defaultPermIds = allPerms
317
- .filter((p) => p.isAuthenticatedDefault)
317
+ .filter((p) => p.isDefault)
318
318
  .map((p) => p.id);
319
319
 
320
- // Find authenticated default permissions that are being removed
320
+ // Find authenticated default access rules that are being removed
321
321
  const removedDefaults = defaultPermIds.filter(
322
- (defId) => !validPermissions.includes(defId)
322
+ (defId) => !validAccessRules.includes(defId)
323
323
  );
324
324
 
325
- // Insert into disabled_default_permission table
325
+ // Insert into disabled_default_access_rule table
326
326
  for (const permId of removedDefaults) {
327
327
  await internalDb
328
- .insert(schema.disabledDefaultPermission)
328
+ .insert(schema.disabledDefaultAccessRule)
329
329
  .values({
330
- permissionId: permId,
330
+ accessRuleId: permId,
331
331
  disabledAt: new Date(),
332
332
  })
333
333
  .onConflictDoNothing();
334
334
  }
335
335
 
336
336
  // Remove from disabled table if being re-added
337
- const readdedDefaults = validPermissions.filter((p) =>
337
+ const readdedDefaults = validAccessRules.filter((p) =>
338
338
  defaultPermIds.includes(p)
339
339
  );
340
340
  for (const permId of readdedDefaults) {
341
341
  await internalDb
342
- .delete(schema.disabledDefaultPermission)
343
- .where(eq(schema.disabledDefaultPermission.permissionId, permId));
342
+ .delete(schema.disabledDefaultAccessRule)
343
+ .where(eq(schema.disabledDefaultAccessRule.accessRuleId, permId));
344
344
  }
345
345
  }
346
346
 
347
- // Track disabled public default permissions for "anonymous" role
347
+ // Track disabled public default access rules for "anonymous" role
348
348
  const isAnonymousRole = id === "anonymous";
349
349
  if (isAnonymousRole) {
350
- const allPerms = permissionRegistry.getPermissions();
350
+ const allPerms = accessRuleRegistry.getAccessRules();
351
351
  const publicDefaultPermIds = allPerms
352
- .filter((p) => p.isPublicDefault)
352
+ .filter((p) => p.isPublic)
353
353
  .map((p) => p.id);
354
354
 
355
- // Find public default permissions that are being removed
355
+ // Find public default access rules that are being removed
356
356
  const removedPublicDefaults = publicDefaultPermIds.filter(
357
- (defId) => !validPermissions.includes(defId)
357
+ (defId) => !validAccessRules.includes(defId)
358
358
  );
359
359
 
360
- // Insert into disabled_public_default_permission table
360
+ // Insert into disabled_public_default_access_rule table
361
361
  for (const permId of removedPublicDefaults) {
362
362
  await internalDb
363
- .insert(schema.disabledPublicDefaultPermission)
363
+ .insert(schema.disabledPublicDefaultAccessRule)
364
364
  .values({
365
- permissionId: permId,
365
+ accessRuleId: permId,
366
366
  disabledAt: new Date(),
367
367
  })
368
368
  .onConflictDoNothing();
369
369
  }
370
370
 
371
371
  // Remove from disabled table if being re-added
372
- const readdedPublicDefaults = validPermissions.filter((p) =>
372
+ const readdedPublicDefaults = validAccessRules.filter((p) =>
373
373
  publicDefaultPermIds.includes(p)
374
374
  );
375
375
  for (const permId of readdedPublicDefaults) {
376
376
  await internalDb
377
- .delete(schema.disabledPublicDefaultPermission)
377
+ .delete(schema.disabledPublicDefaultAccessRule)
378
378
  .where(
379
- eq(schema.disabledPublicDefaultPermission.permissionId, permId)
379
+ eq(schema.disabledPublicDefaultAccessRule.accessRuleId, permId)
380
380
  );
381
381
  }
382
382
  }
@@ -391,21 +391,21 @@ export const createAuthRouter = (
391
391
  await tx.update(schema.role).set(updates).where(eq(schema.role.id, id));
392
392
  }
393
393
 
394
- // Skip permission changes for admin role (wildcard) or user's own role (prevent self-elevation)
394
+ // Skip access rule changes for admin role (wildcard) or user's own role (prevent access elevation)
395
395
  if (isAdminRole || isUserOwnRole) {
396
- return; // Don't modify permissions
396
+ return; // Don't modify access rules
397
397
  }
398
398
 
399
- // Replace permission mappings for non-admin roles
399
+ // Replace access rule mappings for non-admin roles
400
400
  await tx
401
- .delete(schema.rolePermission)
402
- .where(eq(schema.rolePermission.roleId, id));
401
+ .delete(schema.roleAccessRule)
402
+ .where(eq(schema.roleAccessRule.roleId, id));
403
403
 
404
- if (validPermissions.length > 0) {
405
- await tx.insert(schema.rolePermission).values(
406
- validPermissions.map((permissionId) => ({
404
+ if (validAccessRules.length > 0) {
405
+ await tx.insert(schema.roleAccessRule).values(
406
+ validAccessRules.map((accessRuleId) => ({
407
407
  roleId: id,
408
- permissionId,
408
+ accessRuleId,
409
409
  }))
410
410
  );
411
411
  }
@@ -441,10 +441,10 @@ export const createAuthRouter = (
441
441
 
442
442
  // Delete role and related records in transaction
443
443
  await internalDb.transaction(async (tx) => {
444
- // Delete role-permission mappings
444
+ // Delete role-access-rule mappings
445
445
  await tx
446
- .delete(schema.rolePermission)
447
- .where(eq(schema.rolePermission.roleId, id));
446
+ .delete(schema.roleAccessRule)
447
+ .where(eq(schema.roleAccessRule.roleId, id));
448
448
 
449
449
  // Delete user-role mappings
450
450
  await tx.delete(schema.userRole).where(eq(schema.userRole.roleId, id));
@@ -582,40 +582,40 @@ export const createAuthRouter = (
582
582
  }
583
583
  );
584
584
 
585
- const getAnonymousPermissions = os.getAnonymousPermissions.handler(
585
+ const getAnonymousAccessRules = os.getAnonymousAccessRules.handler(
586
586
  async () => {
587
587
  const rolePerms = await internalDb
588
588
  .select()
589
- .from(schema.rolePermission)
590
- .where(eq(schema.rolePermission.roleId, "anonymous"));
591
- return rolePerms.map((rp) => rp.permissionId);
589
+ .from(schema.roleAccessRule)
590
+ .where(eq(schema.roleAccessRule.roleId, "anonymous"));
591
+ return rolePerms.map((rp) => rp.accessRuleId);
592
592
  }
593
593
  );
594
594
 
595
- const filterUsersByPermission = os.filterUsersByPermission.handler(
595
+ const filterUsersByAccessRule = os.filterUsersByAccessRule.handler(
596
596
  async ({ input }) => {
597
- const { userIds, permission } = input;
597
+ const { userIds, accessRule } = input;
598
598
 
599
599
  if (userIds.length === 0) return [];
600
600
 
601
- // Single efficient query: join user_role with role_permission
602
- // and filter by both userIds AND the specific permission
603
- const usersWithPermission = await internalDb
601
+ // Single efficient query: join user_role with role_access_rule
602
+ // and filter by both userIds AND the specific access rule
603
+ const usersWithAccess = await internalDb
604
604
  .select({ userId: schema.userRole.userId })
605
605
  .from(schema.userRole)
606
606
  .innerJoin(
607
- schema.rolePermission,
608
- eq(schema.userRole.roleId, schema.rolePermission.roleId)
607
+ schema.roleAccessRule,
608
+ eq(schema.userRole.roleId, schema.roleAccessRule.roleId)
609
609
  )
610
610
  .where(
611
611
  and(
612
612
  inArray(schema.userRole.userId, userIds),
613
- eq(schema.rolePermission.permissionId, permission)
613
+ eq(schema.roleAccessRule.accessRuleId, accessRule)
614
614
  )
615
615
  )
616
616
  .groupBy(schema.userRole.userId);
617
617
 
618
- return usersWithPermission.map((row) => row.userId);
618
+ return usersWithAccess.map((row) => row.userId);
619
619
  }
620
620
  );
621
621
 
@@ -1016,13 +1016,430 @@ export const createAuthRouter = (
1016
1016
  }
1017
1017
  );
1018
1018
 
1019
+ // ==========================================================================
1020
+ // TEAM MANAGEMENT HANDLERS
1021
+ // ==========================================================================
1022
+
1023
+ const getTeams = os.getTeams.handler(async ({ context }) => {
1024
+ const teams = await internalDb.select().from(schema.team);
1025
+ const memberCounts = await internalDb
1026
+ .select({ teamId: schema.userTeam.teamId })
1027
+ .from(schema.userTeam);
1028
+
1029
+ const userId = isRealUser(context.user) ? context.user.id : undefined;
1030
+ const managerRows = userId
1031
+ ? await internalDb
1032
+ .select()
1033
+ .from(schema.teamManager)
1034
+ .where(eq(schema.teamManager.userId, userId))
1035
+ : [];
1036
+ const managedTeamIds = new Set(managerRows.map((m) => m.teamId));
1037
+
1038
+ return teams.map((t) => ({
1039
+ id: t.id,
1040
+ name: t.name,
1041
+ description: t.description,
1042
+ memberCount: memberCounts.filter((m) => m.teamId === t.id).length,
1043
+ isManager: managedTeamIds.has(t.id),
1044
+ }));
1045
+ });
1046
+
1047
+ const getTeam = os.getTeam.handler(async ({ input }) => {
1048
+ const teams = await internalDb
1049
+ .select()
1050
+ .from(schema.team)
1051
+ .where(eq(schema.team.id, input.teamId))
1052
+ .limit(1);
1053
+ if (teams.length === 0) return;
1054
+
1055
+ const team = teams[0];
1056
+ const memberRows = await internalDb
1057
+ .select({ userId: schema.userTeam.userId })
1058
+ .from(schema.userTeam)
1059
+ .where(eq(schema.userTeam.teamId, team.id));
1060
+ const managerRows = await internalDb
1061
+ .select({ userId: schema.teamManager.userId })
1062
+ .from(schema.teamManager)
1063
+ .where(eq(schema.teamManager.teamId, team.id));
1064
+
1065
+ const userIds = [
1066
+ ...new Set([
1067
+ ...memberRows.map((m) => m.userId),
1068
+ ...managerRows.map((m) => m.userId),
1069
+ ]),
1070
+ ];
1071
+ const users =
1072
+ userIds.length > 0
1073
+ ? await internalDb
1074
+ .select({
1075
+ id: schema.user.id,
1076
+ name: schema.user.name,
1077
+ email: schema.user.email,
1078
+ })
1079
+ .from(schema.user)
1080
+ .where(inArray(schema.user.id, userIds))
1081
+ : [];
1082
+ const userMap = new Map(users.map((u) => [u.id, u]));
1083
+
1084
+ return {
1085
+ id: team.id,
1086
+ name: team.name,
1087
+ description: team.description,
1088
+ members: memberRows
1089
+ .map((m) => userMap.get(m.userId))
1090
+ .filter((u): u is NonNullable<typeof u> => u !== undefined),
1091
+ managers: managerRows
1092
+ .map((m) => userMap.get(m.userId))
1093
+ .filter((u): u is NonNullable<typeof u> => u !== undefined),
1094
+ };
1095
+ });
1096
+
1097
+ const createTeam = os.createTeam.handler(async ({ input, context }) => {
1098
+ const id = crypto.randomUUID();
1099
+ const now = new Date();
1100
+ await internalDb.insert(schema.team).values({
1101
+ id,
1102
+ name: input.name,
1103
+ description: input.description,
1104
+ createdAt: now,
1105
+ updatedAt: now,
1106
+ });
1107
+ context.logger.info(`[auth-backend] Created team: ${input.name}`);
1108
+ return { id };
1109
+ });
1110
+
1111
+ const updateTeam = os.updateTeam.handler(async ({ input, context }) => {
1112
+ const { id, name, description } = input;
1113
+ // TODO: Check if user is manager or has teamsManage access
1114
+ const updates: {
1115
+ name?: string;
1116
+ description?: string | null;
1117
+ updatedAt: Date;
1118
+ } = {
1119
+ updatedAt: new Date(),
1120
+ };
1121
+ if (name !== undefined) updates.name = name;
1122
+ if (description !== undefined) updates.description = description;
1123
+ await internalDb
1124
+ .update(schema.team)
1125
+ .set(updates)
1126
+ .where(eq(schema.team.id, id));
1127
+ context.logger.info(`[auth-backend] Updated team: ${id}`);
1128
+ });
1129
+
1130
+ const deleteTeam = os.deleteTeam.handler(async ({ input: id, context }) => {
1131
+ await internalDb.transaction(async (tx) => {
1132
+ await tx.delete(schema.userTeam).where(eq(schema.userTeam.teamId, id));
1133
+ await tx
1134
+ .delete(schema.teamManager)
1135
+ .where(eq(schema.teamManager.teamId, id));
1136
+ await tx
1137
+ .delete(schema.applicationTeam)
1138
+ .where(eq(schema.applicationTeam.teamId, id));
1139
+ await tx
1140
+ .delete(schema.resourceTeamAccess)
1141
+ .where(eq(schema.resourceTeamAccess.teamId, id));
1142
+ await tx.delete(schema.team).where(eq(schema.team.id, id));
1143
+ });
1144
+ context.logger.info(`[auth-backend] Deleted team: ${id}`);
1145
+ });
1146
+
1147
+ const addUserToTeam = os.addUserToTeam.handler(async ({ input }) => {
1148
+ await internalDb
1149
+ .insert(schema.userTeam)
1150
+ .values({ userId: input.userId, teamId: input.teamId })
1151
+ .onConflictDoNothing();
1152
+ });
1153
+
1154
+ const removeUserFromTeam = os.removeUserFromTeam.handler(
1155
+ async ({ input }) => {
1156
+ await internalDb
1157
+ .delete(schema.userTeam)
1158
+ .where(
1159
+ and(
1160
+ eq(schema.userTeam.userId, input.userId),
1161
+ eq(schema.userTeam.teamId, input.teamId)
1162
+ )
1163
+ );
1164
+ }
1165
+ );
1166
+
1167
+ const addTeamManager = os.addTeamManager.handler(async ({ input }) => {
1168
+ await internalDb
1169
+ .insert(schema.teamManager)
1170
+ .values({ userId: input.userId, teamId: input.teamId })
1171
+ .onConflictDoNothing();
1172
+ });
1173
+
1174
+ const removeTeamManager = os.removeTeamManager.handler(async ({ input }) => {
1175
+ await internalDb
1176
+ .delete(schema.teamManager)
1177
+ .where(
1178
+ and(
1179
+ eq(schema.teamManager.userId, input.userId),
1180
+ eq(schema.teamManager.teamId, input.teamId)
1181
+ )
1182
+ );
1183
+ });
1184
+
1185
+ const getResourceTeamAccess = os.getResourceTeamAccess.handler(
1186
+ async ({ input }) => {
1187
+ const rows = await internalDb
1188
+ .select()
1189
+ .from(schema.resourceTeamAccess)
1190
+ .innerJoin(
1191
+ schema.team,
1192
+ eq(schema.resourceTeamAccess.teamId, schema.team.id)
1193
+ )
1194
+ .where(
1195
+ and(
1196
+ eq(schema.resourceTeamAccess.resourceType, input.resourceType),
1197
+ eq(schema.resourceTeamAccess.resourceId, input.resourceId)
1198
+ )
1199
+ );
1200
+ return rows.map((r) => ({
1201
+ teamId: r.resource_team_access.teamId,
1202
+ teamName: r.team.name,
1203
+ canRead: r.resource_team_access.canRead,
1204
+ canManage: r.resource_team_access.canManage,
1205
+ }));
1206
+ }
1207
+ );
1208
+
1209
+ const setResourceTeamAccess = os.setResourceTeamAccess.handler(
1210
+ async ({ input }) => {
1211
+ const { resourceType, resourceId, teamId, canRead, canManage } = input;
1212
+ await internalDb
1213
+ .insert(schema.resourceTeamAccess)
1214
+ .values({
1215
+ resourceType,
1216
+ resourceId,
1217
+ teamId,
1218
+ canRead: canRead ?? true,
1219
+ canManage: canManage ?? false,
1220
+ })
1221
+ .onConflictDoUpdate({
1222
+ target: [
1223
+ schema.resourceTeamAccess.resourceType,
1224
+ schema.resourceTeamAccess.resourceId,
1225
+ schema.resourceTeamAccess.teamId,
1226
+ ],
1227
+ set: {
1228
+ canRead: canRead ?? true,
1229
+ canManage: canManage ?? false,
1230
+ },
1231
+ });
1232
+ }
1233
+ );
1234
+
1235
+ const removeResourceTeamAccess = os.removeResourceTeamAccess.handler(
1236
+ async ({ input }) => {
1237
+ await internalDb
1238
+ .delete(schema.resourceTeamAccess)
1239
+ .where(
1240
+ and(
1241
+ eq(schema.resourceTeamAccess.resourceType, input.resourceType),
1242
+ eq(schema.resourceTeamAccess.resourceId, input.resourceId),
1243
+ eq(schema.resourceTeamAccess.teamId, input.teamId)
1244
+ )
1245
+ );
1246
+ }
1247
+ );
1248
+
1249
+ // Resource-level access settings
1250
+ const getResourceAccessSettings = os.getResourceAccessSettings.handler(
1251
+ async ({ input }) => {
1252
+ const rows = await internalDb
1253
+ .select()
1254
+ .from(schema.resourceAccessSettings)
1255
+ .where(
1256
+ and(
1257
+ eq(schema.resourceAccessSettings.resourceType, input.resourceType),
1258
+ eq(schema.resourceAccessSettings.resourceId, input.resourceId)
1259
+ )
1260
+ )
1261
+ .limit(1);
1262
+ return { teamOnly: rows[0]?.teamOnly ?? false };
1263
+ }
1264
+ );
1265
+
1266
+ const setResourceAccessSettings = os.setResourceAccessSettings.handler(
1267
+ async ({ input }) => {
1268
+ const { resourceType, resourceId, teamOnly } = input;
1269
+ await internalDb
1270
+ .insert(schema.resourceAccessSettings)
1271
+ .values({ resourceType, resourceId, teamOnly })
1272
+ .onConflictDoUpdate({
1273
+ target: [
1274
+ schema.resourceAccessSettings.resourceType,
1275
+ schema.resourceAccessSettings.resourceId,
1276
+ ],
1277
+ set: { teamOnly },
1278
+ });
1279
+ }
1280
+ );
1281
+
1282
+ // S2S Endpoints for middleware
1283
+ const checkResourceTeamAccess = os.checkResourceTeamAccess.handler(
1284
+ async ({ input }) => {
1285
+ const {
1286
+ userId,
1287
+ userType,
1288
+ resourceType,
1289
+ resourceId,
1290
+ action,
1291
+ hasGlobalAccess,
1292
+ } = input;
1293
+
1294
+ const grants = await internalDb
1295
+ .select()
1296
+ .from(schema.resourceTeamAccess)
1297
+ .where(
1298
+ and(
1299
+ eq(schema.resourceTeamAccess.resourceType, resourceType),
1300
+ eq(schema.resourceTeamAccess.resourceId, resourceId)
1301
+ )
1302
+ );
1303
+
1304
+ // No grants = global access applies
1305
+ if (grants.length === 0) return { hasAccess: hasGlobalAccess };
1306
+
1307
+ // Check resource-level settings for teamOnly
1308
+ const settingsRows = await internalDb
1309
+ .select()
1310
+ .from(schema.resourceAccessSettings)
1311
+ .where(
1312
+ and(
1313
+ eq(schema.resourceAccessSettings.resourceType, resourceType),
1314
+ eq(schema.resourceAccessSettings.resourceId, resourceId)
1315
+ )
1316
+ )
1317
+ .limit(1);
1318
+ const isTeamOnly = settingsRows[0]?.teamOnly ?? false;
1319
+
1320
+ if (!isTeamOnly && hasGlobalAccess) return { hasAccess: true };
1321
+
1322
+ // Get user's teams
1323
+ const teamTable =
1324
+ userType === "user" ? schema.userTeam : schema.applicationTeam;
1325
+ const userIdCol =
1326
+ userType === "user"
1327
+ ? schema.userTeam.userId
1328
+ : schema.applicationTeam.applicationId;
1329
+ const userTeams = await internalDb
1330
+ .select({
1331
+ teamId:
1332
+ userType === "user"
1333
+ ? schema.userTeam.teamId
1334
+ : schema.applicationTeam.teamId,
1335
+ })
1336
+ .from(teamTable)
1337
+ .where(eq(userIdCol, userId));
1338
+ const userTeamIds = new Set(userTeams.map((t) => t.teamId));
1339
+
1340
+ const field = action === "manage" ? "canManage" : "canRead";
1341
+ const hasAccess = grants.some(
1342
+ (g) => userTeamIds.has(g.teamId) && g[field]
1343
+ );
1344
+ return { hasAccess };
1345
+ }
1346
+ );
1347
+
1348
+ const getAccessibleResourceIds = os.getAccessibleResourceIds.handler(
1349
+ async ({ input }) => {
1350
+ const {
1351
+ userId,
1352
+ userType,
1353
+ resourceType,
1354
+ resourceIds,
1355
+ action,
1356
+ hasGlobalAccess,
1357
+ } = input;
1358
+ if (resourceIds.length === 0) return [];
1359
+
1360
+ // Get all grants for these resources
1361
+ const grants = await internalDb
1362
+ .select()
1363
+ .from(schema.resourceTeamAccess)
1364
+ .where(
1365
+ and(
1366
+ eq(schema.resourceTeamAccess.resourceType, resourceType),
1367
+ inArray(schema.resourceTeamAccess.resourceId, resourceIds)
1368
+ )
1369
+ );
1370
+
1371
+ // Get resource-level settings for teamOnly
1372
+ const settingsRows = await internalDb
1373
+ .select()
1374
+ .from(schema.resourceAccessSettings)
1375
+ .where(
1376
+ and(
1377
+ eq(schema.resourceAccessSettings.resourceType, resourceType),
1378
+ inArray(schema.resourceAccessSettings.resourceId, resourceIds)
1379
+ )
1380
+ );
1381
+ const teamOnlyByResource = new Map(
1382
+ settingsRows.map((s) => [s.resourceId, s.teamOnly])
1383
+ );
1384
+
1385
+ // Get user's teams
1386
+ const teamTable =
1387
+ userType === "user" ? schema.userTeam : schema.applicationTeam;
1388
+ const userIdCol =
1389
+ userType === "user"
1390
+ ? schema.userTeam.userId
1391
+ : schema.applicationTeam.applicationId;
1392
+ const userTeams = await internalDb
1393
+ .select({
1394
+ teamId:
1395
+ userType === "user"
1396
+ ? schema.userTeam.teamId
1397
+ : schema.applicationTeam.teamId,
1398
+ })
1399
+ .from(teamTable)
1400
+ .where(eq(userIdCol, userId));
1401
+ const userTeamIds = new Set(userTeams.map((t) => t.teamId));
1402
+
1403
+ const field = action === "manage" ? "canManage" : "canRead";
1404
+ const grantsByResource = new Map<string, typeof grants>();
1405
+ for (const g of grants) {
1406
+ const existing = grantsByResource.get(g.resourceId) || [];
1407
+ existing.push(g);
1408
+ grantsByResource.set(g.resourceId, existing);
1409
+ }
1410
+
1411
+ return resourceIds.filter((id) => {
1412
+ const resourceGrants = grantsByResource.get(id) || [];
1413
+ if (resourceGrants.length === 0) return hasGlobalAccess;
1414
+ const isTeamOnly = teamOnlyByResource.get(id) ?? false;
1415
+ if (!isTeamOnly && hasGlobalAccess) return true;
1416
+ return resourceGrants.some(
1417
+ (g) => userTeamIds.has(g.teamId) && g[field]
1418
+ );
1419
+ });
1420
+ }
1421
+ );
1422
+
1423
+ const deleteResourceGrants = os.deleteResourceGrants.handler(
1424
+ async ({ input }) => {
1425
+ await internalDb
1426
+ .delete(schema.resourceTeamAccess)
1427
+ .where(
1428
+ and(
1429
+ eq(schema.resourceTeamAccess.resourceType, input.resourceType),
1430
+ eq(schema.resourceTeamAccess.resourceId, input.resourceId)
1431
+ )
1432
+ );
1433
+ }
1434
+ );
1435
+
1019
1436
  return os.router({
1020
1437
  getEnabledStrategies,
1021
- permissions,
1438
+ accessRules: accessRulesHandler,
1022
1439
  getUsers,
1023
1440
  deleteUser,
1024
1441
  getRoles,
1025
- getPermissions,
1442
+ getAccessRules,
1026
1443
  createRole,
1027
1444
  updateRole,
1028
1445
  deleteRole,
@@ -1033,9 +1450,9 @@ export const createAuthRouter = (
1033
1450
  getRegistrationSchema,
1034
1451
  getRegistrationStatus,
1035
1452
  setRegistrationStatus,
1036
- getAnonymousPermissions,
1453
+ getAnonymousAccessRules,
1037
1454
  getUserById,
1038
- filterUsersByPermission,
1455
+ filterUsersByAccessRule,
1039
1456
  findUserByEmail,
1040
1457
  upsertExternalUser,
1041
1458
  createSession,
@@ -1045,6 +1462,24 @@ export const createAuthRouter = (
1045
1462
  updateApplication,
1046
1463
  deleteApplication,
1047
1464
  regenerateApplicationSecret,
1465
+ // Teams
1466
+ getTeams,
1467
+ getTeam,
1468
+ createTeam,
1469
+ updateTeam,
1470
+ deleteTeam,
1471
+ addUserToTeam,
1472
+ removeUserFromTeam,
1473
+ addTeamManager,
1474
+ removeTeamManager,
1475
+ getResourceTeamAccess,
1476
+ setResourceTeamAccess,
1477
+ removeResourceTeamAccess,
1478
+ getResourceAccessSettings,
1479
+ setResourceAccessSettings,
1480
+ checkResourceTeamAccess,
1481
+ getAccessibleResourceIds,
1482
+ deleteResourceGrants,
1048
1483
  });
1049
1484
  };
1050
1485