@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.js CHANGED
@@ -14,6 +14,7 @@ var EVENT_KEYS = {
14
14
 
15
15
  // src/routes.ts
16
16
  import { requireAuth, validate } from "@fonderie/core/middlewares";
17
+ import { byIp, rateLimit, StoreAdapterStore } from "@fonderie/rate-limit";
17
18
 
18
19
  // src/schemas.ts
19
20
  var schemas_exports = {};
@@ -70,7 +71,10 @@ var createInvitationsSchema = z.union([
70
71
  inviteEntry,
71
72
  z.array(inviteEntry).min(1, "at least one invite is required")
72
73
  ]);
73
- var acceptInvitationSchema = z.object({ pin: z.string().trim().min(1, "pin is required") });
74
+ var acceptInvitationSchema = z.union([
75
+ z.object({ pin: z.string().trim().min(1, "pin is required") }),
76
+ z.object({ token: z.string().trim().min(1, "token is required") })
77
+ ]);
74
78
  var createRoleSchema = z.object({
75
79
  name,
76
80
  description: z.string().max(1e3).optional()
@@ -423,8 +427,45 @@ function withWorkspace(store, ctx, next) {
423
427
  return handler;
424
428
  }
425
429
 
426
- // src/controllers/workspace.controller.ts
430
+ // src/middlewares/require-manager.ts
427
431
  import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
432
+ function requireManager(store, config) {
433
+ const managerRoles = config.managerRoles ?? ["ADMIN"];
434
+ return async (ctx, next) => {
435
+ if (config.management === "any-member") return next();
436
+ if (!ctx.user) {
437
+ return setApiResponse2(HTTP2.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
438
+ }
439
+ if (!ctx.workspace) return next();
440
+ const ownerId = ctx.workspace.ownerId;
441
+ if (ownerId && ownerId === ctx.user.id) return next();
442
+ const [row] = await store.query(
443
+ `SELECT 1 AS ok
444
+ FROM fonderie_role_user_workspaces ruw
445
+ JOIN fonderie_roles r ON r.id = ruw.role_id
446
+ WHERE ruw.user_id = $1
447
+ AND ruw.workspace_id = $2
448
+ AND ruw.removed = false
449
+ AND ruw.suspended = false
450
+ AND r.is_system = true
451
+ AND r.active = true
452
+ AND r.name = ANY($3)
453
+ LIMIT 1`,
454
+ [ctx.user.id, ctx.workspace.id, managerRoles]
455
+ );
456
+ if (!row) {
457
+ return setApiResponse2(
458
+ HTTP2.FORBIDDEN,
459
+ "MANAGER_REQUIRED",
460
+ "This action requires the workspace owner or an admin role"
461
+ );
462
+ }
463
+ return next();
464
+ };
465
+ }
466
+
467
+ // src/controllers/workspace.controller.ts
468
+ import { setApiResponse as setApiResponse3, HTTP as HTTP3 } from "@fonderie/core";
428
469
 
429
470
  // src/models/workspace.model.ts
430
471
  var WorkspaceModel = class {
@@ -702,7 +743,11 @@ function toInvitationDTO(inv) {
702
743
  workspaceId: stringOrEmpty(inv.workspaceId),
703
744
  email: stringOrEmpty(inv.email),
704
745
  roleId: stringOrEmpty(inv.roleId),
705
- token: stringOrEmpty(inv.token),
746
+ // The accept token is a bearer credential delivered to the INVITEE by
747
+ // email — never expose it through the API. Listing invitations used to
748
+ // leak it, letting any member accept (hijack) someone else's pending
749
+ // invite. The field stays for DTO-shape compatibility, always empty.
750
+ token: "",
706
751
  status: stringOrEmpty(inv.status),
707
752
  expiresAt: dateOrEmpty(inv.expiresAt),
708
753
  createdAt: dateOrEmpty(inv.createdAt)
@@ -726,9 +771,9 @@ function workspaceController(store, config) {
726
771
  return {
727
772
  async list(ctx) {
728
773
  if (!ctx.user)
729
- return setApiResponse2(HTTP2.UNAUTHORIZED, "UNAUTHORIZED", "Authentication required");
774
+ return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Authentication required");
730
775
  const list = await workspaces.findByUserId(ctx.user.id);
731
- return setApiResponse2(HTTP2.OK, "WORKSPACES_FETCHED", "Workspaces retrieved successfully.", {
776
+ return setApiResponse3(HTTP3.OK, "WORKSPACES_FETCHED", "Workspaces retrieved successfully.", {
732
777
  workspaces: list.map(toWorkspaceDTO)
733
778
  });
734
779
  },
@@ -738,11 +783,11 @@ function workspaceController(store, config) {
738
783
  const description = body?.["description"];
739
784
  const type = body?.["type"];
740
785
  if (typeof name2 !== "string" || name2.trim().length === 0) {
741
- return setApiResponse2(HTTP2.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
786
+ return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
742
787
  }
743
788
  if (type === "PERSONAL") {
744
- return setApiResponse2(
745
- HTTP2.UNPROCESSABLE,
789
+ return setApiResponse3(
790
+ HTTP3.UNPROCESSABLE,
746
791
  "INVALID_PARAMETER",
747
792
  "Personal workspaces are created automatically"
748
793
  );
@@ -765,18 +810,18 @@ function workspaceController(store, config) {
765
810
  await memModel.add({ userId: ctx.user.id, workspaceId: ws.id, roleId: adminRole.id });
766
811
  return ws;
767
812
  });
768
- return setApiResponse2(HTTP2.CREATED, "WORKSPACE_CREATED", "Workspace created successfully.", {
813
+ return setApiResponse3(HTTP3.CREATED, "WORKSPACE_CREATED", "Workspace created successfully.", {
769
814
  workspace: toWorkspaceDTO(workspace)
770
815
  });
771
816
  },
772
817
  async get(ctx) {
773
- if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
774
- return setApiResponse2(HTTP2.OK, "WORKSPACE_FETCHED", "Workspace retrieved successfully.", {
818
+ if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
819
+ return setApiResponse3(HTTP3.OK, "WORKSPACE_FETCHED", "Workspace retrieved successfully.", {
775
820
  workspace: toWorkspaceDTO(ctx.workspace)
776
821
  });
777
822
  },
778
823
  async update(ctx) {
779
- if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
824
+ if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
780
825
  const body = ctx.meta["body"];
781
826
  const opts = {};
782
827
  if (typeof body?.["name"] === "string") opts.name = body["name"].trim();
@@ -791,33 +836,33 @@ function workspaceController(store, config) {
791
836
  if (body?.["address"] !== void 0 && typeof body["address"] === "object")
792
837
  opts.address = body["address"] ?? null;
793
838
  const workspace = await workspaces.update(ctx.workspace.id, opts);
794
- if (!workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
795
- return setApiResponse2(HTTP2.OK, "WORKSPACE_UPDATED", "Workspace updated successfully.", {
839
+ if (!workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
840
+ return setApiResponse3(HTTP3.OK, "WORKSPACE_UPDATED", "Workspace updated successfully.", {
796
841
  workspace: toWorkspaceDTO(workspace)
797
842
  });
798
843
  },
799
844
  async archive(ctx) {
800
- if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
845
+ if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
801
846
  if (ctx.workspace.isPersonal) {
802
- return setApiResponse2(
803
- HTTP2.FORBIDDEN,
847
+ return setApiResponse3(
848
+ HTTP3.FORBIDDEN,
804
849
  "FORBIDDEN",
805
850
  "Personal workspaces cannot be archived"
806
851
  );
807
852
  }
808
853
  await workspaces.archive(ctx.workspace.id, ctx.user.id);
809
- return setApiResponse2(HTTP2.OK, "WORKSPACE_ARCHIVED", "Workspace archived successfully.");
854
+ return setApiResponse3(HTTP3.OK, "WORKSPACE_ARCHIVED", "Workspace archived successfully.");
810
855
  },
811
856
  async restore(ctx) {
812
- if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
857
+ if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
813
858
  await workspaces.restore(ctx.workspace.id);
814
- return setApiResponse2(HTTP2.OK, "WORKSPACE_RESTORED", "Workspace restored successfully.");
859
+ return setApiResponse3(HTTP3.OK, "WORKSPACE_RESTORED", "Workspace restored successfully.");
815
860
  },
816
861
  async getSettings(ctx) {
817
- if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
862
+ if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
818
863
  const settings = await workspaces.getSettings(ctx.workspace.id);
819
- return setApiResponse2(
820
- HTTP2.OK,
864
+ return setApiResponse3(
865
+ HTTP3.OK,
821
866
  "SETTINGS_FETCHED",
822
867
  "Workspace settings retrieved successfully.",
823
868
  {
@@ -826,18 +871,18 @@ function workspaceController(store, config) {
826
871
  );
827
872
  },
828
873
  async updateSettings(ctx) {
829
- if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
874
+ if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
830
875
  const body = ctx.meta["body"];
831
876
  if (!body || Object.keys(body).length === 0) {
832
- return setApiResponse2(HTTP2.UNPROCESSABLE, "INVALID_PARAMETER", "No settings provided");
877
+ return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "No settings provided");
833
878
  }
834
879
  const patch = {};
835
880
  for (const key of ["locale", "timezone", "currency", "dateFormat", "timeFormat"]) {
836
881
  if (typeof body[key] === "string") patch[key] = body[key];
837
882
  }
838
883
  const settings = await workspaces.updateSettings(ctx.workspace.id, patch);
839
- return setApiResponse2(
840
- HTTP2.OK,
884
+ return setApiResponse3(
885
+ HTTP3.OK,
841
886
  "SETTINGS_UPDATED",
842
887
  "Workspace settings updated successfully.",
843
888
  {
@@ -849,22 +894,22 @@ function workspaceController(store, config) {
849
894
  }
850
895
 
851
896
  // src/controllers/member.controller.ts
852
- import { setApiResponse as setApiResponse3, HTTP as HTTP3 } from "@fonderie/core";
897
+ import { setApiResponse as setApiResponse4, HTTP as HTTP4 } from "@fonderie/core";
853
898
  function memberController(store) {
854
899
  const members = new MemberModel(store);
855
900
  return {
856
901
  async list(ctx) {
857
- if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
902
+ if (!ctx.workspace) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
858
903
  const list = await members.list(ctx.workspace.id);
859
- return setApiResponse3(HTTP3.OK, "MEMBERS_FETCHED", "Members retrieved successfully.", {
904
+ return setApiResponse4(HTTP4.OK, "MEMBERS_FETCHED", "Members retrieved successfully.", {
860
905
  members: list.map(toMemberDTO)
861
906
  });
862
907
  },
863
908
  async remove(ctx) {
864
- if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
909
+ if (!ctx.workspace) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
865
910
  if (ctx.workspace.isPersonal) {
866
- return setApiResponse3(
867
- HTTP3.FORBIDDEN,
911
+ return setApiResponse4(
912
+ HTTP4.FORBIDDEN,
868
913
  "FORBIDDEN",
869
914
  "Personal workspaces do not support member management"
870
915
  );
@@ -872,78 +917,86 @@ function memberController(store) {
872
917
  const params = ctx.meta["params"];
873
918
  const userId = params?.["userId"];
874
919
  if (!userId)
875
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
920
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
876
921
  if (userId === ctx.user?.id) {
877
- return setApiResponse3(HTTP3.BAD_REQUEST, "INVALID_OPERATION", "Cannot remove yourself");
922
+ return setApiResponse4(HTTP4.BAD_REQUEST, "INVALID_OPERATION", "Cannot remove yourself");
923
+ }
924
+ const ownerId = ctx.workspace.ownerId;
925
+ if (ownerId && userId === ownerId) {
926
+ return setApiResponse4(
927
+ HTTP4.BAD_REQUEST,
928
+ "INVALID_OPERATION",
929
+ "Cannot remove the workspace owner"
930
+ );
878
931
  }
879
932
  await members.remove(userId, ctx.workspace.id);
880
- return setApiResponse3(HTTP3.OK, "MEMBER_REMOVED", "Member removed successfully.");
933
+ return setApiResponse4(HTTP4.OK, "MEMBER_REMOVED", "Member removed successfully.");
881
934
  },
882
935
  async getUserRoles(ctx) {
883
- if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
936
+ if (!ctx.workspace) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
884
937
  const params = ctx.meta["params"];
885
938
  const userId = params?.["userId"];
886
939
  if (!userId)
887
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
940
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
888
941
  const roles = await members.getUserRoles(userId, ctx.workspace.id);
889
- return setApiResponse3(HTTP3.OK, "ROLES_FETCHED", "Member roles retrieved successfully.", {
942
+ return setApiResponse4(HTTP4.OK, "ROLES_FETCHED", "Member roles retrieved successfully.", {
890
943
  roles: roles.map(toRoleDTO)
891
944
  });
892
945
  },
893
946
  async addRole(ctx) {
894
- if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
947
+ if (!ctx.workspace) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
895
948
  const params = ctx.meta["params"];
896
949
  const body = ctx.meta["body"];
897
950
  const userId = params?.["userId"];
898
951
  const roleId = body?.["roleId"] ?? params?.["roleId"];
899
952
  if (!userId)
900
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
953
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
901
954
  if (!roleId)
902
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
955
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
903
956
  const assigned = await members.addRole(userId, ctx.workspace.id, roleId);
904
957
  if (!assigned) {
905
- return setApiResponse3(
906
- HTTP3.UNPROCESSABLE,
958
+ return setApiResponse4(
959
+ HTTP4.UNPROCESSABLE,
907
960
  "INVALID_ROLE",
908
961
  "Role is not assignable in this workspace."
909
962
  );
910
963
  }
911
- return setApiResponse3(HTTP3.OK, "ROLE_ASSIGNED", "Role assigned successfully.");
964
+ return setApiResponse4(HTTP4.OK, "ROLE_ASSIGNED", "Role assigned successfully.");
912
965
  },
913
966
  async removeRole(ctx) {
914
- if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
967
+ if (!ctx.workspace) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
915
968
  const params = ctx.meta["params"];
916
969
  const userId = params?.["userId"];
917
970
  const roleId = params?.["roleId"];
918
971
  if (!userId)
919
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
972
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
920
973
  if (!roleId)
921
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
974
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
922
975
  try {
923
976
  await members.removeRole(userId, ctx.workspace.id, roleId);
924
- return setApiResponse3(HTTP3.OK, "ROLE_REMOVED", "Role removed successfully.");
977
+ return setApiResponse4(HTTP4.OK, "ROLE_REMOVED", "Role removed successfully.");
925
978
  } catch (err) {
926
979
  const message = err instanceof Error ? err.message : "Failed";
927
- return setApiResponse3(HTTP3.BAD_REQUEST, "OPERATION_FAILED", message);
980
+ return setApiResponse4(HTTP4.BAD_REQUEST, "OPERATION_FAILED", message);
928
981
  }
929
982
  }
930
983
  };
931
984
  }
932
985
 
933
986
  // src/controllers/role.controller.ts
934
- import { setApiResponse as setApiResponse4, HTTP as HTTP4 } from "@fonderie/core";
987
+ import { setApiResponse as setApiResponse5, HTTP as HTTP5 } from "@fonderie/core";
935
988
  function roleController(store) {
936
989
  const roles = new RoleModel(store);
937
990
  return {
938
991
  async create(ctx) {
939
992
  if (!ctx.workspace) {
940
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
993
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
941
994
  }
942
995
  const body = ctx.meta["body"];
943
996
  const name2 = body?.["name"];
944
997
  const description = body?.["description"];
945
998
  if (typeof name2 !== "string" || name2.trim().length === 0) {
946
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
999
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
947
1000
  }
948
1001
  try {
949
1002
  const opts = {
@@ -954,49 +1007,49 @@ function roleController(store) {
954
1007
  opts.description = description;
955
1008
  }
956
1009
  const role = await roles.create(opts);
957
- return setApiResponse4(HTTP4.CREATED, "ROLE_CREATED", "Role created successfully.", {
1010
+ return setApiResponse5(HTTP5.CREATED, "ROLE_CREATED", "Role created successfully.", {
958
1011
  role: toRoleDTO(role)
959
1012
  });
960
1013
  } catch (err) {
961
1014
  const message = err instanceof Error ? err.message : "Failed to create role";
962
- return setApiResponse4(HTTP4.BAD_REQUEST, "OPERATION_FAILED", message);
1015
+ return setApiResponse5(HTTP5.BAD_REQUEST, "OPERATION_FAILED", message);
963
1016
  }
964
1017
  },
965
1018
  async list(ctx) {
966
1019
  if (!ctx.workspace) {
967
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1020
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
968
1021
  }
969
1022
  const list = await roles.list(ctx.workspace.id);
970
- return setApiResponse4(HTTP4.OK, "ROLES_FETCHED", "Roles retrieved successfully.", {
1023
+ return setApiResponse5(HTTP5.OK, "ROLES_FETCHED", "Roles retrieved successfully.", {
971
1024
  roles: list.map(toRoleDTO)
972
1025
  });
973
1026
  },
974
1027
  async get(ctx) {
975
1028
  if (!ctx.workspace) {
976
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1029
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
977
1030
  }
978
1031
  const params = ctx.meta["params"];
979
1032
  const roleId = params?.["roleId"];
980
1033
  if (!roleId) {
981
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1034
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
982
1035
  }
983
1036
  const role = await roles.findById(roleId, ctx.workspace.id);
984
1037
  if (!role) {
985
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Role not found");
1038
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Role not found");
986
1039
  }
987
- return setApiResponse4(HTTP4.OK, "ROLE_FETCHED", "Role retrieved successfully.", {
1040
+ return setApiResponse5(HTTP5.OK, "ROLE_FETCHED", "Role retrieved successfully.", {
988
1041
  role: toRoleDTO(role)
989
1042
  });
990
1043
  },
991
1044
  async update(ctx) {
992
1045
  if (!ctx.workspace) {
993
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1046
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
994
1047
  }
995
1048
  const params = ctx.meta["params"];
996
1049
  const body = ctx.meta["body"];
997
1050
  const roleId = params?.["roleId"];
998
1051
  if (!roleId) {
999
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1052
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1000
1053
  }
1001
1054
  const opts = {};
1002
1055
  if (typeof body?.["name"] === "string") {
@@ -1013,60 +1066,60 @@ function roleController(store) {
1013
1066
  }
1014
1067
  const role = await roles.update(roleId, ctx.workspace.id, opts);
1015
1068
  if (!role) {
1016
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Role not found or is a system role");
1069
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Role not found or is a system role");
1017
1070
  }
1018
- return setApiResponse4(HTTP4.OK, "ROLE_UPDATED", "Role updated successfully.", {
1071
+ return setApiResponse5(HTTP5.OK, "ROLE_UPDATED", "Role updated successfully.", {
1019
1072
  role: toRoleDTO(role)
1020
1073
  });
1021
1074
  },
1022
1075
  async remove(ctx) {
1023
1076
  if (!ctx.workspace) {
1024
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1077
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1025
1078
  }
1026
1079
  const params = ctx.meta["params"];
1027
1080
  const roleId = params?.["roleId"];
1028
1081
  if (!roleId) {
1029
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1082
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1030
1083
  }
1031
1084
  await roles.delete(roleId, ctx.workspace.id);
1032
- return setApiResponse4(HTTP4.OK, "ROLE_DELETED", "Role deleted successfully.");
1085
+ return setApiResponse5(HTTP5.OK, "ROLE_DELETED", "Role deleted successfully.");
1033
1086
  },
1034
1087
  async getPermissions(ctx) {
1035
1088
  if (!ctx.workspace) {
1036
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1089
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1037
1090
  }
1038
1091
  const params = ctx.meta["params"];
1039
1092
  const roleId = params?.["roleId"];
1040
1093
  if (!roleId) {
1041
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1094
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1042
1095
  }
1043
1096
  const role = await roles.findById(roleId, ctx.workspace.id);
1044
1097
  if (!role) {
1045
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Role not found");
1098
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Role not found");
1046
1099
  }
1047
1100
  const permissions = await roles.getPermissions(roleId, ctx.workspace.id);
1048
- return setApiResponse4(HTTP4.OK, "PERMISSIONS_FETCHED", "Role permissions retrieved successfully.", {
1101
+ return setApiResponse5(HTTP5.OK, "PERMISSIONS_FETCHED", "Role permissions retrieved successfully.", {
1049
1102
  permissions
1050
1103
  });
1051
1104
  },
1052
1105
  async setPermissions(ctx) {
1053
1106
  if (!ctx.workspace) {
1054
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1107
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1055
1108
  }
1056
1109
  const params = ctx.meta["params"];
1057
1110
  const body = ctx.meta["body"];
1058
1111
  const roleId = params?.["roleId"];
1059
1112
  if (!roleId) {
1060
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1113
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
1061
1114
  }
1062
1115
  const target = await roles.findById(roleId, ctx.workspace.id);
1063
1116
  if (!target || target.isSystem) {
1064
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Role not found");
1117
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Role not found");
1065
1118
  }
1066
1119
  const perms = body?.["permissions"];
1067
1120
  if (!Array.isArray(perms)) {
1068
- return setApiResponse4(
1069
- HTTP4.UNPROCESSABLE,
1121
+ return setApiResponse5(
1122
+ HTTP5.UNPROCESSABLE,
1070
1123
  "INVALID_PARAMETER",
1071
1124
  "permissions array is required"
1072
1125
  );
@@ -1082,22 +1135,22 @@ function roleController(store) {
1082
1135
  };
1083
1136
  }).filter((p) => p.permissionKey.length > 0);
1084
1137
  await roles.setPermissions(roleId, ctx.workspace.id, normalized);
1085
- return setApiResponse4(HTTP4.OK, "PERMISSIONS_SET", "Role permissions updated successfully.");
1138
+ return setApiResponse5(HTTP5.OK, "PERMISSIONS_SET", "Role permissions updated successfully.");
1086
1139
  }
1087
1140
  };
1088
1141
  }
1089
1142
 
1090
1143
  // src/controllers/invitation.controller.ts
1091
- import { setApiResponse as setApiResponse5, HTTP as HTTP5 } from "@fonderie/core";
1144
+ import { setApiResponse as setApiResponse6, HTTP as HTTP6 } from "@fonderie/core";
1092
1145
  import { NOTIFICATION_EVENT } from "@fonderie/events";
1093
1146
 
1094
1147
  // src/services/invitations.ts
1095
- import { randomBytes } from "crypto";
1148
+ import { randomBytes, randomInt } from "crypto";
1096
1149
  function generateToken() {
1097
1150
  return randomBytes(32).toString("hex");
1098
1151
  }
1099
1152
  function generatePin() {
1100
- return Math.floor(1e5 + Math.random() * 9e5).toString();
1153
+ return randomInt(1e5, 1e6).toString();
1101
1154
  }
1102
1155
  function parseTtl(ttl) {
1103
1156
  const units = {
@@ -1168,8 +1221,8 @@ async function acceptInvitationByPin(opts, store) {
1168
1221
  const [inv] = await store.query(
1169
1222
  `SELECT id, workspace_id AS "workspaceId", role_id AS "roleId", expires_at AS "expiresAt"
1170
1223
  FROM fonderie_workspace_invitations
1171
- WHERE pin = $1 AND status = 'PENDING'`,
1172
- [opts.pin]
1224
+ WHERE pin = $1 AND lower(email) = lower($2) AND status = 'PENDING'`,
1225
+ [opts.pin, opts.email]
1173
1226
  );
1174
1227
  if (!inv) throw new Error("Invalid PIN");
1175
1228
  if (/* @__PURE__ */ new Date() > new Date(inv.expiresAt)) throw new Error("Invitation expired");
@@ -1189,6 +1242,31 @@ async function acceptInvitationByPin(opts, store) {
1189
1242
  });
1190
1243
  return { workspaceId: inv.workspaceId, roleId: inv.roleId };
1191
1244
  }
1245
+ async function acceptInvitationByToken(token, userId, store) {
1246
+ const [inv] = await store.query(
1247
+ `SELECT id, workspace_id AS "workspaceId", role_id AS "roleId", expires_at AS "expiresAt"
1248
+ FROM fonderie_workspace_invitations
1249
+ WHERE token = $1 AND status = 'PENDING'`,
1250
+ [token]
1251
+ );
1252
+ if (!inv) throw new Error("Invalid token");
1253
+ if (/* @__PURE__ */ new Date() > new Date(inv.expiresAt)) throw new Error("Invitation expired");
1254
+ await store.transaction(async (tx) => {
1255
+ await Promise.all([
1256
+ tx.query(
1257
+ `INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
1258
+ VALUES ($1, $2, $3, true)
1259
+ ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
1260
+ SET confirmed = true, removed = false`,
1261
+ [userId, inv.workspaceId, inv.roleId]
1262
+ ),
1263
+ tx.query(`UPDATE fonderie_workspace_invitations SET status = 'ACCEPTED' WHERE id = $1`, [
1264
+ inv.id
1265
+ ])
1266
+ ]);
1267
+ });
1268
+ return { workspaceId: inv.workspaceId, roleId: inv.roleId };
1269
+ }
1192
1270
 
1193
1271
  // src/models/invitation.model.ts
1194
1272
  var InvitationModel = class {
@@ -1208,6 +1286,9 @@ var InvitationModel = class {
1208
1286
  acceptByPin(opts) {
1209
1287
  return acceptInvitationByPin(opts, this.store);
1210
1288
  }
1289
+ acceptByToken(token, userId) {
1290
+ return acceptInvitationByToken(token, userId, this.store);
1291
+ }
1211
1292
  };
1212
1293
 
1213
1294
  // src/controllers/invitation.controller.ts
@@ -1221,17 +1302,17 @@ function invitationController(store, ttl, bus) {
1221
1302
  const invitations = new InvitationModel(store);
1222
1303
  return {
1223
1304
  async list(ctx) {
1224
- if (!ctx.workspace) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1305
+ if (!ctx.workspace) return setApiResponse6(HTTP6.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1225
1306
  const list = await invitations.list(ctx.workspace.id);
1226
- return setApiResponse5(HTTP5.OK, "INVITATIONS_FETCHED", "Invitations retrieved successfully.", {
1307
+ return setApiResponse6(HTTP6.OK, "INVITATIONS_FETCHED", "Invitations retrieved successfully.", {
1227
1308
  invitations: list.map(toInvitationDTO)
1228
1309
  });
1229
1310
  },
1230
1311
  async invite(ctx) {
1231
- if (!ctx.workspace) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1312
+ if (!ctx.workspace) return setApiResponse6(HTTP6.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1232
1313
  if (ctx.workspace.isPersonal) {
1233
- return setApiResponse5(
1234
- HTTP5.FORBIDDEN,
1314
+ return setApiResponse6(
1315
+ HTTP6.FORBIDDEN,
1235
1316
  "FORBIDDEN",
1236
1317
  "Personal workspaces do not support invitations"
1237
1318
  );
@@ -1239,11 +1320,11 @@ function invitationController(store, ttl, bus) {
1239
1320
  const body = ctx.meta["body"];
1240
1321
  const entries = Array.isArray(body) ? body : [body];
1241
1322
  if (!entries.length) {
1242
- return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "at least one invite is required");
1323
+ return setApiResponse6(HTTP6.UNPROCESSABLE, "INVALID_PARAMETER", "at least one invite is required");
1243
1324
  }
1244
1325
  for (const entry of entries) {
1245
1326
  if (typeof entry?.["email"] !== "string") {
1246
- return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "email is required for every invite");
1327
+ return setApiResponse6(HTTP6.UNPROCESSABLE, "INVALID_PARAMETER", "email is required for every invite");
1247
1328
  }
1248
1329
  }
1249
1330
  const seatLimit = seatLimitFromMeta(ctx);
@@ -1256,8 +1337,8 @@ function invitationController(store, ttl, bus) {
1256
1337
  const total = parseInt(countRow.count, 10);
1257
1338
  const occupied = ctx.workspace.isPersonal ? total : Math.max(0, total - 1);
1258
1339
  if (occupied + entries.length > seatLimit) {
1259
- return setApiResponse5(
1260
- HTTP5.PAYMENT_REQUIRED,
1340
+ return setApiResponse6(
1341
+ HTTP6.PAYMENT_REQUIRED,
1261
1342
  "SEAT_LIMIT_REACHED",
1262
1343
  `Your plan allows ${seatLimit} seat${seatLimit === 1 ? "" : "s"}. Upgrade to invite more members.`,
1263
1344
  { limit: seatLimit }
@@ -1274,7 +1355,7 @@ function invitationController(store, ttl, bus) {
1274
1355
  );
1275
1356
  defaultRoleId = row?.id;
1276
1357
  if (!defaultRoleId) {
1277
- return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "Default role not found");
1358
+ return setApiResponse6(HTTP6.SERVER_ERROR, "SERVER_ERROR", "Default role not found");
1278
1359
  }
1279
1360
  }
1280
1361
  const results = await Promise.all(
@@ -1296,33 +1377,48 @@ function invitationController(store, ttl, bus) {
1296
1377
  return { invitationId: invitation.id, email: email2 };
1297
1378
  })
1298
1379
  );
1299
- return setApiResponse5(HTTP5.CREATED, "INVITATIONS_SENT", "Invitations sent successfully.", {
1380
+ return setApiResponse6(HTTP6.CREATED, "INVITATIONS_SENT", "Invitations sent successfully.", {
1300
1381
  invitations: results
1301
1382
  });
1302
1383
  },
1303
1384
  async cancel(ctx) {
1304
- if (!ctx.workspace) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1385
+ if (!ctx.workspace) return setApiResponse6(HTTP6.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1305
1386
  const params = ctx.meta["params"];
1306
1387
  const invitationId = params?.["inviteId"];
1307
1388
  if (!invitationId)
1308
- return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "inviteId is required");
1389
+ return setApiResponse6(HTTP6.UNPROCESSABLE, "INVALID_PARAMETER", "inviteId is required");
1309
1390
  await invitations.cancel(invitationId, ctx.workspace.id);
1310
- return setApiResponse5(HTTP5.OK, "INVITATION_CANCELLED", "Invitation cancelled successfully.");
1391
+ return setApiResponse6(HTTP6.OK, "INVITATION_CANCELLED", "Invitation cancelled successfully.");
1311
1392
  },
1312
1393
  async accept(ctx) {
1313
1394
  const body = ctx.meta["body"];
1314
1395
  const pin = body?.["pin"];
1315
- if (typeof pin !== "string") {
1316
- return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "pin is required");
1317
- }
1396
+ const token = body?.["token"];
1318
1397
  try {
1319
- const { workspaceId } = await invitations.acceptByPin({ pin, userId: ctx.user.id });
1320
- return setApiResponse5(HTTP5.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1398
+ if (typeof token === "string") {
1399
+ const { workspaceId: workspaceId2 } = await invitations.acceptByToken(token, ctx.user.id);
1400
+ return setApiResponse6(HTTP6.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1401
+ workspaceId: workspaceId2
1402
+ });
1403
+ }
1404
+ if (typeof pin !== "string") {
1405
+ return setApiResponse6(HTTP6.UNPROCESSABLE, "INVALID_PARAMETER", "pin or token is required");
1406
+ }
1407
+ const email2 = ctx.user.email;
1408
+ if (!email2) {
1409
+ return setApiResponse6(
1410
+ HTTP6.BAD_REQUEST,
1411
+ "INVITATION_FAILED",
1412
+ "This account has no email address \u2014 use the invitation link instead of the PIN"
1413
+ );
1414
+ }
1415
+ const { workspaceId } = await invitations.acceptByPin({ pin, userId: ctx.user.id, email: email2 });
1416
+ return setApiResponse6(HTTP6.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1321
1417
  workspaceId
1322
1418
  });
1323
1419
  } catch (err) {
1324
1420
  const message = err instanceof Error ? err.message : "Invalid invitation";
1325
- return setApiResponse5(HTTP5.BAD_REQUEST, "INVITATION_FAILED", message);
1421
+ return setApiResponse6(HTTP6.BAD_REQUEST, "INVITATION_FAILED", message);
1326
1422
  }
1327
1423
  }
1328
1424
  };
@@ -1332,6 +1428,12 @@ function invitationController(store, ttl, bus) {
1332
1428
  function buildWorkspaceRoutes(store, config, bus) {
1333
1429
  const ttl = config.invitationTtl ?? "7d";
1334
1430
  const wsCtx = withWorkspace(store);
1431
+ const manager = requireManager(store, config);
1432
+ const acceptLimit = rateLimit({
1433
+ store: new StoreAdapterStore(store),
1434
+ rule: { capacity: 10, refillPerSec: 10 / (15 * 60) },
1435
+ key: byIp("workspaces:invitation-accept")
1436
+ });
1335
1437
  const workspace = workspaceController(store, config);
1336
1438
  const member = memberController(store);
1337
1439
  const role = roleController(store);
@@ -1348,34 +1450,34 @@ function buildWorkspaceRoutes(store, config, bus) {
1348
1450
  R("listWorkspaces", "GET", "/workspaces", requireAuth, workspace.list),
1349
1451
  // ── Members (workspace resolved from X-Workspace-ID header)
1350
1452
  R("listMembers", "GET", "/workspaces/members", requireAuth, wsCtx, member.list),
1351
- R("removeMember", "DELETE", "/workspaces/members/:userId", requireAuth, wsCtx, member.remove),
1453
+ R("removeMember", "DELETE", "/workspaces/members/:userId", requireAuth, wsCtx, manager, member.remove),
1352
1454
  R("getMemberRoles", "GET", "/workspaces/members/:userId/roles", requireAuth, wsCtx, member.getUserRoles),
1353
- R("addMemberRole", "POST", "/workspaces/members/:userId/roles", requireAuth, wsCtx, validate(addMemberRoleSchema), member.addRole),
1354
- R("removeMemberRole", "DELETE", "/workspaces/members/:userId/roles/:roleId", requireAuth, wsCtx, member.removeRole),
1455
+ R("addMemberRole", "POST", "/workspaces/members/:userId/roles", requireAuth, wsCtx, manager, validate(addMemberRoleSchema), member.addRole),
1456
+ R("removeMemberRole", "DELETE", "/workspaces/members/:userId/roles/:roleId", requireAuth, wsCtx, manager, member.removeRole),
1355
1457
  // ── Invitations
1356
1458
  R("listInvitations", "GET", "/workspaces/invitations", requireAuth, wsCtx, invitation.list),
1357
- R("invite", "POST", "/workspaces/invitations", requireAuth, wsCtx, validate(createInvitationsSchema), invitation.invite),
1358
- R("cancelInvitation", "DELETE", "/workspaces/invitations/:inviteId", requireAuth, wsCtx, invitation.cancel),
1359
- R("acceptInvitation", "POST", "/workspaces/invitations/accept", requireAuth, validate(acceptInvitationSchema), invitation.accept),
1459
+ R("invite", "POST", "/workspaces/invitations", requireAuth, wsCtx, manager, validate(createInvitationsSchema), invitation.invite),
1460
+ R("cancelInvitation", "DELETE", "/workspaces/invitations/:inviteId", requireAuth, wsCtx, manager, invitation.cancel),
1461
+ R("acceptInvitation", "POST", "/workspaces/invitations/accept", acceptLimit, requireAuth, validate(acceptInvitationSchema), invitation.accept),
1360
1462
  // ── Roles
1361
- R("createRole", "POST", "/workspaces/roles", requireAuth, wsCtx, validate(createRoleSchema), role.create),
1463
+ R("createRole", "POST", "/workspaces/roles", requireAuth, wsCtx, manager, validate(createRoleSchema), role.create),
1362
1464
  R("listRoles", "GET", "/workspaces/roles", requireAuth, wsCtx, role.list),
1363
1465
  R("getRole", "GET", "/workspaces/roles/:roleId", requireAuth, wsCtx, role.get),
1364
- R("updateRole", "PUT", "/workspaces/roles/:roleId", requireAuth, wsCtx, validate(updateRoleSchema), role.update),
1365
- R("removeRole", "DELETE", "/workspaces/roles/:roleId", requireAuth, wsCtx, role.remove),
1466
+ R("updateRole", "PUT", "/workspaces/roles/:roleId", requireAuth, wsCtx, manager, validate(updateRoleSchema), role.update),
1467
+ R("removeRole", "DELETE", "/workspaces/roles/:roleId", requireAuth, wsCtx, manager, role.remove),
1366
1468
  R("getRolePermissions", "GET", "/workspaces/roles/:roleId/permissions", requireAuth, wsCtx, role.getPermissions),
1367
- R("setRolePermissions", "POST", "/workspaces/roles/:roleId/permissions", requireAuth, wsCtx, validate(setRolePermissionsSchema), role.setPermissions),
1469
+ R("setRolePermissions", "POST", "/workspaces/roles/:roleId/permissions", requireAuth, wsCtx, manager, validate(setRolePermissionsSchema), role.setPermissions),
1368
1470
  // ── Workspace lifecycle
1369
- R("archive", "POST", "/workspaces/archive", requireAuth, wsCtx, workspace.archive),
1370
- R("restore", "POST", "/workspaces/restore", requireAuth, wsCtx, workspace.restore),
1471
+ R("archive", "POST", "/workspaces/archive", requireAuth, wsCtx, manager, workspace.archive),
1472
+ R("restore", "POST", "/workspaces/restore", requireAuth, wsCtx, manager, workspace.restore),
1371
1473
  R("getSettings", "GET", "/workspaces/settings", requireAuth, wsCtx, workspace.getSettings),
1372
- R("updateSettings", "PUT", "/workspaces/settings", requireAuth, wsCtx, validate(updateSettingsSchema), workspace.updateSettings),
1474
+ R("updateSettings", "PUT", "/workspaces/settings", requireAuth, wsCtx, manager, validate(updateSettingsSchema), workspace.updateSettings),
1373
1475
  // ── Path-based lookup by ID (admin / cross-workspace use)
1374
1476
  R("getWorkspace", "GET", "/workspaces/:id", requireAuth, wsCtx, workspace.get),
1375
1477
  // ── Update current workspace — ID from :id path param (wsCtx) or the
1376
1478
  // X-Workspace-ID header (or personal-workspace fallback when absent). Set
1377
1479
  // routes.updateWorkspace = '/workspaces/:id' to match a path-id frontend.
1378
- R("updateWorkspace", "PUT", "/workspaces", requireAuth, wsCtx, validate(updateWorkspaceSchema), workspace.update)
1480
+ R("updateWorkspace", "PUT", "/workspaces", requireAuth, wsCtx, manager, validate(updateWorkspaceSchema), workspace.update)
1379
1481
  ];
1380
1482
  }
1381
1483
 
@@ -1459,10 +1561,10 @@ var SAMPLE_PAYLOADS = {
1459
1561
  };
1460
1562
 
1461
1563
  // src/middlewares/require-workspace.ts
1462
- import { setApiResponse as setApiResponse6, HTTP as HTTP6 } from "@fonderie/core";
1564
+ import { setApiResponse as setApiResponse7, HTTP as HTTP7 } from "@fonderie/core";
1463
1565
  var requireWorkspace = async (ctx, next) => {
1464
1566
  if (!ctx.workspace) {
1465
- return setApiResponse6(HTTP6.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
1567
+ return setApiResponse7(HTTP7.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
1466
1568
  }
1467
1569
  return next();
1468
1570
  };
@@ -1574,6 +1676,7 @@ export {
1574
1676
  importMembership,
1575
1677
  importRole,
1576
1678
  importWorkspace,
1679
+ requireManager,
1577
1680
  requireWorkspace,
1578
1681
  schemas_exports as schemas,
1579
1682
  toInvitationDTO,