@fonderie/workspaces 5.3.2 → 6.0.1

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/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  importMembership: () => importMembership,
30
30
  importRole: () => importRole,
31
31
  importWorkspace: () => importWorkspace,
32
+ requireManager: () => requireManager,
32
33
  requireWorkspace: () => requireWorkspace,
33
34
  schemas: () => schemas_exports,
34
35
  toInvitationDTO: () => toInvitationDTO,
@@ -51,6 +52,7 @@ var EVENT_KEYS = {
51
52
 
52
53
  // src/routes.ts
53
54
  var import_middlewares = require("@fonderie/core/middlewares");
55
+ var import_rate_limit = require("@fonderie/rate-limit");
54
56
 
55
57
  // src/schemas.ts
56
58
  var schemas_exports = {};
@@ -107,7 +109,10 @@ var createInvitationsSchema = import_zod.z.union([
107
109
  inviteEntry,
108
110
  import_zod.z.array(inviteEntry).min(1, "at least one invite is required")
109
111
  ]);
110
- var acceptInvitationSchema = import_zod.z.object({ pin: import_zod.z.string().trim().min(1, "pin is required") });
112
+ var acceptInvitationSchema = import_zod.z.union([
113
+ import_zod.z.object({ pin: import_zod.z.string().trim().min(1, "pin is required") }),
114
+ import_zod.z.object({ token: import_zod.z.string().trim().min(1, "token is required") })
115
+ ]);
111
116
  var createRoleSchema = import_zod.z.object({
112
117
  name,
113
118
  description: import_zod.z.string().max(1e3).optional()
@@ -460,8 +465,45 @@ function withWorkspace(store, ctx, next) {
460
465
  return handler;
461
466
  }
462
467
 
463
- // src/controllers/workspace.controller.ts
468
+ // src/middlewares/require-manager.ts
464
469
  var import_core2 = require("@fonderie/core");
470
+ function requireManager(store, config) {
471
+ const managerRoles = config.managerRoles ?? ["ADMIN"];
472
+ return async (ctx, next) => {
473
+ if (config.management === "any-member") return next();
474
+ if (!ctx.user) {
475
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
476
+ }
477
+ if (!ctx.workspace) return next();
478
+ const ownerId = ctx.workspace.ownerId;
479
+ if (ownerId && ownerId === ctx.user.id) return next();
480
+ const [row] = await store.query(
481
+ `SELECT 1 AS ok
482
+ FROM fonderie_role_user_workspaces ruw
483
+ JOIN fonderie_roles r ON r.id = ruw.role_id
484
+ WHERE ruw.user_id = $1
485
+ AND ruw.workspace_id = $2
486
+ AND ruw.removed = false
487
+ AND ruw.suspended = false
488
+ AND r.is_system = true
489
+ AND r.active = true
490
+ AND r.name = ANY($3)
491
+ LIMIT 1`,
492
+ [ctx.user.id, ctx.workspace.id, managerRoles]
493
+ );
494
+ if (!row) {
495
+ return (0, import_core2.setApiResponse)(
496
+ import_core2.HTTP.FORBIDDEN,
497
+ "MANAGER_REQUIRED",
498
+ "This action requires the workspace owner or an admin role"
499
+ );
500
+ }
501
+ return next();
502
+ };
503
+ }
504
+
505
+ // src/controllers/workspace.controller.ts
506
+ var import_core3 = require("@fonderie/core");
465
507
 
466
508
  // src/models/workspace.model.ts
467
509
  var WorkspaceModel = class {
@@ -739,7 +781,11 @@ function toInvitationDTO(inv) {
739
781
  workspaceId: (0, import_parser.stringOrEmpty)(inv.workspaceId),
740
782
  email: (0, import_parser.stringOrEmpty)(inv.email),
741
783
  roleId: (0, import_parser.stringOrEmpty)(inv.roleId),
742
- token: (0, import_parser.stringOrEmpty)(inv.token),
784
+ // The accept token is a bearer credential delivered to the INVITEE by
785
+ // email — never expose it through the API. Listing invitations used to
786
+ // leak it, letting any member accept (hijack) someone else's pending
787
+ // invite. The field stays for DTO-shape compatibility, always empty.
788
+ token: "",
743
789
  status: (0, import_parser.stringOrEmpty)(inv.status),
744
790
  expiresAt: (0, import_parser.dateOrEmpty)(inv.expiresAt),
745
791
  createdAt: (0, import_parser.dateOrEmpty)(inv.createdAt)
@@ -763,9 +809,9 @@ function workspaceController(store, config) {
763
809
  return {
764
810
  async list(ctx) {
765
811
  if (!ctx.user)
766
- return (0, import_core2.setApiResponse)(import_core2.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Authentication required");
812
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Authentication required");
767
813
  const list = await workspaces.findByUserId(ctx.user.id);
768
- return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACES_FETCHED", "Workspaces retrieved successfully.", {
814
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "WORKSPACES_FETCHED", "Workspaces retrieved successfully.", {
769
815
  workspaces: list.map(toWorkspaceDTO)
770
816
  });
771
817
  },
@@ -775,11 +821,11 @@ function workspaceController(store, config) {
775
821
  const description = body?.["description"];
776
822
  const type = body?.["type"];
777
823
  if (typeof name2 !== "string" || name2.trim().length === 0) {
778
- return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
824
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
779
825
  }
780
826
  if (type === "PERSONAL") {
781
- return (0, import_core2.setApiResponse)(
782
- import_core2.HTTP.UNPROCESSABLE,
827
+ return (0, import_core3.setApiResponse)(
828
+ import_core3.HTTP.UNPROCESSABLE,
783
829
  "INVALID_PARAMETER",
784
830
  "Personal workspaces are created automatically"
785
831
  );
@@ -802,18 +848,18 @@ function workspaceController(store, config) {
802
848
  await memModel.add({ userId: ctx.user.id, workspaceId: ws.id, roleId: adminRole.id });
803
849
  return ws;
804
850
  });
805
- return (0, import_core2.setApiResponse)(import_core2.HTTP.CREATED, "WORKSPACE_CREATED", "Workspace created successfully.", {
851
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.CREATED, "WORKSPACE_CREATED", "Workspace created successfully.", {
806
852
  workspace: toWorkspaceDTO(workspace)
807
853
  });
808
854
  },
809
855
  async get(ctx) {
810
- if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
811
- return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_FETCHED", "Workspace retrieved successfully.", {
856
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
857
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "WORKSPACE_FETCHED", "Workspace retrieved successfully.", {
812
858
  workspace: toWorkspaceDTO(ctx.workspace)
813
859
  });
814
860
  },
815
861
  async update(ctx) {
816
- if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
862
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
817
863
  const body = ctx.meta["body"];
818
864
  const opts = {};
819
865
  if (typeof body?.["name"] === "string") opts.name = body["name"].trim();
@@ -828,33 +874,33 @@ function workspaceController(store, config) {
828
874
  if (body?.["address"] !== void 0 && typeof body["address"] === "object")
829
875
  opts.address = body["address"] ?? null;
830
876
  const workspace = await workspaces.update(ctx.workspace.id, opts);
831
- if (!workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
832
- return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_UPDATED", "Workspace updated successfully.", {
877
+ if (!workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
878
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "WORKSPACE_UPDATED", "Workspace updated successfully.", {
833
879
  workspace: toWorkspaceDTO(workspace)
834
880
  });
835
881
  },
836
882
  async archive(ctx) {
837
- if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
883
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
838
884
  if (ctx.workspace.isPersonal) {
839
- return (0, import_core2.setApiResponse)(
840
- import_core2.HTTP.FORBIDDEN,
885
+ return (0, import_core3.setApiResponse)(
886
+ import_core3.HTTP.FORBIDDEN,
841
887
  "FORBIDDEN",
842
888
  "Personal workspaces cannot be archived"
843
889
  );
844
890
  }
845
891
  await workspaces.archive(ctx.workspace.id, ctx.user.id);
846
- return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_ARCHIVED", "Workspace archived successfully.");
892
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "WORKSPACE_ARCHIVED", "Workspace archived successfully.");
847
893
  },
848
894
  async restore(ctx) {
849
- if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
895
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
850
896
  await workspaces.restore(ctx.workspace.id);
851
- return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_RESTORED", "Workspace restored successfully.");
897
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "WORKSPACE_RESTORED", "Workspace restored successfully.");
852
898
  },
853
899
  async getSettings(ctx) {
854
- if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
900
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
855
901
  const settings = await workspaces.getSettings(ctx.workspace.id);
856
- return (0, import_core2.setApiResponse)(
857
- import_core2.HTTP.OK,
902
+ return (0, import_core3.setApiResponse)(
903
+ import_core3.HTTP.OK,
858
904
  "SETTINGS_FETCHED",
859
905
  "Workspace settings retrieved successfully.",
860
906
  {
@@ -863,18 +909,18 @@ function workspaceController(store, config) {
863
909
  );
864
910
  },
865
911
  async updateSettings(ctx) {
866
- if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
912
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
867
913
  const body = ctx.meta["body"];
868
914
  if (!body || Object.keys(body).length === 0) {
869
- return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "No settings provided");
915
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "No settings provided");
870
916
  }
871
917
  const patch = {};
872
918
  for (const key of ["locale", "timezone", "currency", "dateFormat", "timeFormat"]) {
873
919
  if (typeof body[key] === "string") patch[key] = body[key];
874
920
  }
875
921
  const settings = await workspaces.updateSettings(ctx.workspace.id, patch);
876
- return (0, import_core2.setApiResponse)(
877
- import_core2.HTTP.OK,
922
+ return (0, import_core3.setApiResponse)(
923
+ import_core3.HTTP.OK,
878
924
  "SETTINGS_UPDATED",
879
925
  "Workspace settings updated successfully.",
880
926
  {
@@ -886,22 +932,22 @@ function workspaceController(store, config) {
886
932
  }
887
933
 
888
934
  // src/controllers/member.controller.ts
889
- var import_core3 = require("@fonderie/core");
935
+ var import_core4 = require("@fonderie/core");
890
936
  function memberController(store) {
891
937
  const members = new MemberModel(store);
892
938
  return {
893
939
  async list(ctx) {
894
- if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
940
+ if (!ctx.workspace) return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
895
941
  const list = await members.list(ctx.workspace.id);
896
- return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "MEMBERS_FETCHED", "Members retrieved successfully.", {
942
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MEMBERS_FETCHED", "Members retrieved successfully.", {
897
943
  members: list.map(toMemberDTO)
898
944
  });
899
945
  },
900
946
  async remove(ctx) {
901
- if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
947
+ if (!ctx.workspace) return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
902
948
  if (ctx.workspace.isPersonal) {
903
- return (0, import_core3.setApiResponse)(
904
- import_core3.HTTP.FORBIDDEN,
949
+ return (0, import_core4.setApiResponse)(
950
+ import_core4.HTTP.FORBIDDEN,
905
951
  "FORBIDDEN",
906
952
  "Personal workspaces do not support member management"
907
953
  );
@@ -909,78 +955,86 @@ function memberController(store) {
909
955
  const params = ctx.meta["params"];
910
956
  const userId = params?.["userId"];
911
957
  if (!userId)
912
- return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
958
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
913
959
  if (userId === ctx.user?.id) {
914
- return (0, import_core3.setApiResponse)(import_core3.HTTP.BAD_REQUEST, "INVALID_OPERATION", "Cannot remove yourself");
960
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "INVALID_OPERATION", "Cannot remove yourself");
961
+ }
962
+ const ownerId = ctx.workspace.ownerId;
963
+ if (ownerId && userId === ownerId) {
964
+ return (0, import_core4.setApiResponse)(
965
+ import_core4.HTTP.BAD_REQUEST,
966
+ "INVALID_OPERATION",
967
+ "Cannot remove the workspace owner"
968
+ );
915
969
  }
916
970
  await members.remove(userId, ctx.workspace.id);
917
- return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "MEMBER_REMOVED", "Member removed successfully.");
971
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MEMBER_REMOVED", "Member removed successfully.");
918
972
  },
919
973
  async getUserRoles(ctx) {
920
- if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
974
+ if (!ctx.workspace) return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
921
975
  const params = ctx.meta["params"];
922
976
  const userId = params?.["userId"];
923
977
  if (!userId)
924
- return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
978
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
925
979
  const roles = await members.getUserRoles(userId, ctx.workspace.id);
926
- return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ROLES_FETCHED", "Member roles retrieved successfully.", {
980
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLES_FETCHED", "Member roles retrieved successfully.", {
927
981
  roles: roles.map(toRoleDTO)
928
982
  });
929
983
  },
930
984
  async addRole(ctx) {
931
- if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
985
+ if (!ctx.workspace) return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
932
986
  const params = ctx.meta["params"];
933
987
  const body = ctx.meta["body"];
934
988
  const userId = params?.["userId"];
935
989
  const roleId = body?.["roleId"] ?? params?.["roleId"];
936
990
  if (!userId)
937
- return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
991
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
938
992
  if (!roleId)
939
- return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
993
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
940
994
  const assigned = await members.addRole(userId, ctx.workspace.id, roleId);
941
995
  if (!assigned) {
942
- return (0, import_core3.setApiResponse)(
943
- import_core3.HTTP.UNPROCESSABLE,
996
+ return (0, import_core4.setApiResponse)(
997
+ import_core4.HTTP.UNPROCESSABLE,
944
998
  "INVALID_ROLE",
945
999
  "Role is not assignable in this workspace."
946
1000
  );
947
1001
  }
948
- return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ROLE_ASSIGNED", "Role assigned successfully.");
1002
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_ASSIGNED", "Role assigned successfully.");
949
1003
  },
950
1004
  async removeRole(ctx) {
951
- if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1005
+ if (!ctx.workspace) return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
952
1006
  const params = ctx.meta["params"];
953
1007
  const userId = params?.["userId"];
954
1008
  const roleId = params?.["roleId"];
955
1009
  if (!userId)
956
- return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
1010
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
957
1011
  if (!roleId)
958
- return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1012
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
959
1013
  try {
960
1014
  await members.removeRole(userId, ctx.workspace.id, roleId);
961
- return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ROLE_REMOVED", "Role removed successfully.");
1015
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_REMOVED", "Role removed successfully.");
962
1016
  } catch (err) {
963
1017
  const message = err instanceof Error ? err.message : "Failed";
964
- return (0, import_core3.setApiResponse)(import_core3.HTTP.BAD_REQUEST, "OPERATION_FAILED", message);
1018
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "OPERATION_FAILED", message);
965
1019
  }
966
1020
  }
967
1021
  };
968
1022
  }
969
1023
 
970
1024
  // src/controllers/role.controller.ts
971
- var import_core4 = require("@fonderie/core");
1025
+ var import_core5 = require("@fonderie/core");
972
1026
  function roleController(store) {
973
1027
  const roles = new RoleModel(store);
974
1028
  return {
975
1029
  async create(ctx) {
976
1030
  if (!ctx.workspace) {
977
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1031
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
978
1032
  }
979
1033
  const body = ctx.meta["body"];
980
1034
  const name2 = body?.["name"];
981
1035
  const description = body?.["description"];
982
1036
  if (typeof name2 !== "string" || name2.trim().length === 0) {
983
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
1037
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
984
1038
  }
985
1039
  try {
986
1040
  const opts = {
@@ -991,49 +1045,49 @@ function roleController(store) {
991
1045
  opts.description = description;
992
1046
  }
993
1047
  const role = await roles.create(opts);
994
- return (0, import_core4.setApiResponse)(import_core4.HTTP.CREATED, "ROLE_CREATED", "Role created successfully.", {
1048
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.CREATED, "ROLE_CREATED", "Role created successfully.", {
995
1049
  role: toRoleDTO(role)
996
1050
  });
997
1051
  } catch (err) {
998
1052
  const message = err instanceof Error ? err.message : "Failed to create role";
999
- return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "OPERATION_FAILED", message);
1053
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "OPERATION_FAILED", message);
1000
1054
  }
1001
1055
  },
1002
1056
  async list(ctx) {
1003
1057
  if (!ctx.workspace) {
1004
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1058
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1005
1059
  }
1006
1060
  const list = await roles.list(ctx.workspace.id);
1007
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLES_FETCHED", "Roles retrieved successfully.", {
1061
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "ROLES_FETCHED", "Roles retrieved successfully.", {
1008
1062
  roles: list.map(toRoleDTO)
1009
1063
  });
1010
1064
  },
1011
1065
  async get(ctx) {
1012
1066
  if (!ctx.workspace) {
1013
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1067
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1014
1068
  }
1015
1069
  const params = ctx.meta["params"];
1016
1070
  const roleId = params?.["roleId"];
1017
1071
  if (!roleId) {
1018
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1072
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1019
1073
  }
1020
1074
  const role = await roles.findById(roleId, ctx.workspace.id);
1021
1075
  if (!role) {
1022
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
1076
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
1023
1077
  }
1024
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_FETCHED", "Role retrieved successfully.", {
1078
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "ROLE_FETCHED", "Role retrieved successfully.", {
1025
1079
  role: toRoleDTO(role)
1026
1080
  });
1027
1081
  },
1028
1082
  async update(ctx) {
1029
1083
  if (!ctx.workspace) {
1030
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1084
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1031
1085
  }
1032
1086
  const params = ctx.meta["params"];
1033
1087
  const body = ctx.meta["body"];
1034
1088
  const roleId = params?.["roleId"];
1035
1089
  if (!roleId) {
1036
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1090
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1037
1091
  }
1038
1092
  const opts = {};
1039
1093
  if (typeof body?.["name"] === "string") {
@@ -1050,60 +1104,60 @@ function roleController(store) {
1050
1104
  }
1051
1105
  const role = await roles.update(roleId, ctx.workspace.id, opts);
1052
1106
  if (!role) {
1053
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found or is a system role");
1107
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found or is a system role");
1054
1108
  }
1055
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_UPDATED", "Role updated successfully.", {
1109
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "ROLE_UPDATED", "Role updated successfully.", {
1056
1110
  role: toRoleDTO(role)
1057
1111
  });
1058
1112
  },
1059
1113
  async remove(ctx) {
1060
1114
  if (!ctx.workspace) {
1061
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1115
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1062
1116
  }
1063
1117
  const params = ctx.meta["params"];
1064
1118
  const roleId = params?.["roleId"];
1065
1119
  if (!roleId) {
1066
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1120
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1067
1121
  }
1068
1122
  await roles.delete(roleId, ctx.workspace.id);
1069
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_DELETED", "Role deleted successfully.");
1123
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "ROLE_DELETED", "Role deleted successfully.");
1070
1124
  },
1071
1125
  async getPermissions(ctx) {
1072
1126
  if (!ctx.workspace) {
1073
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1127
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1074
1128
  }
1075
1129
  const params = ctx.meta["params"];
1076
1130
  const roleId = params?.["roleId"];
1077
1131
  if (!roleId) {
1078
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1132
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1079
1133
  }
1080
1134
  const role = await roles.findById(roleId, ctx.workspace.id);
1081
1135
  if (!role) {
1082
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
1136
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
1083
1137
  }
1084
1138
  const permissions = await roles.getPermissions(roleId, ctx.workspace.id);
1085
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "PERMISSIONS_FETCHED", "Role permissions retrieved successfully.", {
1139
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PERMISSIONS_FETCHED", "Role permissions retrieved successfully.", {
1086
1140
  permissions
1087
1141
  });
1088
1142
  },
1089
1143
  async setPermissions(ctx) {
1090
1144
  if (!ctx.workspace) {
1091
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1145
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1092
1146
  }
1093
1147
  const params = ctx.meta["params"];
1094
1148
  const body = ctx.meta["body"];
1095
1149
  const roleId = params?.["roleId"];
1096
1150
  if (!roleId) {
1097
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1151
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1098
1152
  }
1099
1153
  const target = await roles.findById(roleId, ctx.workspace.id);
1100
1154
  if (!target || target.isSystem) {
1101
- return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
1155
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
1102
1156
  }
1103
1157
  const perms = body?.["permissions"];
1104
1158
  if (!Array.isArray(perms)) {
1105
- return (0, import_core4.setApiResponse)(
1106
- import_core4.HTTP.UNPROCESSABLE,
1159
+ return (0, import_core5.setApiResponse)(
1160
+ import_core5.HTTP.UNPROCESSABLE,
1107
1161
  "INVALID_PARAMETER",
1108
1162
  "permissions array is required"
1109
1163
  );
@@ -1119,13 +1173,13 @@ function roleController(store) {
1119
1173
  };
1120
1174
  }).filter((p) => p.permissionKey.length > 0);
1121
1175
  await roles.setPermissions(roleId, ctx.workspace.id, normalized);
1122
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "PERMISSIONS_SET", "Role permissions updated successfully.");
1176
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PERMISSIONS_SET", "Role permissions updated successfully.");
1123
1177
  }
1124
1178
  };
1125
1179
  }
1126
1180
 
1127
1181
  // src/controllers/invitation.controller.ts
1128
- var import_core5 = require("@fonderie/core");
1182
+ var import_core6 = require("@fonderie/core");
1129
1183
  var import_events = require("@fonderie/events");
1130
1184
 
1131
1185
  // src/services/invitations.ts
@@ -1134,7 +1188,7 @@ function generateToken() {
1134
1188
  return (0, import_node_crypto.randomBytes)(32).toString("hex");
1135
1189
  }
1136
1190
  function generatePin() {
1137
- return Math.floor(1e5 + Math.random() * 9e5).toString();
1191
+ return (0, import_node_crypto.randomInt)(1e5, 1e6).toString();
1138
1192
  }
1139
1193
  function parseTtl(ttl) {
1140
1194
  const units = {
@@ -1205,8 +1259,8 @@ async function acceptInvitationByPin(opts, store) {
1205
1259
  const [inv] = await store.query(
1206
1260
  `SELECT id, workspace_id AS "workspaceId", role_id AS "roleId", expires_at AS "expiresAt"
1207
1261
  FROM fonderie_workspace_invitations
1208
- WHERE pin = $1 AND status = 'PENDING'`,
1209
- [opts.pin]
1262
+ WHERE pin = $1 AND lower(email) = lower($2) AND status = 'PENDING'`,
1263
+ [opts.pin, opts.email]
1210
1264
  );
1211
1265
  if (!inv) throw new Error("Invalid PIN");
1212
1266
  if (/* @__PURE__ */ new Date() > new Date(inv.expiresAt)) throw new Error("Invitation expired");
@@ -1226,6 +1280,31 @@ async function acceptInvitationByPin(opts, store) {
1226
1280
  });
1227
1281
  return { workspaceId: inv.workspaceId, roleId: inv.roleId };
1228
1282
  }
1283
+ async function acceptInvitationByToken(token, userId, store) {
1284
+ const [inv] = await store.query(
1285
+ `SELECT id, workspace_id AS "workspaceId", role_id AS "roleId", expires_at AS "expiresAt"
1286
+ FROM fonderie_workspace_invitations
1287
+ WHERE token = $1 AND status = 'PENDING'`,
1288
+ [token]
1289
+ );
1290
+ if (!inv) throw new Error("Invalid token");
1291
+ if (/* @__PURE__ */ new Date() > new Date(inv.expiresAt)) throw new Error("Invitation expired");
1292
+ await store.transaction(async (tx) => {
1293
+ await Promise.all([
1294
+ tx.query(
1295
+ `INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
1296
+ VALUES ($1, $2, $3, true)
1297
+ ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
1298
+ SET confirmed = true, removed = false`,
1299
+ [userId, inv.workspaceId, inv.roleId]
1300
+ ),
1301
+ tx.query(`UPDATE fonderie_workspace_invitations SET status = 'ACCEPTED' WHERE id = $1`, [
1302
+ inv.id
1303
+ ])
1304
+ ]);
1305
+ });
1306
+ return { workspaceId: inv.workspaceId, roleId: inv.roleId };
1307
+ }
1229
1308
 
1230
1309
  // src/models/invitation.model.ts
1231
1310
  var InvitationModel = class {
@@ -1245,6 +1324,9 @@ var InvitationModel = class {
1245
1324
  acceptByPin(opts) {
1246
1325
  return acceptInvitationByPin(opts, this.store);
1247
1326
  }
1327
+ acceptByToken(token, userId) {
1328
+ return acceptInvitationByToken(token, userId, this.store);
1329
+ }
1248
1330
  };
1249
1331
 
1250
1332
  // src/controllers/invitation.controller.ts
@@ -1258,17 +1340,17 @@ function invitationController(store, ttl, bus) {
1258
1340
  const invitations = new InvitationModel(store);
1259
1341
  return {
1260
1342
  async list(ctx) {
1261
- if (!ctx.workspace) return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1343
+ if (!ctx.workspace) return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1262
1344
  const list = await invitations.list(ctx.workspace.id);
1263
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "INVITATIONS_FETCHED", "Invitations retrieved successfully.", {
1345
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "INVITATIONS_FETCHED", "Invitations retrieved successfully.", {
1264
1346
  invitations: list.map(toInvitationDTO)
1265
1347
  });
1266
1348
  },
1267
1349
  async invite(ctx) {
1268
- if (!ctx.workspace) return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1350
+ if (!ctx.workspace) return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1269
1351
  if (ctx.workspace.isPersonal) {
1270
- return (0, import_core5.setApiResponse)(
1271
- import_core5.HTTP.FORBIDDEN,
1352
+ return (0, import_core6.setApiResponse)(
1353
+ import_core6.HTTP.FORBIDDEN,
1272
1354
  "FORBIDDEN",
1273
1355
  "Personal workspaces do not support invitations"
1274
1356
  );
@@ -1276,11 +1358,11 @@ function invitationController(store, ttl, bus) {
1276
1358
  const body = ctx.meta["body"];
1277
1359
  const entries = Array.isArray(body) ? body : [body];
1278
1360
  if (!entries.length) {
1279
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "at least one invite is required");
1361
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "at least one invite is required");
1280
1362
  }
1281
1363
  for (const entry of entries) {
1282
1364
  if (typeof entry?.["email"] !== "string") {
1283
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required for every invite");
1365
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required for every invite");
1284
1366
  }
1285
1367
  }
1286
1368
  const seatLimit = seatLimitFromMeta(ctx);
@@ -1293,8 +1375,8 @@ function invitationController(store, ttl, bus) {
1293
1375
  const total = parseInt(countRow.count, 10);
1294
1376
  const occupied = ctx.workspace.isPersonal ? total : Math.max(0, total - 1);
1295
1377
  if (occupied + entries.length > seatLimit) {
1296
- return (0, import_core5.setApiResponse)(
1297
- import_core5.HTTP.PAYMENT_REQUIRED,
1378
+ return (0, import_core6.setApiResponse)(
1379
+ import_core6.HTTP.PAYMENT_REQUIRED,
1298
1380
  "SEAT_LIMIT_REACHED",
1299
1381
  `Your plan allows ${seatLimit} seat${seatLimit === 1 ? "" : "s"}. Upgrade to invite more members.`,
1300
1382
  { limit: seatLimit }
@@ -1311,7 +1393,7 @@ function invitationController(store, ttl, bus) {
1311
1393
  );
1312
1394
  defaultRoleId = row?.id;
1313
1395
  if (!defaultRoleId) {
1314
- return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Default role not found");
1396
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Default role not found");
1315
1397
  }
1316
1398
  }
1317
1399
  const results = await Promise.all(
@@ -1333,33 +1415,48 @@ function invitationController(store, ttl, bus) {
1333
1415
  return { invitationId: invitation.id, email: email2 };
1334
1416
  })
1335
1417
  );
1336
- return (0, import_core5.setApiResponse)(import_core5.HTTP.CREATED, "INVITATIONS_SENT", "Invitations sent successfully.", {
1418
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.CREATED, "INVITATIONS_SENT", "Invitations sent successfully.", {
1337
1419
  invitations: results
1338
1420
  });
1339
1421
  },
1340
1422
  async cancel(ctx) {
1341
- if (!ctx.workspace) return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1423
+ if (!ctx.workspace) return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1342
1424
  const params = ctx.meta["params"];
1343
1425
  const invitationId = params?.["inviteId"];
1344
1426
  if (!invitationId)
1345
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "inviteId is required");
1427
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "inviteId is required");
1346
1428
  await invitations.cancel(invitationId, ctx.workspace.id);
1347
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "INVITATION_CANCELLED", "Invitation cancelled successfully.");
1429
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "INVITATION_CANCELLED", "Invitation cancelled successfully.");
1348
1430
  },
1349
1431
  async accept(ctx) {
1350
1432
  const body = ctx.meta["body"];
1351
1433
  const pin = body?.["pin"];
1352
- if (typeof pin !== "string") {
1353
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "pin is required");
1354
- }
1434
+ const token = body?.["token"];
1355
1435
  try {
1356
- const { workspaceId } = await invitations.acceptByPin({ pin, userId: ctx.user.id });
1357
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1436
+ if (typeof token === "string") {
1437
+ const { workspaceId: workspaceId2 } = await invitations.acceptByToken(token, ctx.user.id);
1438
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1439
+ workspaceId: workspaceId2
1440
+ });
1441
+ }
1442
+ if (typeof pin !== "string") {
1443
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "pin or token is required");
1444
+ }
1445
+ const email2 = ctx.user.email;
1446
+ if (!email2) {
1447
+ return (0, import_core6.setApiResponse)(
1448
+ import_core6.HTTP.BAD_REQUEST,
1449
+ "INVITATION_FAILED",
1450
+ "This account has no email address \u2014 use the invitation link instead of the PIN"
1451
+ );
1452
+ }
1453
+ const { workspaceId } = await invitations.acceptByPin({ pin, userId: ctx.user.id, email: email2 });
1454
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1358
1455
  workspaceId
1359
1456
  });
1360
1457
  } catch (err) {
1361
1458
  const message = err instanceof Error ? err.message : "Invalid invitation";
1362
- return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "INVITATION_FAILED", message);
1459
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "INVITATION_FAILED", message);
1363
1460
  }
1364
1461
  }
1365
1462
  };
@@ -1369,6 +1466,12 @@ function invitationController(store, ttl, bus) {
1369
1466
  function buildWorkspaceRoutes(store, config, bus) {
1370
1467
  const ttl = config.invitationTtl ?? "7d";
1371
1468
  const wsCtx = withWorkspace(store);
1469
+ const manager = requireManager(store, config);
1470
+ const acceptLimit = (0, import_rate_limit.rateLimit)({
1471
+ store: new import_rate_limit.StoreAdapterStore(store),
1472
+ rule: { capacity: 10, refillPerSec: 10 / (15 * 60) },
1473
+ key: (0, import_rate_limit.byIp)("workspaces:invitation-accept")
1474
+ });
1372
1475
  const workspace = workspaceController(store, config);
1373
1476
  const member = memberController(store);
1374
1477
  const role = roleController(store);
@@ -1385,34 +1488,34 @@ function buildWorkspaceRoutes(store, config, bus) {
1385
1488
  R("listWorkspaces", "GET", "/workspaces", import_middlewares.requireAuth, workspace.list),
1386
1489
  // ── Members (workspace resolved from X-Workspace-ID header)
1387
1490
  R("listMembers", "GET", "/workspaces/members", import_middlewares.requireAuth, wsCtx, member.list),
1388
- R("removeMember", "DELETE", "/workspaces/members/:userId", import_middlewares.requireAuth, wsCtx, member.remove),
1491
+ R("removeMember", "DELETE", "/workspaces/members/:userId", import_middlewares.requireAuth, wsCtx, manager, member.remove),
1389
1492
  R("getMemberRoles", "GET", "/workspaces/members/:userId/roles", import_middlewares.requireAuth, wsCtx, member.getUserRoles),
1390
- R("addMemberRole", "POST", "/workspaces/members/:userId/roles", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(addMemberRoleSchema), member.addRole),
1391
- R("removeMemberRole", "DELETE", "/workspaces/members/:userId/roles/:roleId", import_middlewares.requireAuth, wsCtx, member.removeRole),
1493
+ R("addMemberRole", "POST", "/workspaces/members/:userId/roles", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(addMemberRoleSchema), member.addRole),
1494
+ R("removeMemberRole", "DELETE", "/workspaces/members/:userId/roles/:roleId", import_middlewares.requireAuth, wsCtx, manager, member.removeRole),
1392
1495
  // ── Invitations
1393
1496
  R("listInvitations", "GET", "/workspaces/invitations", import_middlewares.requireAuth, wsCtx, invitation.list),
1394
- R("invite", "POST", "/workspaces/invitations", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(createInvitationsSchema), invitation.invite),
1395
- R("cancelInvitation", "DELETE", "/workspaces/invitations/:inviteId", import_middlewares.requireAuth, wsCtx, invitation.cancel),
1396
- R("acceptInvitation", "POST", "/workspaces/invitations/accept", import_middlewares.requireAuth, (0, import_middlewares.validate)(acceptInvitationSchema), invitation.accept),
1497
+ R("invite", "POST", "/workspaces/invitations", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(createInvitationsSchema), invitation.invite),
1498
+ R("cancelInvitation", "DELETE", "/workspaces/invitations/:inviteId", import_middlewares.requireAuth, wsCtx, manager, invitation.cancel),
1499
+ R("acceptInvitation", "POST", "/workspaces/invitations/accept", acceptLimit, import_middlewares.requireAuth, (0, import_middlewares.validate)(acceptInvitationSchema), invitation.accept),
1397
1500
  // ── Roles
1398
- R("createRole", "POST", "/workspaces/roles", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(createRoleSchema), role.create),
1501
+ R("createRole", "POST", "/workspaces/roles", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(createRoleSchema), role.create),
1399
1502
  R("listRoles", "GET", "/workspaces/roles", import_middlewares.requireAuth, wsCtx, role.list),
1400
1503
  R("getRole", "GET", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, role.get),
1401
- R("updateRole", "PUT", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(updateRoleSchema), role.update),
1402
- R("removeRole", "DELETE", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, role.remove),
1504
+ R("updateRole", "PUT", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(updateRoleSchema), role.update),
1505
+ R("removeRole", "DELETE", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, manager, role.remove),
1403
1506
  R("getRolePermissions", "GET", "/workspaces/roles/:roleId/permissions", import_middlewares.requireAuth, wsCtx, role.getPermissions),
1404
- R("setRolePermissions", "POST", "/workspaces/roles/:roleId/permissions", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(setRolePermissionsSchema), role.setPermissions),
1507
+ R("setRolePermissions", "POST", "/workspaces/roles/:roleId/permissions", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(setRolePermissionsSchema), role.setPermissions),
1405
1508
  // ── Workspace lifecycle
1406
- R("archive", "POST", "/workspaces/archive", import_middlewares.requireAuth, wsCtx, workspace.archive),
1407
- R("restore", "POST", "/workspaces/restore", import_middlewares.requireAuth, wsCtx, workspace.restore),
1509
+ R("archive", "POST", "/workspaces/archive", import_middlewares.requireAuth, wsCtx, manager, workspace.archive),
1510
+ R("restore", "POST", "/workspaces/restore", import_middlewares.requireAuth, wsCtx, manager, workspace.restore),
1408
1511
  R("getSettings", "GET", "/workspaces/settings", import_middlewares.requireAuth, wsCtx, workspace.getSettings),
1409
- R("updateSettings", "PUT", "/workspaces/settings", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(updateSettingsSchema), workspace.updateSettings),
1512
+ R("updateSettings", "PUT", "/workspaces/settings", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(updateSettingsSchema), workspace.updateSettings),
1410
1513
  // ── Path-based lookup by ID (admin / cross-workspace use)
1411
1514
  R("getWorkspace", "GET", "/workspaces/:id", import_middlewares.requireAuth, wsCtx, workspace.get),
1412
1515
  // ── Update current workspace — ID from :id path param (wsCtx) or the
1413
1516
  // X-Workspace-ID header (or personal-workspace fallback when absent). Set
1414
1517
  // routes.updateWorkspace = '/workspaces/:id' to match a path-id frontend.
1415
- R("updateWorkspace", "PUT", "/workspaces", import_middlewares.requireAuth, wsCtx, (0, import_middlewares.validate)(updateWorkspaceSchema), workspace.update)
1518
+ R("updateWorkspace", "PUT", "/workspaces", import_middlewares.requireAuth, wsCtx, manager, (0, import_middlewares.validate)(updateWorkspaceSchema), workspace.update)
1416
1519
  ];
1417
1520
  }
1418
1521
 
@@ -1496,10 +1599,10 @@ var SAMPLE_PAYLOADS = {
1496
1599
  };
1497
1600
 
1498
1601
  // src/middlewares/require-workspace.ts
1499
- var import_core6 = require("@fonderie/core");
1602
+ var import_core7 = require("@fonderie/core");
1500
1603
  var requireWorkspace = async (ctx, next) => {
1501
1604
  if (!ctx.workspace) {
1502
- return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
1605
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
1503
1606
  }
1504
1607
  return next();
1505
1608
  };
@@ -1612,6 +1715,7 @@ function workspaceExportContributor(store) {
1612
1715
  importMembership,
1613
1716
  importRole,
1614
1717
  importWorkspace,
1718
+ requireManager,
1615
1719
  requireWorkspace,
1616
1720
  schemas,
1617
1721
  toInvitationDTO,