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