@jskit-ai/users-core 0.1.48 → 0.1.50

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.
Files changed (87) hide show
  1. package/package.descriptor.mjs +7 -7
  2. package/package.json +7 -17
  3. package/src/server/common/services/authProfileSyncService.js +28 -7
  4. package/src/server/common/support/realtimeServiceEvents.js +1 -59
  5. package/src/server/profileSyncLifecycleContributorRegistry.js +56 -0
  6. package/src/server/registerUsersBootstrap.js +0 -1
  7. package/src/server/registerUsersCore.js +2 -14
  8. package/src/server/usersBootstrapContributor.js +2 -64
  9. package/src/shared/index.js +2 -99
  10. package/src/shared/settings.js +1 -119
  11. package/test/authProfileSyncService.test.js +19 -10
  12. package/test/registerServiceRealtimeEvents.test.js +0 -86
  13. package/test/registerUsersCore.test.js +6 -15
  14. package/test/repositoryContracts.test.js +1 -9
  15. package/test/resourcesCanonical.test.js +0 -16
  16. package/test/settingsFieldRegistriesSingleton.test.js +0 -5
  17. package/test/usersBootstrapContributor.test.js +2 -26
  18. package/test/usersRouteResources.test.js +0 -16
  19. package/src/server/UsersWorkspacesServiceProvider.js +0 -44
  20. package/src/server/common/contributors/workspaceActionContextContributor.js +0 -88
  21. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +0 -34
  22. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +0 -78
  23. package/src/server/common/formatters/workspaceFormatter.js +0 -53
  24. package/src/server/common/repositories/workspaceInvitesRepository.js +0 -208
  25. package/src/server/common/repositories/workspaceMembershipsRepository.js +0 -190
  26. package/src/server/common/repositories/workspacesRepository.js +0 -202
  27. package/src/server/common/services/workspaceContextService.js +0 -281
  28. package/src/server/common/support/workspaceRoutePaths.js +0 -17
  29. package/src/server/common/validators/routeParamsValidator.js +0 -62
  30. package/src/server/registerWorkspaceBootstrap.js +0 -27
  31. package/src/server/registerWorkspaceCore.js +0 -73
  32. package/src/server/registerWorkspaceRepositories.js +0 -26
  33. package/src/server/support/resolveWorkspace.js +0 -16
  34. package/src/server/support/workspaceActionSurfaces.js +0 -118
  35. package/src/server/support/workspaceInvitationsPolicy.js +0 -45
  36. package/src/server/support/workspaceRouteInput.js +0 -22
  37. package/src/server/workspaceBootstrapContributor.js +0 -212
  38. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +0 -133
  39. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  40. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +0 -133
  41. package/src/server/workspaceMembers/bootWorkspaceMembers.js +0 -236
  42. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -108
  43. package/src/server/workspaceMembers/workspaceMembersActions.js +0 -186
  44. package/src/server/workspaceMembers/workspaceMembersService.js +0 -222
  45. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +0 -62
  46. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -119
  47. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +0 -74
  48. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +0 -138
  49. package/src/server/workspaceSettings/bootWorkspaceSettings.js +0 -76
  50. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -62
  51. package/src/server/workspaceSettings/workspaceSettingsActions.js +0 -72
  52. package/src/server/workspaceSettings/workspaceSettingsRepository.js +0 -154
  53. package/src/server/workspaceSettings/workspaceSettingsService.js +0 -66
  54. package/src/shared/resources/workspaceMembersResource.js +0 -354
  55. package/src/shared/resources/workspacePendingInvitationsResource.js +0 -82
  56. package/src/shared/resources/workspaceResource.js +0 -176
  57. package/src/shared/resources/workspaceSettingsFields.js +0 -59
  58. package/src/shared/resources/workspaceSettingsResource.js +0 -169
  59. package/src/shared/roles.js +0 -161
  60. package/src/shared/support/usersApiPaths.js +0 -43
  61. package/src/shared/support/usersVisibility.js +0 -42
  62. package/src/shared/support/workspacePathModel.js +0 -145
  63. package/src/shared/tenancyMode.js +0 -35
  64. package/src/shared/tenancyProfile.js +0 -73
  65. package/test/registerWorkspaceDirectory.test.js +0 -31
  66. package/test/registerWorkspaceSettings.test.js +0 -40
  67. package/test/roles.test.js +0 -159
  68. package/test/tenancyProfile.test.js +0 -67
  69. package/test/usersApiPaths.test.js +0 -49
  70. package/test/usersRouteValidators.test.js +0 -49
  71. package/test/usersVisibility.test.js +0 -27
  72. package/test/workspaceActionContextContributor.test.js +0 -344
  73. package/test/workspaceActionSurfaces.test.js +0 -85
  74. package/test/workspaceAuthPolicyContextResolver.test.js +0 -119
  75. package/test/workspaceBootstrapContributor.test.js +0 -154
  76. package/test/workspaceInvitationsPolicy.test.js +0 -71
  77. package/test/workspaceInvitesRepository.test.js +0 -111
  78. package/test/workspaceMembersService.test.js +0 -398
  79. package/test/workspacePathModel.test.js +0 -93
  80. package/test/workspacePendingInvitationsResource.test.js +0 -38
  81. package/test/workspacePendingInvitationsService.test.js +0 -151
  82. package/test/workspaceRouteVisibilityResolver.test.js +0 -83
  83. package/test/workspaceService.test.js +0 -546
  84. package/test/workspaceSettingsActions.test.js +0 -52
  85. package/test/workspaceSettingsRepository.test.js +0 -202
  86. package/test/workspaceSettingsResource.test.js +0 -169
  87. package/test/workspaceSettingsService.test.js +0 -140
@@ -1,222 +0,0 @@
1
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { buildInviteToken, hashInviteToken } from "@jskit-ai/auth-core/server/inviteTokens";
3
- import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
4
- import { OWNER_ROLE_ID, createWorkspaceRoleCatalog, cloneWorkspaceRoleCatalog } from "../../shared/roles.js";
5
-
6
- function createService({
7
- workspaceMembershipsRepository,
8
- workspaceInvitesRepository,
9
- inviteExpiresInMs,
10
- roleCatalog = null,
11
- workspaceInvitationsEnabled = true
12
- } = {}) {
13
- if (!workspaceMembershipsRepository || !workspaceInvitesRepository) {
14
- throw new Error("workspaceMembersService requires membership and invite repositories.");
15
- }
16
- const resolvedInviteExpiresInMs = Number(inviteExpiresInMs);
17
- if (!Number.isInteger(resolvedInviteExpiresInMs) || resolvedInviteExpiresInMs < 1) {
18
- throw new Error("workspaceMembersService requires inviteExpiresInMs.");
19
- }
20
-
21
- const resolvedRoleCatalog = roleCatalog && typeof roleCatalog === "object" ? roleCatalog : createWorkspaceRoleCatalog();
22
- const assignableRoleIds = Array.isArray(resolvedRoleCatalog.assignableRoleIds)
23
- ? [...resolvedRoleCatalog.assignableRoleIds]
24
- : [];
25
- const resolvedWorkspaceInvitationsEnabled = workspaceInvitationsEnabled === true;
26
-
27
- function ensureWorkspaceInvitationsEnabled() {
28
- if (resolvedWorkspaceInvitationsEnabled) {
29
- return;
30
- }
31
- throw new AppError(403, "Workspace invitations are disabled.");
32
- }
33
-
34
- function withRoleCatalog(payload = {}) {
35
- return {
36
- ...payload,
37
- roleCatalog: cloneWorkspaceRoleCatalog({
38
- ...resolvedRoleCatalog,
39
- assignableRoleIds
40
- })
41
- };
42
- }
43
-
44
- async function listRoles(options = {}) {
45
- return cloneWorkspaceRoleCatalog({
46
- ...resolvedRoleCatalog,
47
- assignableRoleIds
48
- });
49
- }
50
-
51
- async function listMembersPayload(workspace, options = {}) {
52
- const members = await workspaceMembershipsRepository.listActiveByWorkspaceId(workspace.id, options);
53
-
54
- return withRoleCatalog({
55
- workspace,
56
- members
57
- });
58
- }
59
-
60
- async function listMembers(workspace, options = {}) {
61
- return listMembersPayload(workspace, options);
62
- }
63
-
64
- async function updateMemberRole(workspace, payload = {}, options = {}) {
65
- const memberUserId = normalizeRecordId(payload.memberUserId, { fallback: null });
66
- const roleSid = payload.roleSid;
67
- if (!memberUserId) {
68
- throw new AppError(400, "Validation failed.");
69
- }
70
- if (!assignableRoleIds.includes(roleSid)) {
71
- throw new AppError(400, "Validation failed.", {
72
- details: {
73
- fieldErrors: {
74
- roleSid: "Role is not assignable."
75
- }
76
- }
77
- });
78
- }
79
-
80
- const existingMembership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(workspace.id, memberUserId, options);
81
- if (!existingMembership || existingMembership.status !== "active") {
82
- throw new AppError(404, "Member not found.");
83
- }
84
- if (memberUserId === normalizeRecordId(workspace.ownerUserId, { fallback: null }) || existingMembership.roleSid === OWNER_ROLE_ID) {
85
- throw new AppError(409, "Cannot change workspace owner role.");
86
- }
87
-
88
- await workspaceMembershipsRepository.upsertMembership(
89
- workspace.id,
90
- memberUserId,
91
- {
92
- roleSid,
93
- status: "active"
94
- },
95
- options
96
- );
97
-
98
- return listMembersPayload(workspace, options);
99
- }
100
-
101
- async function removeMember(workspace, payload = {}, options = {}) {
102
- const memberUserId = normalizeRecordId(payload.memberUserId, { fallback: null });
103
- if (!memberUserId) {
104
- throw new AppError(400, "Validation failed.");
105
- }
106
-
107
- const existingMembership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(workspace.id, memberUserId, options);
108
- if (!existingMembership || existingMembership.status !== "active") {
109
- throw new AppError(404, "Member not found.");
110
- }
111
- if (memberUserId === normalizeRecordId(workspace.ownerUserId, { fallback: null }) || existingMembership.roleSid === OWNER_ROLE_ID) {
112
- throw new AppError(409, "Cannot remove workspace owner.");
113
- }
114
-
115
- await workspaceMembershipsRepository.upsertMembership(
116
- workspace.id,
117
- memberUserId,
118
- {
119
- roleSid: existingMembership.roleSid,
120
- status: "revoked"
121
- },
122
- options
123
- );
124
-
125
- return listMembersPayload(workspace, options);
126
- }
127
-
128
- async function listInvitesPayload(workspace, options = {}) {
129
- ensureWorkspaceInvitationsEnabled();
130
- const invites = await workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace(workspace.id, options);
131
-
132
- return withRoleCatalog({
133
- workspace,
134
- invites
135
- });
136
- }
137
-
138
- async function listInvites(workspace, options = {}) {
139
- return listInvitesPayload(workspace, options);
140
- }
141
-
142
- async function createInvite(workspace, user, payload = {}, options = {}) {
143
- const email = payload.email;
144
- const roleSid = payload.roleSid;
145
- if (!assignableRoleIds.includes(roleSid)) {
146
- throw new AppError(400, "Validation failed.", {
147
- details: {
148
- fieldErrors: {
149
- roleSid: "Role is not assignable."
150
- }
151
- }
152
- });
153
- }
154
-
155
- const token = buildInviteToken();
156
- const tokenHash = hashInviteToken(token);
157
- await workspaceInvitesRepository.expirePendingByWorkspaceIdAndEmail(workspace.id, email, options);
158
- const createdInvite = await workspaceInvitesRepository.insert(
159
- {
160
- workspaceId: workspace.id,
161
- email,
162
- roleSid,
163
- status: "pending",
164
- tokenHash,
165
- invitedByUserId: normalizeRecordId(user?.id, { fallback: null }),
166
- expiresAt: new Date(Date.now() + resolvedInviteExpiresInMs).toISOString()
167
- },
168
- options
169
- );
170
- const createdInviteId = normalizeRecordId(createdInvite?.id, { fallback: null });
171
- if (!createdInviteId) {
172
- throw new Error("workspaceMembersService.createInvite expected repository to return created invite id.");
173
- }
174
-
175
- const response = await listInvitesPayload(workspace, options);
176
- return {
177
- ...response,
178
- inviteTokenPreview: token,
179
- createdInviteId
180
- };
181
- }
182
-
183
- async function revokeInvite(workspace, inviteId, options = {}) {
184
- const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
185
- if (!normalizedInviteId) {
186
- throw new AppError(400, "Validation failed.");
187
- }
188
-
189
- const invite = await workspaceInvitesRepository.findPendingByIdForWorkspace(
190
- normalizedInviteId,
191
- workspace.id,
192
- options
193
- );
194
- if (!invite) {
195
- throw new AppError(404, "Invite not found.");
196
- }
197
-
198
- await workspaceInvitesRepository.revokeById(normalizedInviteId, options);
199
- const revokedInviteId = normalizeRecordId(invite?.id, { fallback: null });
200
- if (!revokedInviteId) {
201
- throw new Error("workspaceMembersService.revokeInvite expected repository to return pending invite id.");
202
- }
203
-
204
- const response = await listInvitesPayload(workspace, options);
205
- return {
206
- ...response,
207
- revokedInviteId
208
- };
209
- }
210
-
211
- return Object.freeze({
212
- listRoles,
213
- listMembers,
214
- updateMemberRole,
215
- removeMember,
216
- listInvites,
217
- createInvite,
218
- revokeInvite
219
- });
220
- }
221
-
222
- export { createService };
@@ -1,62 +0,0 @@
1
- import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
- import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
3
- import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
4
-
5
- function bootWorkspacePendingInvitations(app) {
6
- if (!app || typeof app.make !== "function") {
7
- throw new Error("bootWorkspacePendingInvitations requires application make().");
8
- }
9
-
10
- const router = app.make("jskit.http.router");
11
-
12
- router.register(
13
- "GET",
14
- "/api/workspace/invitations/pending",
15
- {
16
- auth: "required",
17
- meta: {
18
- tags: ["workspace"],
19
- summary: "List pending workspace invitations for authenticated user"
20
- },
21
- responseValidators: withStandardErrorResponses({
22
- 200: workspacePendingInvitationsResource.operations.list.outputValidator
23
- })
24
- },
25
- async function (request, reply) {
26
- const response = await request.executeAction({
27
- actionId: "workspace.invitations.pending.list"
28
- });
29
- reply.code(200).send(response);
30
- }
31
- );
32
-
33
- router.register(
34
- "POST",
35
- "/api/workspace/invitations/redeem",
36
- {
37
- auth: "required",
38
- meta: {
39
- tags: ["workspace"],
40
- summary: "Accept or refuse a workspace invitation using an invite token"
41
- },
42
- bodyValidator: workspaceMembersResource.operations.redeemInvite.bodyValidator,
43
- responseValidators: withStandardErrorResponses(
44
- {
45
- 200: workspaceMembersResource.operations.redeemInvite.outputValidator
46
- },
47
- { includeValidation400: true }
48
- )
49
- },
50
- async function (request, reply) {
51
- const response = await request.executeAction({
52
- actionId: "workspace.invite.redeem",
53
- input: {
54
- payload: request.input.body
55
- }
56
- });
57
- reply.code(200).send(response);
58
- }
59
- );
60
- }
61
-
62
- export { bootWorkspacePendingInvitations };
@@ -1,119 +0,0 @@
1
- import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
2
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { createService } from "./workspacePendingInvitationsService.js";
4
- import { workspacePendingInvitationsActions } from "./workspacePendingInvitationsActions.js";
5
- import { deepFreeze } from "../common/support/deepFreeze.js";
6
-
7
- function workspaceAudienceFromEntityId({ event } = {}) {
8
- const workspaceId = normalizeRecordId(event?.entityId, { fallback: null });
9
- if (!workspaceId) {
10
- return "none";
11
- }
12
- return {
13
- workspaceId
14
- };
15
- }
16
-
17
- function actorUserEntityId({ options } = {}) {
18
- return normalizeRecordId(options?.context?.actor?.id, { fallback: "" });
19
- }
20
-
21
- function createActorUserEvent({ source, entity, realtimeEvent }) {
22
- return {
23
- type: "entity.changed",
24
- source,
25
- entity,
26
- operation: "updated",
27
- entityId: actorUserEntityId,
28
- realtime: {
29
- event: realtimeEvent,
30
- audience: "actor_user"
31
- }
32
- };
33
- }
34
-
35
- function createWorkspaceAudienceEvent({ entity, realtimeEvent }) {
36
- return {
37
- type: "entity.changed",
38
- source: "workspace",
39
- entity,
40
- operation: "updated",
41
- entityId: ({ result }) => normalizeRecordId(result?.workspaceId, { fallback: "" }),
42
- realtime: {
43
- event: realtimeEvent,
44
- audience: workspaceAudienceFromEntityId
45
- }
46
- };
47
- }
48
-
49
- function createInviteDecisionEvents({ includeDirectoryAndMembers = false } = {}) {
50
- const events = [
51
- createActorUserEvent({
52
- source: "workspace",
53
- entity: "invitation",
54
- realtimeEvent: "workspace.invitations.pending.changed"
55
- }),
56
- createActorUserEvent({
57
- source: "users",
58
- entity: "bootstrap",
59
- realtimeEvent: "users.bootstrap.changed"
60
- })
61
- ];
62
-
63
- if (includeDirectoryAndMembers) {
64
- events.push(
65
- createActorUserEvent({
66
- source: "workspace",
67
- entity: "directory",
68
- realtimeEvent: "workspaces.changed"
69
- }),
70
- createWorkspaceAudienceEvent({
71
- entity: "member",
72
- realtimeEvent: "workspace.members.changed"
73
- })
74
- );
75
- }
76
-
77
- events.push(
78
- createWorkspaceAudienceEvent({
79
- entity: "invite",
80
- realtimeEvent: "workspace.invites.changed"
81
- })
82
- );
83
-
84
- return events;
85
- }
86
-
87
- function registerWorkspacePendingInvitations(app) {
88
- if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
89
- throw new Error("registerWorkspacePendingInvitations requires application singleton()/service()/actions().");
90
- }
91
-
92
- app.service(
93
- "users.workspace.pending-invitations.service",
94
- (scope) =>
95
- createService({
96
- workspaceInvitesRepository: scope.make("workspaceInvitesRepository"),
97
- workspaceMembershipsRepository: scope.make("workspaceMembershipsRepository")
98
- }),
99
- {
100
- events: deepFreeze({
101
- acceptInviteByToken: createInviteDecisionEvents({
102
- includeDirectoryAndMembers: true
103
- }),
104
- refuseInviteByToken: createInviteDecisionEvents()
105
- })
106
- }
107
- );
108
-
109
- app.actions(
110
- withActionDefaults(workspacePendingInvitationsActions, {
111
- domain: "workspace",
112
- dependencies: {
113
- workspacePendingInvitationsService: "users.workspace.pending-invitations.service"
114
- }
115
- })
116
- );
117
- }
118
-
119
- export { registerWorkspacePendingInvitations };
@@ -1,74 +0,0 @@
1
- import {
2
- EMPTY_INPUT_VALIDATOR
3
- } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
4
- import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
5
- import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
6
- import { resolveActionUser } from "../common/support/resolveActionUser.js";
7
-
8
- const workspacePendingInvitationsActions = Object.freeze([
9
- {
10
- id: "workspace.invitations.pending.list",
11
- version: 1,
12
- kind: "query",
13
- channels: ["api", "automation", "internal"],
14
- surfacesFrom: "enabled",
15
- permission: {
16
- require: "authenticated"
17
- },
18
- inputValidator: EMPTY_INPUT_VALIDATOR,
19
- outputValidator: workspacePendingInvitationsResource.operations.list.outputValidator,
20
- idempotency: "none",
21
- audit: {
22
- actionName: "workspace.invitations.pending.list"
23
- },
24
- observability: {},
25
- async execute(input, context, deps) {
26
- return {
27
- pendingInvites: await deps.workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
28
- context
29
- })
30
- };
31
- }
32
- },
33
- {
34
- id: "workspace.invite.redeem",
35
- version: 1,
36
- kind: "command",
37
- channels: ["api", "automation", "internal"],
38
- surfacesFrom: "enabled",
39
- permission: {
40
- require: "authenticated"
41
- },
42
- inputValidator: {
43
- payload: workspaceMembersResource.operations.redeemInvite.bodyValidator
44
- },
45
- outputValidator: workspaceMembersResource.operations.redeemInvite.outputValidator,
46
- idempotency: "optional",
47
- audit: {
48
- actionName: "workspace.invite.redeem"
49
- },
50
- observability: {},
51
- async execute(input, context, deps) {
52
- const payload = input.payload || {};
53
- const user = resolveActionUser(context, input);
54
-
55
- if (payload.decision === "accept") {
56
- return deps.workspacePendingInvitationsService.acceptInviteByToken({
57
- user,
58
- token: payload.token
59
- }, {
60
- context
61
- });
62
- }
63
-
64
- return deps.workspacePendingInvitationsService.refuseInviteByToken({
65
- user,
66
- token: payload.token
67
- }, {
68
- context
69
- });
70
- }
71
- }
72
- ]);
73
-
74
- export { workspacePendingInvitationsActions };
@@ -1,138 +0,0 @@
1
- import { resolveInviteTokenHash } from "@jskit-ai/auth-core/server/inviteTokens";
2
- import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
3
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
5
- import { authenticatedUserValidator } from "../common/validators/authenticatedUserValidator.js";
6
-
7
- function createService({
8
- workspaceInvitesRepository,
9
- workspaceMembershipsRepository
10
- } = {}) {
11
- if (!workspaceInvitesRepository || !workspaceMembershipsRepository) {
12
- throw new Error("workspacePendingInvitationsService requires invite and membership repositories.");
13
- }
14
-
15
- function requireAuthenticatedInviteUser(user) {
16
- const normalizedUser = authenticatedUserValidator.normalize(user);
17
- if (!normalizedUser) {
18
- throw new AppError(401, "Authentication required.");
19
- }
20
-
21
- return normalizedUser;
22
- }
23
-
24
- function requireInviteTokenHash(token) {
25
- const normalizedToken = normalizeText(token);
26
- if (!normalizedToken) {
27
- throw new AppError(400, "Invite token is required.");
28
- }
29
-
30
- const tokenHash = resolveInviteTokenHash(normalizedToken);
31
- if (!tokenHash) {
32
- throw new AppError(400, "Invite token is invalid.");
33
- }
34
-
35
- return tokenHash;
36
- }
37
-
38
- async function requirePendingInviteForUserByToken(user, token, options = {}) {
39
- const normalizedUser = requireAuthenticatedInviteUser(user);
40
- const tokenHash = requireInviteTokenHash(token);
41
-
42
- const invite = await workspaceInvitesRepository.findPendingByTokenHash(tokenHash, options);
43
- if (!invite) {
44
- throw new AppError(404, "Invitation not found or already handled.");
45
- }
46
-
47
- if (normalizeLowerText(invite.email) !== normalizedUser.email) {
48
- throw new AppError(403, "Invitation email does not match authenticated user.");
49
- }
50
-
51
- return {
52
- user: normalizedUser,
53
- invite
54
- };
55
- }
56
-
57
- async function revokeExpiredInviteAndThrow(invite, options = {}) {
58
- if (invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now()) {
59
- await workspaceInvitesRepository.revokeById(invite.id, options);
60
- throw new AppError(409, "Invitation has expired.");
61
- }
62
- }
63
-
64
- async function listPendingInvitesForUser(user, options = {}) {
65
- const normalizedUser = requireAuthenticatedInviteUser(user);
66
- if (!normalizedUser.email) {
67
- return [];
68
- }
69
-
70
- return workspaceInvitesRepository.listPendingByEmail(normalizedUser.email, options);
71
- }
72
-
73
- function requireWorkspaceIdFromInvite(invite, methodName = "workspacePendingInvitationsService") {
74
- const workspaceId = normalizeRecordId(invite?.workspaceId, { fallback: null });
75
- if (!workspaceId) {
76
- throw new Error(`${methodName} expected invite workspace id.`);
77
- }
78
- return workspaceId;
79
- }
80
-
81
- async function resolveInviteActionInput(user, token, options = {}, methodName = "workspacePendingInvitationsService") {
82
- const resolvedInvite = await requirePendingInviteForUserByToken(user, token, options);
83
- await revokeExpiredInviteAndThrow(resolvedInvite.invite, options);
84
-
85
- return {
86
- resolvedInvite,
87
- workspaceId: requireWorkspaceIdFromInvite(resolvedInvite.invite, methodName)
88
- };
89
- }
90
-
91
- async function acceptInviteByToken({ user, token } = {}, options = {}) {
92
- const { resolvedInvite, workspaceId } = await resolveInviteActionInput(
93
- user,
94
- token,
95
- options,
96
- "workspacePendingInvitationsService.acceptInviteByToken"
97
- );
98
-
99
- await workspaceMembershipsRepository.upsertMembership(
100
- workspaceId,
101
- resolvedInvite.user.id,
102
- {
103
- roleSid: resolvedInvite.invite.roleSid,
104
- status: "active"
105
- },
106
- options
107
- );
108
- await workspaceInvitesRepository.markAcceptedById(resolvedInvite.invite.id, options);
109
-
110
- return {
111
- decision: "accepted",
112
- workspaceId
113
- };
114
- }
115
-
116
- async function refuseInviteByToken({ user, token } = {}, options = {}) {
117
- const { resolvedInvite, workspaceId } = await resolveInviteActionInput(
118
- user,
119
- token,
120
- options,
121
- "workspacePendingInvitationsService.refuseInviteByToken"
122
- );
123
- await workspaceInvitesRepository.revokeById(resolvedInvite.invite.id, options);
124
-
125
- return {
126
- decision: "refused",
127
- workspaceId
128
- };
129
- }
130
-
131
- return Object.freeze({
132
- listPendingInvitesForUser,
133
- acceptInviteByToken,
134
- refuseInviteByToken
135
- });
136
- }
137
-
138
- export { createService };
@@ -1,76 +0,0 @@
1
- import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
- import { workspaceSettingsResource } from "../../shared/resources/workspaceSettingsResource.js";
3
- import { resolveWorkspaceRoutePath } from "../common/support/workspaceRoutePaths.js";
4
- import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
5
- import { resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig } from "../support/workspaceActionSurfaces.js";
6
-
7
- function bootWorkspaceSettings(app) {
8
- if (!app || typeof app.make !== "function") {
9
- throw new Error("bootWorkspaceSettings requires application make().");
10
- }
11
-
12
- const router = app.make("jskit.http.router");
13
- const appConfig = typeof app.has === "function" && app.has("appConfig") ? app.make("appConfig") : {};
14
- const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
15
-
16
- router.register(
17
- "GET",
18
- resolveWorkspaceRoutePath("/settings"),
19
- {
20
- auth: "required",
21
- surface: workspaceRouteSurfaceId,
22
- visibility: "workspace",
23
- meta: {
24
- tags: ["workspace"],
25
- summary: "Get workspace settings and role catalog by workspace slug"
26
- },
27
- paramsValidator: workspaceSlugParamsValidator,
28
- responseValidators: withStandardErrorResponses({
29
- 200: workspaceSettingsResource.operations.view.outputValidator
30
- })
31
- },
32
- async function (request, reply) {
33
- const response = await request.executeAction({
34
- actionId: "workspace.settings.read",
35
- input: {
36
- workspaceSlug: request.input.params.workspaceSlug
37
- }
38
- });
39
- reply.code(200).send(response);
40
- }
41
- );
42
-
43
- router.register(
44
- "PATCH",
45
- resolveWorkspaceRoutePath("/settings"),
46
- {
47
- auth: "required",
48
- surface: workspaceRouteSurfaceId,
49
- visibility: "workspace",
50
- meta: {
51
- tags: ["workspace"],
52
- summary: "Update workspace settings by workspace slug"
53
- },
54
- paramsValidator: workspaceSlugParamsValidator,
55
- bodyValidator: workspaceSettingsResource.operations.patch.bodyValidator,
56
- responseValidators: withStandardErrorResponses(
57
- {
58
- 200: workspaceSettingsResource.operations.patch.outputValidator
59
- },
60
- { includeValidation400: true }
61
- )
62
- },
63
- async function (request, reply) {
64
- const response = await request.executeAction({
65
- actionId: "workspace.settings.update",
66
- input: {
67
- workspaceSlug: request.input.params.workspaceSlug,
68
- patch: request.input.body
69
- }
70
- });
71
- reply.code(200).send(response);
72
- }
73
- );
74
- }
75
-
76
- export { bootWorkspaceSettings };