@shipfox/api-auth 13.1.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.
Files changed (101) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +41 -0
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +4 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/administration.d.ts +24 -2
  9. package/dist/core/administration.d.ts.map +1 -1
  10. package/dist/core/administration.js +126 -3
  11. package/dist/core/administration.js.map +1 -1
  12. package/dist/core/auth.d.ts +31 -0
  13. package/dist/core/auth.d.ts.map +1 -1
  14. package/dist/core/auth.js +70 -2
  15. package/dist/core/auth.js.map +1 -1
  16. package/dist/core/errors.d.ts +15 -0
  17. package/dist/core/errors.d.ts.map +1 -1
  18. package/dist/core/errors.js +30 -0
  19. package/dist/core/errors.js.map +1 -1
  20. package/dist/core/jwt.d.ts +2 -0
  21. package/dist/core/jwt.d.ts.map +1 -1
  22. package/dist/core/jwt.js +22 -1
  23. package/dist/core/jwt.js.map +1 -1
  24. package/dist/db/admin-command.d.ts +12 -4
  25. package/dist/db/admin-command.d.ts.map +1 -1
  26. package/dist/db/admin-command.js +10 -1
  27. package/dist/db/admin-command.js.map +1 -1
  28. package/dist/db/admin-grants.d.ts.map +1 -1
  29. package/dist/db/admin-grants.js +4 -1
  30. package/dist/db/admin-grants.js.map +1 -1
  31. package/dist/db/admin-user-moderation.d.ts +3 -0
  32. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  33. package/dist/db/admin-user-moderation.js +6 -2
  34. package/dist/db/admin-user-moderation.js.map +1 -1
  35. package/dist/db/impersonation.d.ts +55 -0
  36. package/dist/db/impersonation.d.ts.map +1 -0
  37. package/dist/db/impersonation.js +212 -0
  38. package/dist/db/impersonation.js.map +1 -0
  39. package/dist/db/schema/admin-command-results.d.ts +16 -0
  40. package/dist/db/schema/admin-command-results.d.ts.map +1 -1
  41. package/dist/db/schema/admin-command-results.js.map +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +5 -5
  45. package/dist/index.js.map +1 -1
  46. package/dist/metrics/index.d.ts +1 -1
  47. package/dist/metrics/index.d.ts.map +1 -1
  48. package/dist/metrics/index.js +1 -1
  49. package/dist/metrics/index.js.map +1 -1
  50. package/dist/metrics/instance.d.ts +4 -2
  51. package/dist/metrics/instance.d.ts.map +1 -1
  52. package/dist/metrics/instance.js +8 -0
  53. package/dist/metrics/instance.js.map +1 -1
  54. package/dist/presentation/auth/bearer-token-auth.d.ts +2 -2
  55. package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
  56. package/dist/presentation/auth/bearer-token-auth.js +2 -2
  57. package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
  58. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  59. package/dist/presentation/auth/jwt-auth.js +26 -2
  60. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  61. package/dist/presentation/routes/administration.d.ts +2 -1
  62. package/dist/presentation/routes/administration.d.ts.map +1 -1
  63. package/dist/presentation/routes/administration.js +127 -19
  64. package/dist/presentation/routes/administration.js.map +1 -1
  65. package/dist/presentation/routes/rate-limit.d.ts +8 -0
  66. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  67. package/dist/presentation/routes/rate-limit.js +44 -0
  68. package/dist/presentation/routes/rate-limit.js.map +1 -1
  69. package/dist/presentation/routes/session/me.d.ts.map +1 -1
  70. package/dist/presentation/routes/session/me.js +2 -1
  71. package/dist/presentation/routes/session/me.js.map +1 -1
  72. package/dist/tsconfig.test.tsbuildinfo +1 -1
  73. package/package.json +12 -12
  74. package/src/config.ts +4 -0
  75. package/src/core/administration.ts +172 -1
  76. package/src/core/auth.test.ts +79 -0
  77. package/src/core/auth.ts +98 -1
  78. package/src/core/errors.ts +35 -0
  79. package/src/core/jwt.test.ts +92 -0
  80. package/src/core/jwt.ts +38 -9
  81. package/src/db/admin-command.ts +35 -6
  82. package/src/db/admin-grants.ts +5 -1
  83. package/src/db/admin-user-moderation.ts +8 -2
  84. package/src/db/impersonation.ts +323 -0
  85. package/src/db/schema/admin-command-results.ts +18 -0
  86. package/src/index.test.ts +1 -1
  87. package/src/index.ts +12 -3
  88. package/src/metrics/index.ts +2 -0
  89. package/src/metrics/instance.ts +13 -2
  90. package/src/presentation/auth/bearer-token-auth.ts +4 -4
  91. package/src/presentation/auth/jwt-auth.test.ts +191 -0
  92. package/src/presentation/auth/jwt-auth.ts +19 -1
  93. package/src/presentation/routes/administration.test.ts +828 -3
  94. package/src/presentation/routes/administration.ts +130 -11
  95. package/src/presentation/routes/rate-limit.ts +40 -0
  96. package/src/presentation/routes/session/login.test.ts +5 -0
  97. package/src/presentation/routes/session/me.test.ts +24 -0
  98. package/src/presentation/routes/session/me.ts +1 -0
  99. package/src/presentation/routes/session/refresh.test.ts +11 -0
  100. package/test/routes.ts +43 -16
  101. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,13 +1,15 @@
1
- import { AUTH_USER } from '@shipfox/api-auth-context';
2
- import { adminBootstrapStateSchema, administratorUserLookupQuerySchema, administratorUserMutationResponseSchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, reactivateAdministratorUserBodySchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema, revokeAdministratorUserSessionsBodySchema, revokeAdministratorUserSessionsResponseSchema, suspendAdministratorUserBodySchema } from '@shipfox/api-auth-dto';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { AUTH_USER, adoptAdministrationActorGuard, requireAdministrationActor } from '@shipfox/api-auth-context';
3
+ import { adminBootstrapStateSchema, administratorUserLookupQuerySchema, administratorUserMutationResponseSchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, impersonateResponseSchema, impersonateUserBodySchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, reactivateAdministratorUserBodySchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema, revokeAdministratorUserSessionsBodySchema, revokeAdministratorUserSessionsResponseSchema, suspendAdministratorUserBodySchema } from '@shipfox/api-auth-dto';
3
4
  import { decodeTimestampIdCursor, encodeTimestampIdCursor } from '@shipfox/node-drizzle';
4
5
  import { ClientError, defineRoute } from '@shipfox/node-fastify';
5
6
  import { z } from 'zod';
6
7
  import { requireAdminRole } from '#core/admin-role.js';
7
- import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, listAdministratorGrantSummaries, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
8
- import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
8
+ import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, impersonateUser, listAdministratorGrantSummaries, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
9
+ import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, EmailNotVerifiedError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
9
10
  import { getClientContext } from '#presentation/auth/jwt-auth.js';
10
- import { createAuthIpRateLimitPreHandler } from './rate-limit.js';
11
+ import { toUserDto } from '#presentation/dto/user.js';
12
+ import { createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler } from './rate-limit.js';
11
13
  const idempotencyKeyMaxLength = 256;
12
14
  function requireActorId(request) {
13
15
  const client = getClientContext(request);
@@ -114,6 +116,40 @@ function translateAdministrationError(error) {
114
116
  status: 409
115
117
  });
116
118
  }
119
+ if (error instanceof ImpersonationDisabledError) {
120
+ throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {
121
+ status: 403
122
+ });
123
+ }
124
+ if (error instanceof CannotImpersonateSelfError) {
125
+ throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {
126
+ status: 403
127
+ });
128
+ }
129
+ if (error instanceof CannotImpersonateAdministratorError) {
130
+ throw new ClientError('Cannot impersonate an administrator', 'cannot-impersonate-administrator', {
131
+ status: 403
132
+ });
133
+ }
134
+ if (error instanceof ImpersonationTargetNotActiveError) {
135
+ throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
136
+ status: 403
137
+ });
138
+ }
139
+ // The mint primitive re-checks login eligibility (active, verified) on the
140
+ // row it reads, and a concurrent suspension or unverification between the
141
+ // in-transaction ladder read and that read surfaces these errors. Map them
142
+ // to the same documented client error instead of a generic 500.
143
+ if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
144
+ throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
145
+ status: 403
146
+ });
147
+ }
148
+ if (error instanceof ImpersonationExpiredError) {
149
+ throw new ClientError('Impersonation session has expired', 'impersonation-expired', {
150
+ status: 410
151
+ });
152
+ }
117
153
  throw error;
118
154
  }
119
155
  const bootstrapRoute = defineRoute({
@@ -349,7 +385,7 @@ const revokeRoute = defineRoute({
349
385
  return toAdminGrantDto(grant);
350
386
  }
351
387
  });
352
- export const administrationRoutes = {
388
+ export const administrationRoutes = adoptAdministrationActorGuard({
353
389
  prefix: '/admin/auth/admin-grants',
354
390
  auth: AUTH_USER,
355
391
  routes: [
@@ -358,23 +394,95 @@ export const administrationRoutes = {
358
394
  grantRoute,
359
395
  revokeRoute
360
396
  ]
361
- };
362
- export const administrationBootstrapRoutes = {
397
+ });
398
+ export const administrationBootstrapRoutes = adoptAdministrationActorGuard({
363
399
  prefix: '/admin/auth',
364
400
  auth: AUTH_USER,
365
401
  routes: [
366
402
  bootstrapStateRoute
367
403
  ]
368
- };
369
- export const administrationUserRoutes = {
370
- prefix: '/admin/auth/users',
371
- auth: AUTH_USER,
372
- routes: [
373
- userLookupRoute,
374
- suspendUserRoute,
375
- reactivateUserRoute,
376
- revokeUserSessionsRoute
377
- ]
378
- };
404
+ });
405
+ function createImpersonateUserRoute(workspaces) {
406
+ return defineRoute({
407
+ method: 'POST',
408
+ path: '/:userId/impersonate',
409
+ description: 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',
410
+ schema: {
411
+ params: z.object({
412
+ userId: z.string().uuid()
413
+ }),
414
+ body: impersonateUserBodySchema,
415
+ response: {
416
+ 200: impersonateResponseSchema
417
+ }
418
+ },
419
+ // The limiter runs ahead of the impersonated-session rejection: a request
420
+ // carrying an already-issued impersonated token must consume the
421
+ // `impersonate` IP and actor buckets instead of being rejected by the
422
+ // guard first, or marked-session probes would bypass the limiter entirely.
423
+ // The guard still runs before the authorization inside the command.
424
+ preHandler: [
425
+ createAuthActorRateLimitPreHandler('impersonate'),
426
+ (request)=>{
427
+ requireAdministrationActor(request);
428
+ return undefined;
429
+ }
430
+ ],
431
+ errorHandler: translateAdministrationError,
432
+ handler: async (request)=>{
433
+ const client = getClientContext(request);
434
+ const result = await impersonateUser({
435
+ actorId: requireActorId(request),
436
+ ...client?.impersonatorId ? {
437
+ actorImpersonatorId: client.impersonatorId
438
+ } : {},
439
+ targetUserId: request.params.userId,
440
+ reason: request.body.reason,
441
+ idempotencyKey: requireIdempotencyKey(request),
442
+ // Fastify's default request IDs are process-local counters that reset
443
+ // after a redeploy, so a replay could collide with an earlier mint's
444
+ // ID and suppress the required failure event in the ambiguous-COMMIT
445
+ // reconciliation. The correlation is a fresh process-independent UUID
446
+ // per invocation.
447
+ correlationId: randomUUID(),
448
+ workspaces
449
+ });
450
+ // `server_time` is the issuer's clock at response time: the banner
451
+ // derives its countdown and Extend availability from this anchor.
452
+ return {
453
+ token: result.token,
454
+ expires_at: result.expiresAt.toISOString(),
455
+ server_time: new Date().toISOString(),
456
+ impersonator_id: result.impersonatorId,
457
+ user: toUserDto(result.user)
458
+ };
459
+ }
460
+ });
461
+ }
462
+ export function createAdministrationUserRoutes(workspaces) {
463
+ return [
464
+ adoptAdministrationActorGuard({
465
+ prefix: '/admin/auth/users',
466
+ auth: AUTH_USER,
467
+ routes: [
468
+ userLookupRoute,
469
+ suspendUserRoute,
470
+ reactivateUserRoute,
471
+ revokeUserSessionsRoute
472
+ ]
473
+ }),
474
+ // The impersonate route mounts outside the adopted guard on purpose: its
475
+ // limiter must run before the impersonated-session rejection (see
476
+ // `createImpersonateUserRoute`), so the guard is applied positionally
477
+ // there. Every other route under `/admin/auth/users` keeps the adoption.
478
+ {
479
+ prefix: '/admin/auth/users',
480
+ auth: AUTH_USER,
481
+ routes: [
482
+ createImpersonateUserRoute(workspaces)
483
+ ]
484
+ }
485
+ ];
486
+ }
379
487
 
380
488
  //# sourceMappingURL=administration.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserMutationResponseSchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n reactivateAdministratorUserBodySchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n revokeAdministratorUserSessionsBodySchema,\n revokeAdministratorUserSessionsResponseSchema,\n suspendAdministratorUserBodySchema,\n} from '@shipfox/api-auth-dto';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n listAdministratorGrantSummaries,\n reactivateAdministratorUser,\n revokeAdministratorGrant,\n revokeAdministratorUserSessions,\n suspendAdministratorUser,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(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\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction toAdministratorUserMutationDto(result: {\n user: AdministratorUserSummary;\n correlationId: string;\n}) {\n return {\n ...toAdministratorUserSummaryDto(result.user),\n correlation_id: result.correlationId,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst suspendUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/suspend',\n description: 'Suspend a user and revoke all of its active sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: suspendAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await suspendAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst reactivateUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/reactivate',\n description: 'Reactivate a suspended user without restoring revoked sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: reactivateAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await reactivateAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst revokeUserSessionsRoute = defineRoute({\n method: 'POST',\n path: '/:userId/revoke-sessions',\n description: 'Revoke all active sessions for a user without changing account status.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: revokeAdministratorUserSessionsBodySchema,\n response: {200: revokeAdministratorUserSessionsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await revokeAdministratorUserSessions({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return {\n ...toAdministratorUserMutationDto(result),\n sessions_revoked: result.sessionsRevoked,\n };\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = {\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n};\n\nexport const administrationBootstrapRoutes: RouteGroup = {\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n};\n\nexport const administrationUserRoutes: RouteGroup = {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],\n};\n"],"names":["AUTH_USER","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserMutationResponseSchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","reactivateAdministratorUserBodySchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","revokeAdministratorUserSessionsBodySchema","revokeAdministratorUserSessionsResponseSchema","suspendAdministratorUserBodySchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","listAdministratorGrantSummaries","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","getClientContext","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","toAdministratorUserMutationDto","result","correlation_id","correlationId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","suspendUserRoute","params","object","string","uuid","reason","reactivateUserRoute","revokeUserSessionsRoute","sessions_revoked","sessionsRevoked","grantRoute","revokeRoute","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","administrationUserRoutes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {\n AUTH_USER,\n adoptAdministrationActorGuard,\n requireAdministrationActor,\n} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserMutationResponseSchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n impersonateResponseSchema,\n impersonateUserBodySchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n reactivateAdministratorUserBodySchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n revokeAdministratorUserSessionsBodySchema,\n revokeAdministratorUserSessionsResponseSchema,\n suspendAdministratorUserBodySchema,\n} from '@shipfox/api-auth-dto';\nimport type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n impersonateUser,\n listAdministratorGrantSummaries,\n reactivateAdministratorUser,\n revokeAdministratorGrant,\n revokeAdministratorUserSessions,\n suspendAdministratorUser,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n EmailNotVerifiedError,\n ImpersonationDisabledError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n InvalidAdminBootstrapTokenError,\n InvalidCredentialsError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\nimport {createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(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\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction toAdministratorUserMutationDto(result: {\n user: AdministratorUserSummary;\n correlationId: string;\n}) {\n return {\n ...toAdministratorUserSummaryDto(result.user),\n correlation_id: result.correlationId,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n if (error instanceof ImpersonationDisabledError) {\n throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {status: 403});\n }\n if (error instanceof CannotImpersonateSelfError) {\n throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {\n status: 403,\n });\n }\n if (error instanceof CannotImpersonateAdministratorError) {\n throw new ClientError(\n 'Cannot impersonate an administrator',\n 'cannot-impersonate-administrator',\n {status: 403},\n );\n }\n if (error instanceof ImpersonationTargetNotActiveError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n // The mint primitive re-checks login eligibility (active, verified) on the\n // row it reads, and a concurrent suspension or unverification between the\n // in-transaction ladder read and that read surfaces these errors. Map them\n // to the same documented client error instead of a generic 500.\n if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n if (error instanceof ImpersonationExpiredError) {\n throw new ClientError('Impersonation session has expired', 'impersonation-expired', {\n status: 410,\n });\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst suspendUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/suspend',\n description: 'Suspend a user and revoke all of its active sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: suspendAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await suspendAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst reactivateUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/reactivate',\n description: 'Reactivate a suspended user without restoring revoked sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: reactivateAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await reactivateAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst revokeUserSessionsRoute = defineRoute({\n method: 'POST',\n path: '/:userId/revoke-sessions',\n description: 'Revoke all active sessions for a user without changing account status.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: revokeAdministratorUserSessionsBodySchema,\n response: {200: revokeAdministratorUserSessionsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await revokeAdministratorUserSessions({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return {\n ...toAdministratorUserMutationDto(result),\n sessions_revoked: result.sessionsRevoked,\n };\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n});\n\nexport const administrationBootstrapRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n});\n\nfunction createImpersonateUserRoute(workspaces: WorkspacesInterModuleClient) {\n return defineRoute({\n method: 'POST',\n path: '/:userId/impersonate',\n description:\n 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: impersonateUserBodySchema,\n response: {200: impersonateResponseSchema},\n },\n // The limiter runs ahead of the impersonated-session rejection: a request\n // carrying an already-issued impersonated token must consume the\n // `impersonate` IP and actor buckets instead of being rejected by the\n // guard first, or marked-session probes would bypass the limiter entirely.\n // The guard still runs before the authorization inside the command.\n preHandler: [\n createAuthActorRateLimitPreHandler('impersonate'),\n (request) => {\n requireAdministrationActor(request);\n return undefined;\n },\n ],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const client = getClientContext(request);\n const result = await impersonateUser({\n actorId: requireActorId(request),\n ...(client?.impersonatorId ? {actorImpersonatorId: client.impersonatorId} : {}),\n targetUserId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n // Fastify's default request IDs are process-local counters that reset\n // after a redeploy, so a replay could collide with an earlier mint's\n // ID and suppress the required failure event in the ambiguous-COMMIT\n // reconciliation. The correlation is a fresh process-independent UUID\n // per invocation.\n correlationId: randomUUID(),\n workspaces,\n });\n // `server_time` is the issuer's clock at response time: the banner\n // derives its countdown and Extend availability from this anchor.\n return {\n token: result.token,\n expires_at: result.expiresAt.toISOString(),\n server_time: new Date().toISOString(),\n impersonator_id: result.impersonatorId,\n user: toUserDto(result.user),\n };\n },\n });\n}\n\nexport function createAdministrationUserRoutes(\n workspaces: WorkspacesInterModuleClient,\n): RouteGroup[] {\n return [\n adoptAdministrationActorGuard({\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],\n }),\n // The impersonate route mounts outside the adopted guard on purpose: its\n // limiter must run before the impersonated-session rejection (see\n // `createImpersonateUserRoute`), so the guard is applied positionally\n // there. Every other route under `/admin/auth/users` keeps the adoption.\n {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [createImpersonateUserRoute(workspaces)],\n },\n ];\n}\n"],"names":["randomUUID","AUTH_USER","adoptAdministrationActorGuard","requireAdministrationActor","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserMutationResponseSchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","impersonateResponseSchema","impersonateUserBodySchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","reactivateAdministratorUserBodySchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","revokeAdministratorUserSessionsBodySchema","revokeAdministratorUserSessionsResponseSchema","suspendAdministratorUserBodySchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","impersonateUser","listAdministratorGrantSummaries","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","CannotImpersonateAdministratorError","CannotImpersonateSelfError","EmailNotVerifiedError","ImpersonationDisabledError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","InvalidAdminBootstrapTokenError","InvalidCredentialsError","LastAdminOwnerError","UserNotFoundError","getClientContext","toUserDto","createAuthActorRateLimitPreHandler","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","toAdministratorUserMutationDto","result","correlation_id","correlationId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","suspendUserRoute","params","object","string","uuid","reason","reactivateUserRoute","revokeUserSessionsRoute","sessions_revoked","sessionsRevoked","grantRoute","revokeRoute","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","createImpersonateUserRoute","workspaces","impersonatorId","actorImpersonatorId","targetUserId","token","expires_at","expiresAt","server_time","Date","impersonator_id","createAdministrationUserRoutes"],"mappings":"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"}
@@ -7,5 +7,13 @@ export declare function createAuthRateLimitPreHandler(action: AuthRateLimitActio
7
7
  Body: EmailBody;
8
8
  }>, reply: FastifyReply) => Promise<void>;
9
9
  export declare function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
10
+ /**
11
+ * Rate limits an authenticated route by source IP and by actor. Every request
12
+ * consumes the IP bucket, which bounds aggregate source traffic; the actor
13
+ * bucket is an additional per-actor cap that prevents an actor from evading
14
+ * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`
15
+ * hook), so the user context is set.
16
+ */
17
+ export declare function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
10
18
  export {};
11
19
  //# sourceMappingURL=rate-limit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAyB7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,aAChD,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,aAClD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E"}
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAmC7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,aAChD,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,aAClD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,mBAAmB,aACrD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAmB3E"}
@@ -1,3 +1,4 @@
1
+ import { getUserContext } from '@shipfox/api-auth-context';
1
2
  import { ClientError } from '@shipfox/node-fastify';
2
3
  import { enforceRateLimit as enforceSharedRateLimit } from '@shipfox/node-rate-limit';
3
4
  import { checkAuthRateLimit } from '#core/rate-limit.js';
@@ -39,6 +40,22 @@ const policies = {
39
40
  limit: 60,
40
41
  windowSeconds: 5 * 60
41
42
  }
43
+ },
44
+ impersonate: {
45
+ // Bounds mint and probing attempts: the IP bucket bounds aggregate source
46
+ // traffic, and the actor bucket is a per-actor cap that prevents an actor
47
+ // from evading the bound by rotating IPs (every request still consumes the
48
+ // IP bucket first, so a shared NAT cannot exhaust it faster by pooling
49
+ // actors). Ladder denials by an identifiable actor role are audited as
50
+ // `failed` events.
51
+ ip: {
52
+ limit: 20,
53
+ windowSeconds: 15 * 60
54
+ },
55
+ actor: {
56
+ limit: 20,
57
+ windowSeconds: 15 * 60
58
+ }
42
59
  }
43
60
  };
44
61
  function routeName(request) {
@@ -107,5 +124,32 @@ export function createAuthIpRateLimitPreHandler(action) {
107
124
  });
108
125
  };
109
126
  }
127
+ /**
128
+ * Rate limits an authenticated route by source IP and by actor. Every request
129
+ * consumes the IP bucket, which bounds aggregate source traffic; the actor
130
+ * bucket is an additional per-actor cap that prevents an actor from evading
131
+ * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`
132
+ * hook), so the user context is set.
133
+ */ export function createAuthActorRateLimitPreHandler(action) {
134
+ return async (request, reply)=>{
135
+ await enforceRateLimit({
136
+ request,
137
+ reply,
138
+ action,
139
+ scope: 'ip',
140
+ identifier: request.ip
141
+ });
142
+ const client = getUserContext(request);
143
+ if (client) {
144
+ await enforceRateLimit({
145
+ request,
146
+ reply,
147
+ action,
148
+ scope: 'actor',
149
+ identifier: client.userId
150
+ });
151
+ }
152
+ };
153
+ }
110
154
 
111
155
  //# sourceMappingURL=rate-limit.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n"],"names":["ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler"],"mappings":"AAAA,SAAQA,WAAW,QAA+C,wBAAwB;AAC1F,SAAQC,oBAAoBC,sBAAsB,QAAO,2BAA2B;AACpF,SAIEC,kBAAkB,QACb,sBAAsB;AAE7B,MAAMC,WAGF;IACFC,OAAO;QACLC,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;QACrCC,OAAO;YAACF,OAAO;YAAIC,eAAe,KAAK;QAAE;IAC3C;IACA,cAAc;QACZF,IAAI;YAACC,OAAO;YAAIC,eAAe,KAAK;QAAE;QACtCC,OAAO;YAACF,OAAO;YAAGC,eAAe,KAAK;QAAE;IAC1C;IACAE,WAAW;QACTJ,IAAI;YAACC,OAAO;YAAGC,eAAe,KAAK;QAAE;IACvC;IACA,mBAAmB;QACjBF,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;IACvC;IACAG,QAAQ;QACNL,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;IACvC;AACF;AAMA,SAASI,UAAUC,OAAuB;IACxC,OAAOA,QAAQC,YAAY,CAACC,GAAG,IAAIF,QAAQE,GAAG,CAACC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI;AAClE;AAEA,eAAef,iBAAiBgB,MAM/B;IACC,MAAMC,SAASd,QAAQ,CAACa,OAAOE,MAAM,CAAC,CAACF,OAAOG,KAAK,CAAC;IACpD,IAAI,CAACF,QAAQ;IAEb,MAAMhB,uBAAuB;QAC3BW,SAASI,OAAOJ,OAAO;QACvBQ,OAAOJ,OAAOI,KAAK;QACnBC,OAAO,IACLnB,mBAAmB;gBACjBgB,QAAQF,OAAOE,MAAM;gBACrBC,OAAOH,OAAOG,KAAK;gBACnBG,YAAYN,OAAOM,UAAU;gBAC7B,GAAGL,MAAM;YACX;QACFM,OAAOZ,UAAUK,OAAOJ,OAAO;QAC/BY,iBAAiB;QACjBC,oBAAoB;QACpBC,eAAe,CAACN,OAAOO;YACrBP,MAAMQ,MAAM,CAAC,eAAeC,OAAOF;QACrC;QACAG,SAAS,CAAClB,SAASmB;YACjBnB,QAAQoB,GAAG,CAACC,IAAI,CAACF,SAAS;QAC5B;QACAG,UAAU,CAACtB,SAASmB;YAClBnB,QAAQoB,GAAG,CAACG,KAAK,CAACJ,SAAS;QAC7B;QACAK,mBAAmB,CAACC,cAAcC,QAChC,IAAIvC,YAAYsC,aAAaE,OAAO,EAAEF,aAAaG,IAAI,EAAE;gBACvDC,QAAQJ,aAAaI,MAAM;gBAC3B,GAAIJ,aAAaK,OAAO,GAAG;oBAACA,SAASL,aAAaK,OAAO;gBAAA,IAAI,CAAC,CAAC;gBAC/DC,MAAMN,aAAaM,IAAI;gBACvBL;YACF;IACJ;AACF;AAEA,OAAO,SAASM,8BAA8B1B,MAA2B;IACvE,OAAO,OAAON,SAA4CQ;QACxD,MAAMpB,iBAAiB;YACrBY;YACAQ;YACAF;YACAC,OAAO;YACPG,YAAYV,QAAQP,EAAE;QACxB;QAEA,MAAML,iBAAiB;YACrBY;YACAQ;YACAF;YACAC,OAAO;YACPG,YAAYV,QAAQiC,IAAI,CAACrC,KAAK;QAChC;IACF;AACF;AAEA,OAAO,SAASsC,gCAAgC5B,MAA2B;IACzE,OAAO,OAAON,SAAyBQ;QACrC,MAAMpB,iBAAiB;YACrBY;YACAQ;YACAF;YACAC,OAAO;YACPG,YAAYV,QAAQP,EAAE;QACxB;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {getUserContext} from '@shipfox/api-auth-context';\nimport {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n impersonate: {\n // Bounds mint and probing attempts: the IP bucket bounds aggregate source\n // traffic, and the actor bucket is a per-actor cap that prevents an actor\n // from evading the bound by rotating IPs (every request still consumes the\n // IP bucket first, so a shared NAT cannot exhaust it faster by pooling\n // actors). Ladder denials by an identifiable actor role are audited as\n // `failed` events.\n ip: {limit: 20, windowSeconds: 15 * 60},\n actor: {limit: 20, windowSeconds: 15 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n\n/**\n * Rate limits an authenticated route by source IP and by actor. Every request\n * consumes the IP bucket, which bounds aggregate source traffic; the actor\n * bucket is an additional per-actor cap that prevents an actor from evading\n * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`\n * hook), so the user context is set.\n */\nexport function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n const client = getUserContext(request);\n if (client) {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'actor',\n identifier: client.userId,\n });\n }\n };\n}\n"],"names":["getUserContext","ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","impersonate","actor","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler","createAuthActorRateLimitPreHandler","client","userId"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/session/me.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,OAAO,iDA6BlB,CAAC"}
1
+ {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/session/me.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,OAAO,iDA8BlB,CAAC"}
@@ -38,7 +38,8 @@ export const meRoute = defineRoute({
38
38
  user: toUserDto(result.user),
39
39
  admin_role: await getCurrentAdminRole({
40
40
  userId: client.userId
41
- })
41
+ }),
42
+ impersonator_id: client.impersonatorId
42
43
  };
43
44
  }
44
45
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/presentation/routes/session/me.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {meResponseSchema} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {getCurrentAdminRole} from '#core/admin-role.js';\nimport {getCurrentUser} from '#core/auth.js';\nimport {UserNotFoundError} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\n\nexport const meRoute = defineRoute({\n method: 'GET',\n path: '/me',\n description: 'Get the signed-in user.',\n auth: AUTH_USER,\n schema: {\n response: {\n 200: meResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n throw error;\n },\n handler: async (request) => {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const result = await getCurrentUser({userId: client.userId});\n\n return {\n user: toUserDto(result.user),\n admin_role: await getCurrentAdminRole({userId: client.userId}),\n };\n },\n});\n"],"names":["AUTH_USER","meResponseSchema","ClientError","defineRoute","getCurrentAdminRole","getCurrentUser","UserNotFoundError","getClientContext","toUserDto","meRoute","method","path","description","auth","schema","response","errorHandler","error","status","handler","request","client","result","userId","user","admin_role"],"mappings":"AAAA,SAAQA,SAAS,QAAO,4BAA4B;AACpD,SAAQC,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,mBAAmB,QAAO,sBAAsB;AACxD,SAAQC,cAAc,QAAO,gBAAgB;AAC7C,SAAQC,iBAAiB,QAAO,kBAAkB;AAClD,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,SAAS,QAAO,4BAA4B;AAEpD,OAAO,MAAMC,UAAUN,YAAY;IACjCO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMb;IACNc,QAAQ;QACNC,UAAU;YACR,KAAKd;QACP;IACF;IACAe,cAAc,CAACC;QACb,IAAIA,iBAAiBX,mBAAmB;YACtC,MAAM,IAAIJ,YAAY,kBAAkB,aAAa;gBAACgB,QAAQ;YAAG;QACnE;QACA,MAAMD;IACR;IACAE,SAAS,OAAOC;QACd,MAAMC,SAASd,iBAAiBa;QAChC,IAAI,CAACC,QAAQ;YACX,MAAM,IAAInB,YAAY,2BAA2B,gBAAgB;gBAACgB,QAAQ;YAAG;QAC/E;QAEA,MAAMI,SAAS,MAAMjB,eAAe;YAACkB,QAAQF,OAAOE,MAAM;QAAA;QAE1D,OAAO;YACLC,MAAMhB,UAAUc,OAAOE,IAAI;YAC3BC,YAAY,MAAMrB,oBAAoB;gBAACmB,QAAQF,OAAOE,MAAM;YAAA;QAC9D;IACF;AACF,GAAG"}
1
+ {"version":3,"sources":["../../../../src/presentation/routes/session/me.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {meResponseSchema} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {getCurrentAdminRole} from '#core/admin-role.js';\nimport {getCurrentUser} from '#core/auth.js';\nimport {UserNotFoundError} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\n\nexport const meRoute = defineRoute({\n method: 'GET',\n path: '/me',\n description: 'Get the signed-in user.',\n auth: AUTH_USER,\n schema: {\n response: {\n 200: meResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n throw error;\n },\n handler: async (request) => {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n\n const result = await getCurrentUser({userId: client.userId});\n\n return {\n user: toUserDto(result.user),\n admin_role: await getCurrentAdminRole({userId: client.userId}),\n impersonator_id: client.impersonatorId,\n };\n },\n});\n"],"names":["AUTH_USER","meResponseSchema","ClientError","defineRoute","getCurrentAdminRole","getCurrentUser","UserNotFoundError","getClientContext","toUserDto","meRoute","method","path","description","auth","schema","response","errorHandler","error","status","handler","request","client","result","userId","user","admin_role","impersonator_id","impersonatorId"],"mappings":"AAAA,SAAQA,SAAS,QAAO,4BAA4B;AACpD,SAAQC,gBAAgB,QAAO,wBAAwB;AACvD,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,mBAAmB,QAAO,sBAAsB;AACxD,SAAQC,cAAc,QAAO,gBAAgB;AAC7C,SAAQC,iBAAiB,QAAO,kBAAkB;AAClD,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,SAAS,QAAO,4BAA4B;AAEpD,OAAO,MAAMC,UAAUN,YAAY;IACjCO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,MAAMb;IACNc,QAAQ;QACNC,UAAU;YACR,KAAKd;QACP;IACF;IACAe,cAAc,CAACC;QACb,IAAIA,iBAAiBX,mBAAmB;YACtC,MAAM,IAAIJ,YAAY,kBAAkB,aAAa;gBAACgB,QAAQ;YAAG;QACnE;QACA,MAAMD;IACR;IACAE,SAAS,OAAOC;QACd,MAAMC,SAASd,iBAAiBa;QAChC,IAAI,CAACC,QAAQ;YACX,MAAM,IAAInB,YAAY,2BAA2B,gBAAgB;gBAACgB,QAAQ;YAAG;QAC/E;QAEA,MAAMI,SAAS,MAAMjB,eAAe;YAACkB,QAAQF,OAAOE,MAAM;QAAA;QAE1D,OAAO;YACLC,MAAMhB,UAAUc,OAAOE,IAAI;YAC3BC,YAAY,MAAMrB,oBAAoB;gBAACmB,QAAQF,OAAOE,MAAM;YAAA;YAC5DG,iBAAiBL,OAAOM,cAAc;QACxC;IACF;AACF,GAAG"}