@shipfox/api-workspaces 16.0.0 → 17.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.
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 58 files with swc (459.5ms)
2
+ Successfully compiled: 58 files with swc (450.22ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @shipfox/api-workspaces
2
2
 
3
+ ## 17.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a4f56ff: Adds the `requireAdministrationActor` guard to `@shipfox/api-auth-context`. The guard rejects impersonated sessions (`UserContext` carrying `impersonatorId`) with the `admin-role-required` failure on every `/admin` route in the auth, projects, workspaces, and runners modules. It protects the first-owner bootstrap route before the system checks roles.
8
+ - a591e8a: Adds `rejectImpersonatedSession` to `@shipfox/api-auth-context`, which throws `impersonation-not-permitted` when the request's user context carries an `impersonatorId`. The runner manual-registration-token, provisioner-token, and workspace invitation routes (create and accept) now reject impersonated sessions, so an impersonated session cannot use these routes to leave behind a credential or durable grant that outlives its bounded token window.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [a4f56ff]
13
+ - Updated dependencies [5ae8b3d]
14
+ - Updated dependencies [a591e8a]
15
+ - Updated dependencies [918d84a]
16
+ - Updated dependencies [9f898d9]
17
+ - @shipfox/api-auth-context@17.0.0
18
+ - @shipfox/api-auth-dto@17.0.0
19
+ - @shipfox/node-postgres@0.5.1
20
+ - @shipfox/node-outbox@0.2.6
21
+
3
22
  ## 16.0.0
4
23
 
5
24
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"admin-workspaces.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/admin-workspaces.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,qBAAqB,EAE3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wCAAwC,CAAC;AACjF,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,uCAAuC,CAAC;AASpF,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AA6GhF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,wBAAwB,CAAC;CACnC,GAAG,UAAU,CA8Fb"}
1
+ {"version":3,"file":"admin-workspaces.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/admin-workspaces.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,qBAAqB,EAE3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wCAAwC,CAAC;AACjF,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,uCAAuC,CAAC;AASpF,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AA6GhF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,wBAAwB,CAAC;CACnC,GAAG,UAAU,CAiGb"}
@@ -1,4 +1,4 @@
1
- import { AUTH_USER, getUserContext } from '@shipfox/api-auth-context';
1
+ import { AUTH_USER, adoptAdministrationActorGuard, getUserContext } from '@shipfox/api-auth-context';
2
2
  import { authInterModuleContract } from '@shipfox/api-auth-dto/inter-module';
3
3
  import { listWorkspaceAdminSummariesResponseSchema, workspaceAdministrationMutationBodySchema, workspaceAdministrationMutationResponseSchema, workspaceAdminLookupQuerySchema } from '@shipfox/api-workspaces-dto';
4
4
  import { isInterModuleKnownError } from '@shipfox/inter-module';
@@ -176,14 +176,14 @@ export function createAdminWorkspacesRoutes(params) {
176
176
  description: 'Reactivate a suspended workspace.',
177
177
  execute: reactivateWorkspace
178
178
  });
179
- return {
179
+ return adoptAdministrationActorGuard({
180
180
  prefix: '/admin/workspaces',
181
181
  routes: [
182
182
  listRoute,
183
183
  suspendRoute,
184
184
  reactivateRoute
185
185
  ]
186
- };
186
+ });
187
187
  }
188
188
 
189
189
  //# sourceMappingURL=admin-workspaces.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/admin-workspaces.ts"],"sourcesContent":["import {AUTH_USER, getUserContext} from '@shipfox/api-auth-context';\nimport {\n type AuthInterModuleClient,\n authInterModuleContract,\n} from '@shipfox/api-auth-dto/inter-module';\nimport type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';\nimport type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';\nimport {\n listWorkspaceAdminSummariesResponseSchema,\n workspaceAdministrationMutationBodySchema,\n workspaceAdministrationMutationResponseSchema,\n workspaceAdminLookupQuerySchema,\n} from '@shipfox/api-workspaces-dto';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {decodeStringIdCursor, encodeStringIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {\n listWorkspaceAdministratorSummaries,\n reactivateWorkspace,\n suspendWorkspace,\n type WorkspaceAdministrationMutationContext,\n type WorkspaceAdministratorSummary,\n} from '#core/admin-workspaces.js';\nimport {\n WorkspaceAdminIdempotencyKeyReuseError,\n WorkspaceAlreadySuspendedError,\n WorkspaceDeletedError,\n WorkspaceNotFoundError,\n WorkspaceNotSuspendedError,\n} from '#core/errors.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nfunction translateAdminRoleError(error: unknown, message: string): void {\n if (\n isInterModuleKnownError(authInterModuleContract.methods.requireAdminRole, error) &&\n error.code === 'admin-role-required'\n ) {\n throw new ClientError(message, 'forbidden', {\n status: 403,\n details: {required_role: error.details.requiredRole},\n });\n }\n}\n\nfunction translateAdministrationError(error: unknown): never {\n translateAdminRoleError(error, 'Administrator operator role required');\n if (error instanceof WorkspaceNotFoundError) {\n throw new ClientError('Workspace not found', 'workspace-not-found', {status: 404});\n }\n if (error instanceof WorkspaceDeletedError) {\n throw new ClientError('Workspace is deleted', 'workspace-deleted', {status: 409});\n }\n if (error instanceof WorkspaceAlreadySuspendedError) {\n throw new ClientError('Workspace is already suspended', 'workspace-already-suspended', {\n status: 409,\n });\n }\n if (error instanceof WorkspaceNotSuspendedError) {\n throw new ClientError('Workspace is not suspended', 'workspace-not-suspended', {\n status: 409,\n });\n }\n if (error instanceof WorkspaceAdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different workspace command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n throw error;\n}\n\nfunction translateObserverAdministrationError(error: unknown): never {\n translateAdminRoleError(error, 'Administrator observer role required');\n throw error;\n}\n\nfunction toWorkspaceAdministrationMutationDto(result: {\n workspaceId: string;\n status: 'active' | 'suspended';\n correlationId: string;\n}) {\n return {\n workspace_id: result.workspaceId,\n status: result.status,\n correlation_id: result.correlationId,\n };\n}\n\nfunction toWorkspaceAdministratorSummaryDto(summary: WorkspaceAdministratorSummary) {\n return {\n id: summary.id,\n name: summary.name,\n status: summary.status,\n member_summary: summary.memberSummary,\n project_summary: summary.projectSummary,\n job_counts: summary.jobCounts,\n created_at: summary.createdAt.toISOString(),\n updated_at: summary.updatedAt.toISOString(),\n };\n}\n\nexport function createAdminWorkspacesRoutes(params: {\n auth: AuthInterModuleClient;\n projects: ProjectsModuleClient;\n runners: RunnersInterModuleClient;\n}): RouteGroup {\n const listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded safe workspace summaries for administrators.',\n auth: AUTH_USER,\n schema: {\n querystring: workspaceAdminLookupQuerySchema,\n response: {200: listWorkspaceAdminSummariesResponseSchema},\n },\n errorHandler: translateObserverAdministrationError,\n handler: async (request) => {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n await params.auth.requireAdminRole({\n userId: client.userId,\n minimumRole: 'admin-observer',\n });\n\n const {workspace_id: workspaceId, search, status, limit, cursor} = request.query;\n const decodedCursor = decodeStringIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listWorkspaceAdministratorSummaries({\n workspaceId,\n search,\n status,\n limit,\n cursor: decodedCursor,\n projects: params.projects,\n runners: params.runners,\n });\n\n return {\n workspaces: result.workspaces.map(toWorkspaceAdministratorSummaryDto),\n next_cursor: result.nextCursor ? encodeStringIdCursor(result.nextCursor) : null,\n };\n },\n });\n\n const createMutationRoute = (routeParams: {\n action: 'suspend' | 'reactivate';\n description: string;\n execute: (context: WorkspaceAdministrationMutationContext) => Promise<{\n workspaceId: string;\n status: 'active' | 'suspended';\n correlationId: string;\n }>;\n }) =>\n defineRoute({\n method: 'POST',\n path: `/:workspaceId/${routeParams.action}`,\n description: routeParams.description,\n auth: AUTH_USER,\n schema: {\n params: z.object({workspaceId: z.string().uuid()}),\n body: workspaceAdministrationMutationBodySchema,\n response: {200: workspaceAdministrationMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const actorId = requireActorId(request);\n const actorRole = (\n await params.auth.requireAdminRole({userId: actorId, minimumRole: 'admin-operator'})\n ).role;\n return toWorkspaceAdministrationMutationDto(\n await routeParams.execute({\n actorId,\n actorRole,\n workspaceId: request.params.workspaceId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n }),\n );\n },\n });\n\n const suspendRoute = createMutationRoute({\n action: 'suspend',\n description: 'Suspend a workspace for reversible administrator moderation.',\n execute: suspendWorkspace,\n });\n const reactivateRoute = createMutationRoute({\n action: 'reactivate',\n description: 'Reactivate a suspended workspace.',\n execute: reactivateWorkspace,\n });\n\n return {prefix: '/admin/workspaces', routes: [listRoute, suspendRoute, reactivateRoute]};\n}\n"],"names":["AUTH_USER","getUserContext","authInterModuleContract","listWorkspaceAdminSummariesResponseSchema","workspaceAdministrationMutationBodySchema","workspaceAdministrationMutationResponseSchema","workspaceAdminLookupQuerySchema","isInterModuleKnownError","decodeStringIdCursor","encodeStringIdCursor","ClientError","defineRoute","z","listWorkspaceAdministratorSummaries","reactivateWorkspace","suspendWorkspace","WorkspaceAdminIdempotencyKeyReuseError","WorkspaceAlreadySuspendedError","WorkspaceDeletedError","WorkspaceNotFoundError","WorkspaceNotSuspendedError","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","translateAdminRoleError","error","message","methods","requireAdminRole","code","details","required_role","requiredRole","translateAdministrationError","translateObserverAdministrationError","toWorkspaceAdministrationMutationDto","result","workspace_id","workspaceId","correlation_id","correlationId","toWorkspaceAdministratorSummaryDto","summary","id","name","member_summary","memberSummary","project_summary","projectSummary","job_counts","jobCounts","created_at","createdAt","toISOString","updated_at","updatedAt","createAdminWorkspacesRoutes","params","listRoute","method","path","description","auth","schema","querystring","response","errorHandler","handler","minimumRole","search","limit","cursor","query","decodedCursor","projects","runners","workspaces","map","next_cursor","nextCursor","createMutationRoute","routeParams","action","object","string","uuid","body","actorId","actorRole","role","execute","reason","idempotencyKey","suspendRoute","reactivateRoute","prefix","routes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/admin-workspaces.ts"],"sourcesContent":["import {AUTH_USER, adoptAdministrationActorGuard, getUserContext} from '@shipfox/api-auth-context';\nimport {\n type AuthInterModuleClient,\n authInterModuleContract,\n} from '@shipfox/api-auth-dto/inter-module';\nimport type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';\nimport type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-module';\nimport {\n listWorkspaceAdminSummariesResponseSchema,\n workspaceAdministrationMutationBodySchema,\n workspaceAdministrationMutationResponseSchema,\n workspaceAdminLookupQuerySchema,\n} from '@shipfox/api-workspaces-dto';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {decodeStringIdCursor, encodeStringIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {\n listWorkspaceAdministratorSummaries,\n reactivateWorkspace,\n suspendWorkspace,\n type WorkspaceAdministrationMutationContext,\n type WorkspaceAdministratorSummary,\n} from '#core/admin-workspaces.js';\nimport {\n WorkspaceAdminIdempotencyKeyReuseError,\n WorkspaceAlreadySuspendedError,\n WorkspaceDeletedError,\n WorkspaceNotFoundError,\n WorkspaceNotSuspendedError,\n} from '#core/errors.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nfunction translateAdminRoleError(error: unknown, message: string): void {\n if (\n isInterModuleKnownError(authInterModuleContract.methods.requireAdminRole, error) &&\n error.code === 'admin-role-required'\n ) {\n throw new ClientError(message, 'forbidden', {\n status: 403,\n details: {required_role: error.details.requiredRole},\n });\n }\n}\n\nfunction translateAdministrationError(error: unknown): never {\n translateAdminRoleError(error, 'Administrator operator role required');\n if (error instanceof WorkspaceNotFoundError) {\n throw new ClientError('Workspace not found', 'workspace-not-found', {status: 404});\n }\n if (error instanceof WorkspaceDeletedError) {\n throw new ClientError('Workspace is deleted', 'workspace-deleted', {status: 409});\n }\n if (error instanceof WorkspaceAlreadySuspendedError) {\n throw new ClientError('Workspace is already suspended', 'workspace-already-suspended', {\n status: 409,\n });\n }\n if (error instanceof WorkspaceNotSuspendedError) {\n throw new ClientError('Workspace is not suspended', 'workspace-not-suspended', {\n status: 409,\n });\n }\n if (error instanceof WorkspaceAdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different workspace command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n throw error;\n}\n\nfunction translateObserverAdministrationError(error: unknown): never {\n translateAdminRoleError(error, 'Administrator observer role required');\n throw error;\n}\n\nfunction toWorkspaceAdministrationMutationDto(result: {\n workspaceId: string;\n status: 'active' | 'suspended';\n correlationId: string;\n}) {\n return {\n workspace_id: result.workspaceId,\n status: result.status,\n correlation_id: result.correlationId,\n };\n}\n\nfunction toWorkspaceAdministratorSummaryDto(summary: WorkspaceAdministratorSummary) {\n return {\n id: summary.id,\n name: summary.name,\n status: summary.status,\n member_summary: summary.memberSummary,\n project_summary: summary.projectSummary,\n job_counts: summary.jobCounts,\n created_at: summary.createdAt.toISOString(),\n updated_at: summary.updatedAt.toISOString(),\n };\n}\n\nexport function createAdminWorkspacesRoutes(params: {\n auth: AuthInterModuleClient;\n projects: ProjectsModuleClient;\n runners: RunnersInterModuleClient;\n}): RouteGroup {\n const listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded safe workspace summaries for administrators.',\n auth: AUTH_USER,\n schema: {\n querystring: workspaceAdminLookupQuerySchema,\n response: {200: listWorkspaceAdminSummariesResponseSchema},\n },\n errorHandler: translateObserverAdministrationError,\n handler: async (request) => {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n await params.auth.requireAdminRole({\n userId: client.userId,\n minimumRole: 'admin-observer',\n });\n\n const {workspace_id: workspaceId, search, status, limit, cursor} = request.query;\n const decodedCursor = decodeStringIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listWorkspaceAdministratorSummaries({\n workspaceId,\n search,\n status,\n limit,\n cursor: decodedCursor,\n projects: params.projects,\n runners: params.runners,\n });\n\n return {\n workspaces: result.workspaces.map(toWorkspaceAdministratorSummaryDto),\n next_cursor: result.nextCursor ? encodeStringIdCursor(result.nextCursor) : null,\n };\n },\n });\n\n const createMutationRoute = (routeParams: {\n action: 'suspend' | 'reactivate';\n description: string;\n execute: (context: WorkspaceAdministrationMutationContext) => Promise<{\n workspaceId: string;\n status: 'active' | 'suspended';\n correlationId: string;\n }>;\n }) =>\n defineRoute({\n method: 'POST',\n path: `/:workspaceId/${routeParams.action}`,\n description: routeParams.description,\n auth: AUTH_USER,\n schema: {\n params: z.object({workspaceId: z.string().uuid()}),\n body: workspaceAdministrationMutationBodySchema,\n response: {200: workspaceAdministrationMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const actorId = requireActorId(request);\n const actorRole = (\n await params.auth.requireAdminRole({userId: actorId, minimumRole: 'admin-operator'})\n ).role;\n return toWorkspaceAdministrationMutationDto(\n await routeParams.execute({\n actorId,\n actorRole,\n workspaceId: request.params.workspaceId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n }),\n );\n },\n });\n\n const suspendRoute = createMutationRoute({\n action: 'suspend',\n description: 'Suspend a workspace for reversible administrator moderation.',\n execute: suspendWorkspace,\n });\n const reactivateRoute = createMutationRoute({\n action: 'reactivate',\n description: 'Reactivate a suspended workspace.',\n execute: reactivateWorkspace,\n });\n\n return adoptAdministrationActorGuard({\n prefix: '/admin/workspaces',\n routes: [listRoute, suspendRoute, reactivateRoute],\n });\n}\n"],"names":["AUTH_USER","adoptAdministrationActorGuard","getUserContext","authInterModuleContract","listWorkspaceAdminSummariesResponseSchema","workspaceAdministrationMutationBodySchema","workspaceAdministrationMutationResponseSchema","workspaceAdminLookupQuerySchema","isInterModuleKnownError","decodeStringIdCursor","encodeStringIdCursor","ClientError","defineRoute","z","listWorkspaceAdministratorSummaries","reactivateWorkspace","suspendWorkspace","WorkspaceAdminIdempotencyKeyReuseError","WorkspaceAlreadySuspendedError","WorkspaceDeletedError","WorkspaceNotFoundError","WorkspaceNotSuspendedError","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","translateAdminRoleError","error","message","methods","requireAdminRole","code","details","required_role","requiredRole","translateAdministrationError","translateObserverAdministrationError","toWorkspaceAdministrationMutationDto","result","workspace_id","workspaceId","correlation_id","correlationId","toWorkspaceAdministratorSummaryDto","summary","id","name","member_summary","memberSummary","project_summary","projectSummary","job_counts","jobCounts","created_at","createdAt","toISOString","updated_at","updatedAt","createAdminWorkspacesRoutes","params","listRoute","method","path","description","auth","schema","querystring","response","errorHandler","handler","minimumRole","search","limit","cursor","query","decodedCursor","projects","runners","workspaces","map","next_cursor","nextCursor","createMutationRoute","routeParams","action","object","string","uuid","body","actorId","actorRole","role","execute","reason","idempotencyKey","suspendRoute","reactivateRoute","prefix","routes"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,6BAA6B,EAAEC,cAAc,QAAO,4BAA4B;AACnG,SAEEC,uBAAuB,QAClB,qCAAqC;AAG5C,SACEC,yCAAyC,EACzCC,yCAAyC,EACzCC,6CAA6C,EAC7CC,+BAA+B,QAC1B,8BAA8B;AACrC,SAAQC,uBAAuB,QAAO,wBAAwB;AAC9D,SAAQC,oBAAoB,EAAEC,oBAAoB,QAAO,wBAAwB;AACjF,SAAQC,WAAW,EAAEC,WAAW,QAAwB,wBAAwB;AAEhF,SAAQC,CAAC,QAAO,MAAM;AACtB,SACEC,mCAAmC,EACnCC,mBAAmB,EACnBC,gBAAgB,QAGX,4BAA4B;AACnC,SACEC,sCAAsC,EACtCC,8BAA8B,EAC9BC,qBAAqB,EACrBC,sBAAsB,EACtBC,0BAA0B,QACrB,kBAAkB;AAEzB,MAAMC,0BAA0B;AAEhC,SAASC,eAAeC,OAAuB;IAC7C,MAAMC,SAASvB,eAAesB;IAC9B,IAAI,CAACC,QAAQ;QACX,MAAM,IAAId,YAAY,2BAA2B,gBAAgB;YAACe,QAAQ;QAAG;IAC/E;IACA,OAAOD,OAAOE,MAAM;AACtB;AAEA,SAASC,sBAAsBJ,OAAuB;IACpD,MAAMK,QAAQL,QAAQM,OAAO,CAAC,kBAAkB;IAChD,MAAMC,MAAMC,MAAMC,OAAO,CAACJ,SAASA,KAAK,CAAC,EAAE,GAAGA;IAC9C,IAAI,CAACE,OAAOA,IAAIG,IAAI,GAAGC,MAAM,KAAK,KAAKJ,IAAII,MAAM,GAAGb,yBAAyB;QAC3E,MAAM,IAAIX,YAAY,sCAAsC,4BAA4B;YACtFe,QAAQ;QACV;IACF;IACA,OAAOK;AACT;AAEA,SAASK,wBAAwBC,KAAc,EAAEC,OAAe;IAC9D,IACE9B,wBAAwBL,wBAAwBoC,OAAO,CAACC,gBAAgB,EAAEH,UAC1EA,MAAMI,IAAI,KAAK,uBACf;QACA,MAAM,IAAI9B,YAAY2B,SAAS,aAAa;YAC1CZ,QAAQ;YACRgB,SAAS;gBAACC,eAAeN,MAAMK,OAAO,CAACE,YAAY;YAAA;QACrD;IACF;AACF;AAEA,SAASC,6BAA6BR,KAAc;IAClDD,wBAAwBC,OAAO;IAC/B,IAAIA,iBAAiBjB,wBAAwB;QAC3C,MAAM,IAAIT,YAAY,uBAAuB,uBAAuB;YAACe,QAAQ;QAAG;IAClF;IACA,IAAIW,iBAAiBlB,uBAAuB;QAC1C,MAAM,IAAIR,YAAY,wBAAwB,qBAAqB;YAACe,QAAQ;QAAG;IACjF;IACA,IAAIW,iBAAiBnB,gCAAgC;QACnD,MAAM,IAAIP,YAAY,kCAAkC,+BAA+B;YACrFe,QAAQ;QACV;IACF;IACA,IAAIW,iBAAiBhB,4BAA4B;QAC/C,MAAM,IAAIV,YAAY,8BAA8B,2BAA2B;YAC7Ee,QAAQ;QACV;IACF;IACA,IAAIW,iBAAiBpB,wCAAwC;QAC3D,MAAM,IAAIN,YACR,sEACA,0BACA;YAACe,QAAQ;QAAG;IAEhB;IACA,MAAMW;AACR;AAEA,SAASS,qCAAqCT,KAAc;IAC1DD,wBAAwBC,OAAO;IAC/B,MAAMA;AACR;AAEA,SAASU,qCAAqCC,MAI7C;IACC,OAAO;QACLC,cAAcD,OAAOE,WAAW;QAChCxB,QAAQsB,OAAOtB,MAAM;QACrByB,gBAAgBH,OAAOI,aAAa;IACtC;AACF;AAEA,SAASC,mCAAmCC,OAAsC;IAChF,OAAO;QACLC,IAAID,QAAQC,EAAE;QACdC,MAAMF,QAAQE,IAAI;QAClB9B,QAAQ4B,QAAQ5B,MAAM;QACtB+B,gBAAgBH,QAAQI,aAAa;QACrCC,iBAAiBL,QAAQM,cAAc;QACvCC,YAAYP,QAAQQ,SAAS;QAC7BC,YAAYT,QAAQU,SAAS,CAACC,WAAW;QACzCC,YAAYZ,QAAQa,SAAS,CAACF,WAAW;IAC3C;AACF;AAEA,OAAO,SAASG,4BAA4BC,MAI3C;IACC,MAAMC,YAAY1D,YAAY;QAC5B2D,QAAQ;QACRC,MAAM;QACNC,aAAa;QACbC,MAAM1E;QACN2E,QAAQ;YACNC,aAAarE;YACbsE,UAAU;gBAAC,KAAKzE;YAAyC;QAC3D;QACA0E,cAAchC;QACdiC,SAAS,OAAOvD;YACd,MAAMC,SAASvB,eAAesB;YAC9B,IAAI,CAACC,QAAQ;gBACX,MAAM,IAAId,YAAY,2BAA2B,gBAAgB;oBAACe,QAAQ;gBAAG;YAC/E;YACA,MAAM2C,OAAOK,IAAI,CAAClC,gBAAgB,CAAC;gBACjCb,QAAQF,OAAOE,MAAM;gBACrBqD,aAAa;YACf;YAEA,MAAM,EAAC/B,cAAcC,WAAW,EAAE+B,MAAM,EAAEvD,MAAM,EAAEwD,KAAK,EAAEC,MAAM,EAAC,GAAG3D,QAAQ4D,KAAK;YAChF,MAAMC,gBAAgB5E,qBAAqB0E;YAC3C,IAAIA,UAAU,CAACE,eAAe;gBAC5B,MAAM,IAAI1E,YAAY,kBAAkB,kBAAkB;oBAACe,QAAQ;gBAAG;YACxE;YAEA,MAAMsB,SAAS,MAAMlC,oCAAoC;gBACvDoC;gBACA+B;gBACAvD;gBACAwD;gBACAC,QAAQE;gBACRC,UAAUjB,OAAOiB,QAAQ;gBACzBC,SAASlB,OAAOkB,OAAO;YACzB;YAEA,OAAO;gBACLC,YAAYxC,OAAOwC,UAAU,CAACC,GAAG,CAACpC;gBAClCqC,aAAa1C,OAAO2C,UAAU,GAAGjF,qBAAqBsC,OAAO2C,UAAU,IAAI;YAC7E;QACF;IACF;IAEA,MAAMC,sBAAsB,CAACC,cAS3BjF,YAAY;YACV2D,QAAQ;YACRC,MAAM,CAAC,cAAc,EAAEqB,YAAYC,MAAM,EAAE;YAC3CrB,aAAaoB,YAAYpB,WAAW;YACpCC,MAAM1E;YACN2E,QAAQ;gBACNN,QAAQxD,EAAEkF,MAAM,CAAC;oBAAC7C,aAAarC,EAAEmF,MAAM,GAAGC,IAAI;gBAAE;gBAChDC,MAAM7F;gBACNwE,UAAU;oBAAC,KAAKvE;gBAA6C;YAC/D;YACAwE,cAAcjC;YACdkC,SAAS,OAAOvD;gBACd,MAAM2E,UAAU5E,eAAeC;gBAC/B,MAAM4E,YAAY,AAChB,CAAA,MAAM/B,OAAOK,IAAI,CAAClC,gBAAgB,CAAC;oBAACb,QAAQwE;oBAASnB,aAAa;gBAAgB,EAAC,EACnFqB,IAAI;gBACN,OAAOtD,qCACL,MAAM8C,YAAYS,OAAO,CAAC;oBACxBH;oBACAC;oBACAlD,aAAa1B,QAAQ6C,MAAM,CAACnB,WAAW;oBACvCqD,QAAQ/E,QAAQ0E,IAAI,CAACK,MAAM;oBAC3BC,gBAAgB5E,sBAAsBJ;oBACtC4B,eAAe5B,QAAQ+B,EAAE;gBAC3B;YAEJ;QACF;IAEF,MAAMkD,eAAeb,oBAAoB;QACvCE,QAAQ;QACRrB,aAAa;QACb6B,SAAStF;IACX;IACA,MAAM0F,kBAAkBd,oBAAoB;QAC1CE,QAAQ;QACRrB,aAAa;QACb6B,SAASvF;IACX;IAEA,OAAOd,8BAA8B;QACnC0G,QAAQ;QACRC,QAAQ;YAACtC;YAAWmC;YAAcC;SAAgB;IACpD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"accept.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/invitations/accept.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,qBAAqB,iDAuDhC,CAAC"}
1
+ {"version":3,"file":"accept.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/invitations/accept.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,qBAAqB,iDAwDhC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { AUTH_USER, getUserContext } from '@shipfox/api-auth-context';
1
+ import { AUTH_USER, getUserContext, rejectImpersonatedSession } from '@shipfox/api-auth-context';
2
2
  import { acceptInvitationBodySchema, acceptInvitationResponseSchema } from '@shipfox/api-workspaces-dto';
3
3
  import { ClientError, defineRoute } from '@shipfox/node-fastify';
4
4
  import { InvitationEmailMismatchError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError } from '#core/errors.js';
@@ -45,6 +45,7 @@ export const acceptInvitationRoute = defineRoute({
45
45
  status: 401
46
46
  });
47
47
  }
48
+ rejectImpersonatedSession(request);
48
49
  const { token } = request.body;
49
50
  const result = await acceptWorkspaceInvitation({
50
51
  token,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/presentation/routes/invitations/accept.ts"],"sourcesContent":["import {AUTH_USER, getUserContext} from '@shipfox/api-auth-context';\nimport {\n acceptInvitationBodySchema,\n acceptInvitationResponseSchema,\n} from '@shipfox/api-workspaces-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {\n InvitationEmailMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n} from '#core/errors.js';\nimport {acceptWorkspaceInvitation} from '#core/index.js';\n\nexport const acceptInvitationRoute = defineRoute({\n method: 'POST',\n path: '/accept',\n description: 'Accept an invitation to join a workspace.',\n auth: AUTH_USER,\n schema: {\n body: acceptInvitationBodySchema,\n response: {\n 200: acceptInvitationResponseSchema,\n 201: acceptInvitationResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof TokenInvalidError) {\n throw new ClientError('Invitation token is invalid', 'token-invalid', {status: 410});\n }\n if (error instanceof TokenAlreadyUsedError) {\n throw new ClientError('Invitation has already been accepted', 'token-already-used', {\n status: 410,\n });\n }\n if (error instanceof TokenExpiredError) {\n throw new ClientError('Invitation has expired', 'token-expired', {status: 410});\n }\n if (error instanceof InvitationEmailMismatchError) {\n throw new ClientError('Invitation email does not match authenticated user', 'forbidden', {\n status: 403,\n });\n }\n throw error;\n },\n handler: async (request, reply) => {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const {token} = request.body;\n const result = await acceptWorkspaceInvitation({\n token,\n userId: client.userId,\n email: client.email,\n name: client.name,\n });\n\n reply.code(result.alreadyMember ? 200 : 201);\n return {\n membership: {\n id: result.membership.id,\n user_id: result.membership.userId,\n workspace_id: result.membership.workspaceId,\n },\n already_member: result.alreadyMember,\n };\n },\n});\n"],"names":["AUTH_USER","getUserContext","acceptInvitationBodySchema","acceptInvitationResponseSchema","ClientError","defineRoute","InvitationEmailMismatchError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","acceptWorkspaceInvitation","acceptInvitationRoute","method","path","description","auth","schema","body","response","errorHandler","error","status","handler","request","reply","client","token","result","userId","email","name","code","alreadyMember","membership","id","user_id","workspace_id","workspaceId","already_member"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,cAAc,QAAO,4BAA4B;AACpE,SACEC,0BAA0B,EAC1BC,8BAA8B,QACzB,8BAA8B;AACrC,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SACEC,4BAA4B,EAC5BC,qBAAqB,EACrBC,iBAAiB,EACjBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,yBAAyB,QAAO,iBAAiB;AAEzD,OAAO,MAAMC,wBAAwBN,YAAY;IAC/CO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMf;IACNgB,QAAQ;QACNC,MAAMf;QACNgB,UAAU;YACR,KAAKf;YACL,KAAKA;QACP;IACF;IACAgB,cAAc,CAACC;QACb,IAAIA,iBAAiBX,mBAAmB;YACtC,MAAM,IAAIL,YAAY,+BAA+B,iBAAiB;gBAACiB,QAAQ;YAAG;QACpF;QACA,IAAID,iBAAiBb,uBAAuB;YAC1C,MAAM,IAAIH,YAAY,wCAAwC,sBAAsB;gBAClFiB,QAAQ;YACV;QACF;QACA,IAAID,iBAAiBZ,mBAAmB;YACtC,MAAM,IAAIJ,YAAY,0BAA0B,iBAAiB;gBAACiB,QAAQ;YAAG;QAC/E;QACA,IAAID,iBAAiBd,8BAA8B;YACjD,MAAM,IAAIF,YAAY,sDAAsD,aAAa;gBACvFiB,QAAQ;YACV;QACF;QACA,MAAMD;IACR;IACAE,SAAS,OAAOC,SAASC;QACvB,MAAMC,SAASxB,eAAesB;QAC9B,IAAI,CAACE,QAAQ;YACX,MAAM,IAAIrB,YAAY,2BAA2B,gBAAgB;gBAACiB,QAAQ;YAAG;QAC/E;QAEA,MAAM,EAACK,KAAK,EAAC,GAAGH,QAAQN,IAAI;QAC5B,MAAMU,SAAS,MAAMjB,0BAA0B;YAC7CgB;YACAE,QAAQH,OAAOG,MAAM;YACrBC,OAAOJ,OAAOI,KAAK;YACnBC,MAAML,OAAOK,IAAI;QACnB;QAEAN,MAAMO,IAAI,CAACJ,OAAOK,aAAa,GAAG,MAAM;QACxC,OAAO;YACLC,YAAY;gBACVC,IAAIP,OAAOM,UAAU,CAACC,EAAE;gBACxBC,SAASR,OAAOM,UAAU,CAACL,MAAM;gBACjCQ,cAAcT,OAAOM,UAAU,CAACI,WAAW;YAC7C;YACAC,gBAAgBX,OAAOK,aAAa;QACtC;IACF;AACF,GAAG"}
1
+ {"version":3,"sources":["../../../../src/presentation/routes/invitations/accept.ts"],"sourcesContent":["import {AUTH_USER, getUserContext, rejectImpersonatedSession} from '@shipfox/api-auth-context';\nimport {\n acceptInvitationBodySchema,\n acceptInvitationResponseSchema,\n} from '@shipfox/api-workspaces-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {\n InvitationEmailMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n} from '#core/errors.js';\nimport {acceptWorkspaceInvitation} from '#core/index.js';\n\nexport const acceptInvitationRoute = defineRoute({\n method: 'POST',\n path: '/accept',\n description: 'Accept an invitation to join a workspace.',\n auth: AUTH_USER,\n schema: {\n body: acceptInvitationBodySchema,\n response: {\n 200: acceptInvitationResponseSchema,\n 201: acceptInvitationResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof TokenInvalidError) {\n throw new ClientError('Invitation token is invalid', 'token-invalid', {status: 410});\n }\n if (error instanceof TokenAlreadyUsedError) {\n throw new ClientError('Invitation has already been accepted', 'token-already-used', {\n status: 410,\n });\n }\n if (error instanceof TokenExpiredError) {\n throw new ClientError('Invitation has expired', 'token-expired', {status: 410});\n }\n if (error instanceof InvitationEmailMismatchError) {\n throw new ClientError('Invitation email does not match authenticated user', 'forbidden', {\n status: 403,\n });\n }\n throw error;\n },\n handler: async (request, reply) => {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n rejectImpersonatedSession(request);\n\n const {token} = request.body;\n const result = await acceptWorkspaceInvitation({\n token,\n userId: client.userId,\n email: client.email,\n name: client.name,\n });\n\n reply.code(result.alreadyMember ? 200 : 201);\n return {\n membership: {\n id: result.membership.id,\n user_id: result.membership.userId,\n workspace_id: result.membership.workspaceId,\n },\n already_member: result.alreadyMember,\n };\n },\n});\n"],"names":["AUTH_USER","getUserContext","rejectImpersonatedSession","acceptInvitationBodySchema","acceptInvitationResponseSchema","ClientError","defineRoute","InvitationEmailMismatchError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","acceptWorkspaceInvitation","acceptInvitationRoute","method","path","description","auth","schema","body","response","errorHandler","error","status","handler","request","reply","client","token","result","userId","email","name","code","alreadyMember","membership","id","user_id","workspace_id","workspaceId","already_member"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,cAAc,EAAEC,yBAAyB,QAAO,4BAA4B;AAC/F,SACEC,0BAA0B,EAC1BC,8BAA8B,QACzB,8BAA8B;AACrC,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SACEC,4BAA4B,EAC5BC,qBAAqB,EACrBC,iBAAiB,EACjBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,yBAAyB,QAAO,iBAAiB;AAEzD,OAAO,MAAMC,wBAAwBN,YAAY;IAC/CO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMhB;IACNiB,QAAQ;QACNC,MAAMf;QACNgB,UAAU;YACR,KAAKf;YACL,KAAKA;QACP;IACF;IACAgB,cAAc,CAACC;QACb,IAAIA,iBAAiBX,mBAAmB;YACtC,MAAM,IAAIL,YAAY,+BAA+B,iBAAiB;gBAACiB,QAAQ;YAAG;QACpF;QACA,IAAID,iBAAiBb,uBAAuB;YAC1C,MAAM,IAAIH,YAAY,wCAAwC,sBAAsB;gBAClFiB,QAAQ;YACV;QACF;QACA,IAAID,iBAAiBZ,mBAAmB;YACtC,MAAM,IAAIJ,YAAY,0BAA0B,iBAAiB;gBAACiB,QAAQ;YAAG;QAC/E;QACA,IAAID,iBAAiBd,8BAA8B;YACjD,MAAM,IAAIF,YAAY,sDAAsD,aAAa;gBACvFiB,QAAQ;YACV;QACF;QACA,MAAMD;IACR;IACAE,SAAS,OAAOC,SAASC;QACvB,MAAMC,SAASzB,eAAeuB;QAC9B,IAAI,CAACE,QAAQ;YACX,MAAM,IAAIrB,YAAY,2BAA2B,gBAAgB;gBAACiB,QAAQ;YAAG;QAC/E;QACApB,0BAA0BsB;QAE1B,MAAM,EAACG,KAAK,EAAC,GAAGH,QAAQN,IAAI;QAC5B,MAAMU,SAAS,MAAMjB,0BAA0B;YAC7CgB;YACAE,QAAQH,OAAOG,MAAM;YACrBC,OAAOJ,OAAOI,KAAK;YACnBC,MAAML,OAAOK,IAAI;QACnB;QAEAN,MAAMO,IAAI,CAACJ,OAAOK,aAAa,GAAG,MAAM;QACxC,OAAO;YACLC,YAAY;gBACVC,IAAIP,OAAOM,UAAU,CAACC,EAAE;gBACxBC,SAASR,OAAOM,UAAU,CAACL,MAAM;gBACjCQ,cAAcT,OAAOM,UAAU,CAACI,WAAW;YAC7C;YACAC,gBAAgBX,OAAOK,aAAa;QACtC;IACF;AACF,GAAG"}
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/invitations/create.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,qBAAqB,iDA+ChC,CAAC"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/invitations/create.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,qBAAqB,iDAgDhC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { AUTH_USER, getUserContext, requireWorkspaceAccess } from '@shipfox/api-auth-context';
1
+ import { AUTH_USER, getUserContext, rejectImpersonatedSession, requireWorkspaceAccess } from '@shipfox/api-auth-context';
2
2
  import { createInvitationBodySchema, invitationDtoSchema } from '@shipfox/api-workspaces-dto';
3
3
  import { ClientError, defineRoute } from '@shipfox/node-fastify';
4
4
  import { z } from 'zod';
@@ -50,6 +50,7 @@ export const createInvitationRoute = defineRoute({
50
50
  });
51
51
  }
52
52
  const { workspaceId } = request.params;
53
+ rejectImpersonatedSession(request);
53
54
  requireWorkspaceAccess({
54
55
  request,
55
56
  workspaceId
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/presentation/routes/invitations/create.ts"],"sourcesContent":["import {AUTH_USER, getUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport {createInvitationBodySchema, invitationDtoSchema} from '@shipfox/api-workspaces-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {z} from 'zod';\nimport {\n MembershipRequiredError,\n OpenInvitationExistsError,\n WorkspaceInactiveError,\n WorkspaceNotFoundError,\n} from '#core/errors.js';\nimport {createWorkspaceInvitation} from '#core/index.js';\nimport {toInvitationDto} from '#presentation/dto/index.js';\n\nexport const createInvitationRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Invite someone to join a workspace by email.',\n auth: AUTH_USER,\n schema: {\n params: z.object({workspaceId: z.string().uuid()}),\n body: createInvitationBodySchema,\n response: {\n 201: invitationDtoSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof WorkspaceNotFoundError) {\n throw new ClientError('Workspace not found', 'not-found', {status: 404});\n }\n if (error instanceof MembershipRequiredError) {\n throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});\n }\n if (error instanceof OpenInvitationExistsError) {\n throw new ClientError(error.message, 'open-invitation-exists', {status: 409});\n }\n if (error instanceof WorkspaceInactiveError) {\n throw new ClientError('Workspace is not active', 'workspace-inactive', {status: 403});\n }\n throw error;\n },\n handler: async (request, reply) => {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const {workspaceId} = request.params;\n requireWorkspaceAccess({request, workspaceId});\n const {email} = request.body;\n const invitation = await createWorkspaceInvitation({\n workspaceId,\n email,\n invitedByUserId: client.userId,\n invitedByDisplay: client.name ?? 'A workspace admin',\n invitedByMemberships: client.memberships,\n });\n\n reply.code(201);\n return toInvitationDto(invitation);\n },\n});\n"],"names":["AUTH_USER","getUserContext","requireWorkspaceAccess","createInvitationBodySchema","invitationDtoSchema","ClientError","defineRoute","z","MembershipRequiredError","OpenInvitationExistsError","WorkspaceInactiveError","WorkspaceNotFoundError","createWorkspaceInvitation","toInvitationDto","createInvitationRoute","method","path","description","auth","schema","params","object","workspaceId","string","uuid","body","response","errorHandler","error","status","message","handler","request","reply","client","email","invitation","invitedByUserId","userId","invitedByDisplay","name","invitedByMemberships","memberships","code"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,cAAc,EAAEC,sBAAsB,QAAO,4BAA4B;AAC5F,SAAQC,0BAA0B,EAAEC,mBAAmB,QAAO,8BAA8B;AAC5F,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,CAAC,QAAO,MAAM;AACtB,SACEC,uBAAuB,EACvBC,yBAAyB,EACzBC,sBAAsB,EACtBC,sBAAsB,QACjB,kBAAkB;AACzB,SAAQC,yBAAyB,QAAO,iBAAiB;AACzD,SAAQC,eAAe,QAAO,6BAA6B;AAE3D,OAAO,MAAMC,wBAAwBR,YAAY;IAC/CS,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMlB;IACNmB,QAAQ;QACNC,QAAQb,EAAEc,MAAM,CAAC;YAACC,aAAaf,EAAEgB,MAAM,GAAGC,IAAI;QAAE;QAChDC,MAAMtB;QACNuB,UAAU;YACR,KAAKtB;QACP;IACF;IACAuB,cAAc,CAACC;QACb,IAAIA,iBAAiBjB,wBAAwB;YAC3C,MAAM,IAAIN,YAAY,uBAAuB,aAAa;gBAACwB,QAAQ;YAAG;QACxE;QACA,IAAID,iBAAiBpB,yBAAyB;YAC5C,MAAM,IAAIH,YAAY,kCAAkC,aAAa;gBAACwB,QAAQ;YAAG;QACnF;QACA,IAAID,iBAAiBnB,2BAA2B;YAC9C,MAAM,IAAIJ,YAAYuB,MAAME,OAAO,EAAE,0BAA0B;gBAACD,QAAQ;YAAG;QAC7E;QACA,IAAID,iBAAiBlB,wBAAwB;YAC3C,MAAM,IAAIL,YAAY,2BAA2B,sBAAsB;gBAACwB,QAAQ;YAAG;QACrF;QACA,MAAMD;IACR;IACAG,SAAS,OAAOC,SAASC;QACvB,MAAMC,SAASjC,eAAe+B;QAC9B,IAAI,CAACE,QAAQ;YACX,MAAM,IAAI7B,YAAY,2BAA2B,gBAAgB;gBAACwB,QAAQ;YAAG;QAC/E;QAEA,MAAM,EAACP,WAAW,EAAC,GAAGU,QAAQZ,MAAM;QACpClB,uBAAuB;YAAC8B;YAASV;QAAW;QAC5C,MAAM,EAACa,KAAK,EAAC,GAAGH,QAAQP,IAAI;QAC5B,MAAMW,aAAa,MAAMxB,0BAA0B;YACjDU;YACAa;YACAE,iBAAiBH,OAAOI,MAAM;YAC9BC,kBAAkBL,OAAOM,IAAI,IAAI;YACjCC,sBAAsBP,OAAOQ,WAAW;QAC1C;QAEAT,MAAMU,IAAI,CAAC;QACX,OAAO9B,gBAAgBuB;IACzB;AACF,GAAG"}
1
+ {"version":3,"sources":["../../../../src/presentation/routes/invitations/create.ts"],"sourcesContent":["import {\n AUTH_USER,\n getUserContext,\n rejectImpersonatedSession,\n requireWorkspaceAccess,\n} from '@shipfox/api-auth-context';\nimport {createInvitationBodySchema, invitationDtoSchema} from '@shipfox/api-workspaces-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {z} from 'zod';\nimport {\n MembershipRequiredError,\n OpenInvitationExistsError,\n WorkspaceInactiveError,\n WorkspaceNotFoundError,\n} from '#core/errors.js';\nimport {createWorkspaceInvitation} from '#core/index.js';\nimport {toInvitationDto} from '#presentation/dto/index.js';\n\nexport const createInvitationRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Invite someone to join a workspace by email.',\n auth: AUTH_USER,\n schema: {\n params: z.object({workspaceId: z.string().uuid()}),\n body: createInvitationBodySchema,\n response: {\n 201: invitationDtoSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof WorkspaceNotFoundError) {\n throw new ClientError('Workspace not found', 'not-found', {status: 404});\n }\n if (error instanceof MembershipRequiredError) {\n throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});\n }\n if (error instanceof OpenInvitationExistsError) {\n throw new ClientError(error.message, 'open-invitation-exists', {status: 409});\n }\n if (error instanceof WorkspaceInactiveError) {\n throw new ClientError('Workspace is not active', 'workspace-inactive', {status: 403});\n }\n throw error;\n },\n handler: async (request, reply) => {\n const client = getUserContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const {workspaceId} = request.params;\n rejectImpersonatedSession(request);\n requireWorkspaceAccess({request, workspaceId});\n const {email} = request.body;\n const invitation = await createWorkspaceInvitation({\n workspaceId,\n email,\n invitedByUserId: client.userId,\n invitedByDisplay: client.name ?? 'A workspace admin',\n invitedByMemberships: client.memberships,\n });\n\n reply.code(201);\n return toInvitationDto(invitation);\n },\n});\n"],"names":["AUTH_USER","getUserContext","rejectImpersonatedSession","requireWorkspaceAccess","createInvitationBodySchema","invitationDtoSchema","ClientError","defineRoute","z","MembershipRequiredError","OpenInvitationExistsError","WorkspaceInactiveError","WorkspaceNotFoundError","createWorkspaceInvitation","toInvitationDto","createInvitationRoute","method","path","description","auth","schema","params","object","workspaceId","string","uuid","body","response","errorHandler","error","status","message","handler","request","reply","client","email","invitation","invitedByUserId","userId","invitedByDisplay","name","invitedByMemberships","memberships","code"],"mappings":"AAAA,SACEA,SAAS,EACTC,cAAc,EACdC,yBAAyB,EACzBC,sBAAsB,QACjB,4BAA4B;AACnC,SAAQC,0BAA0B,EAAEC,mBAAmB,QAAO,8BAA8B;AAC5F,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,CAAC,QAAO,MAAM;AACtB,SACEC,uBAAuB,EACvBC,yBAAyB,EACzBC,sBAAsB,EACtBC,sBAAsB,QACjB,kBAAkB;AACzB,SAAQC,yBAAyB,QAAO,iBAAiB;AACzD,SAAQC,eAAe,QAAO,6BAA6B;AAE3D,OAAO,MAAMC,wBAAwBR,YAAY;IAC/CS,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMnB;IACNoB,QAAQ;QACNC,QAAQb,EAAEc,MAAM,CAAC;YAACC,aAAaf,EAAEgB,MAAM,GAAGC,IAAI;QAAE;QAChDC,MAAMtB;QACNuB,UAAU;YACR,KAAKtB;QACP;IACF;IACAuB,cAAc,CAACC;QACb,IAAIA,iBAAiBjB,wBAAwB;YAC3C,MAAM,IAAIN,YAAY,uBAAuB,aAAa;gBAACwB,QAAQ;YAAG;QACxE;QACA,IAAID,iBAAiBpB,yBAAyB;YAC5C,MAAM,IAAIH,YAAY,kCAAkC,aAAa;gBAACwB,QAAQ;YAAG;QACnF;QACA,IAAID,iBAAiBnB,2BAA2B;YAC9C,MAAM,IAAIJ,YAAYuB,MAAME,OAAO,EAAE,0BAA0B;gBAACD,QAAQ;YAAG;QAC7E;QACA,IAAID,iBAAiBlB,wBAAwB;YAC3C,MAAM,IAAIL,YAAY,2BAA2B,sBAAsB;gBAACwB,QAAQ;YAAG;QACrF;QACA,MAAMD;IACR;IACAG,SAAS,OAAOC,SAASC;QACvB,MAAMC,SAASlC,eAAegC;QAC9B,IAAI,CAACE,QAAQ;YACX,MAAM,IAAI7B,YAAY,2BAA2B,gBAAgB;gBAACwB,QAAQ;YAAG;QAC/E;QAEA,MAAM,EAACP,WAAW,EAAC,GAAGU,QAAQZ,MAAM;QACpCnB,0BAA0B+B;QAC1B9B,uBAAuB;YAAC8B;YAASV;QAAW;QAC5C,MAAM,EAACa,KAAK,EAAC,GAAGH,QAAQP,IAAI;QAC5B,MAAMW,aAAa,MAAMxB,0BAA0B;YACjDU;YACAa;YACAE,iBAAiBH,OAAOI,MAAM;YAC9BC,kBAAkBL,OAAOM,IAAI,IAAI;YACjCC,sBAAsBP,OAAOQ,WAAW;QAC1C;QAEAT,MAAMU,IAAI,CAAC;QACX,OAAO9B,gBAAgBuB;IACzB;AACF,GAAG"}