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