@shipfox/api-auth 10.0.0 → 10.1.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 (37) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +5 -1
  4. package/dist/core/administration.d.ts +1 -0
  5. package/dist/core/administration.d.ts.map +1 -1
  6. package/dist/core/administration.js +4 -1
  7. package/dist/core/administration.js.map +1 -1
  8. package/dist/db/admin-grants.d.ts +9 -0
  9. package/dist/db/admin-grants.d.ts.map +1 -1
  10. package/dist/db/admin-grants.js +19 -10
  11. package/dist/db/admin-grants.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/metrics/instance.d.ts +1 -1
  16. package/dist/metrics/instance.d.ts.map +1 -1
  17. package/dist/metrics/instance.js.map +1 -1
  18. package/dist/presentation/routes/administration.d.ts +1 -0
  19. package/dist/presentation/routes/administration.d.ts.map +1 -1
  20. package/dist/presentation/routes/administration.js +24 -2
  21. package/dist/presentation/routes/administration.js.map +1 -1
  22. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  23. package/dist/presentation/routes/rate-limit.js +6 -0
  24. package/dist/presentation/routes/rate-limit.js.map +1 -1
  25. package/dist/tsconfig.test.tsbuildinfo +1 -1
  26. package/package.json +3 -3
  27. package/src/core/administration.ts +5 -0
  28. package/src/db/admin-grants.test.ts +17 -1
  29. package/src/db/admin-grants.ts +33 -35
  30. package/src/index.test.ts +4 -1
  31. package/src/index.ts +2 -0
  32. package/src/metrics/instance.ts +6 -1
  33. package/src/presentation/routes/administration.test.ts +128 -0
  34. package/src/presentation/routes/administration.ts +18 -0
  35. package/src/presentation/routes/rate-limit.ts +3 -0
  36. package/test/routes.ts +7 -1
  37. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n administratorUserLookupQuerySchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\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 grantAdministratorRole,\n listAdministratorGrantSummaries,\n revokeAdministratorGrant,\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 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 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 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 administrationUserRoutes: RouteGroup = {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute],\n};\n"],"names":["AUTH_USER","administratorUserLookupQuerySchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","grantAdministratorRole","listAdministratorGrantSummaries","revokeAdministratorGrant","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","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","correlationId","code","listRoute","querystring","limit","cursor","query","decodedCursor","result","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","grantRoute","reason","revokeRoute","params","object","string","uuid","administrationRoutes","prefix","auth","routes","administrationUserRoutes"],"mappings":"AAAA,SAAQA,SAAS,QAAO,4BAA4B;AACpD,SACEC,kCAAkC,EAClCC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,iCAAiC,EACjCC,wBAAwB,EACxBC,4BAA4B,EAC5BC,0BAA0B,EAC1BC,6BAA6B,EAC7BC,0BAA0B,EAC1BC,8BAA8B,QACzB,wBAAwB;AAC/B,SAAQC,uBAAuB,EAAEC,uBAAuB,QAAO,wBAAwB;AACvF,SAAQC,WAAW,EAAEC,WAAW,QAAwB,wBAAwB;AAEhF,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,gBAAgB,QAAO,sBAAsB;AACrD,SACEC,wBAAwB,EACxBC,4BAA4B,EAC5BC,sBAAsB,EACtBC,+BAA+B,EAC/BC,wBAAwB,QACnB,0BAA0B;AAMjC,SACEC,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,sBAAsB,EACtBC,+BAA+B,EAC/BC,mBAAmB,EACnBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,+BAA+B,QAAO,kBAAkB;AAEhE,MAAMC,0BAA0B;AAEhC,SAASC,eAAeC,OAAuB;IAC7C,MAAMC,SAASL,iBAAiBI;IAChC,IAAI,CAACC,QAAQ;QACX,MAAM,IAAItB,YAAY,2BAA2B,gBAAgB;YAACuB,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,IAAInB,YAAY,sCAAsC,4BAA4B;YACtFuB,QAAQ;QACV;IACF;IACA,OAAOK;AACT;AAEA,eAAeK,6BAA6BZ,OAAuB;IACjE,MAAMlB,iBAAiB;QAACqB,QAAQJ,eAAeC;QAAUa,aAAa;IAAgB;AACxF;AAEA,SAASC,gBAAgBC,KAAiB;IACxC,OAAO;QACLC,IAAID,MAAMC,EAAE;QACZC,SAASF,MAAMZ,MAAM;QACrBe,MAAMH,MAAMG,IAAI;QAChBC,YAAYJ,MAAMK,SAAS,EAAEC,iBAAiB;QAC9CC,YAAYP,MAAMQ,SAAS,CAACF,WAAW;QACvCG,YAAYT,MAAMU,SAAS,CAACJ,WAAW;IACzC;AACF;AAEA,SAASK,8BAA8BC,IAA8B;IACnE,OAAO;QACLX,IAAIW,KAAKX,EAAE;QACXY,OAAOD,KAAKC,KAAK;QACjBC,MAAMF,KAAKE,IAAI;QACf3B,QAAQyB,KAAKzB,MAAM;QACnB4B,mBAAmBH,KAAKI,eAAe,EAAEV,iBAAiB;QAC1DC,YAAYK,KAAKJ,SAAS,CAACF,WAAW;QACtCW,YAAYL,KAAKM,SAAS;IAC5B;AACF;AAEA,SAASC,+BAA+BnB,KAAgC;IACtE,OAAO;QACLoB,UAAUpB,MAAMqB,OAAO;QACvBlB,MAAMH,MAAMG,IAAI;QAChBI,YAAYP,MAAMQ,SAAS,CAACF,WAAW;QACvCF,YAAYJ,MAAMK,SAAS,EAAEC,iBAAiB;QAC9CM,MAAMZ,MAAMY,IAAI;IAClB;AACF;AAEA,SAASU,6BAA6BC,KAAc;IAClD,IAAIA,iBAAiB9C,wBAAwB;QAC3C,MAAM,IAAIb,YAAY,+BAA+B,aAAa;YAChEuB,QAAQ;YACRqC,SAAS;gBAACC,eAAeF,MAAMzB,WAAW;YAAA;QAC5C;IACF;IACA,IAAIyB,iBAAiB7C,iCAAiC;QACpD,MAAM,IAAId,YAAY,8BAA8B,2BAA2B;YAC7EuB,QAAQ;QACV;IACF;IACA,IAAIoC,iBAAiBlD,2BAA2B;QAC9C,MAAM,IAAIT,YAAY,4CAA4C,oBAAoB;YACpFuB,QAAQ;QACV;IACF;IACA,IAAIoC,iBAAiBjD,8BAA8B;QACjD,MAAM,IAAIV,YAAY,sCAAsC,wBAAwB;YAClFuB,QAAQ;QACV;IACF;IACA,IAAIoC,iBAAiBhD,yBAAyB;QAC5C,MAAM,IAAIX,YAAY,iCAAiC,aAAa;YAACuB,QAAQ;QAAG;IAClF;IACA,IAAIoC,iBAAiB3C,mBAAmB;QACtC,MAAM,IAAIhB,YAAY,kBAAkB,aAAa;YAACuB,QAAQ;QAAG;IACnE;IACA,IAAIoC,iBAAiB5C,qBAAqB;QACxC,MAAM,IAAIf,YAAY,sDAAsD,cAAc;YACxFuB,QAAQ;QACV;IACF;IACA,IAAIoC,iBAAiB/C,+BAA+B;QAClD,MAAM,IAAIZ,YACR,4DACA,0BACA;YAACuB,QAAQ;QAAG;IAEhB;IACA,MAAMoC;AACR;AAEA,MAAMG,iBAAiB7D,YAAY;IACjC8D,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAM7E;QACN8E,UAAU;YAAC,KAAK7E;QAAiC;IACnD;IACA8E,YAAYnD,gCAAgC;IAC5CoD,cAAcZ;IACda,SAAS,OAAOlD,SAASmD;QACvB,MAAMC,UAAUrD,eAAeC;QAC/B,MAAMe,QAAQ,MAAMhC,yBAAyB;YAC3CqE;YACAC,gBAAgBrD,QAAQ8C,IAAI,CAACQ,eAAe;YAC5CC,gBAAgBnD,sBAAsBJ;YACtCwD,eAAexD,QAAQgB,EAAE;QAC3B;QACAmC,MAAMM,IAAI,CAAC;QACX,OAAO3C,gBAAgBC;IACzB;AACF;AAEA,MAAM2C,YAAY9E,YAAY;IAC5B8D,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNc,aAAatF;QACb0E,UAAU;YAAC,KAAKzE;QAA6B;IAC/C;IACA2E,cAAcZ;IACda,SAAS,OAAOlD;QACd,MAAM,EAAC4D,KAAK,EAAEC,MAAM,EAAC,GAAG7D,QAAQ8D,KAAK;QACrC,MAAMC,gBAAgBtF,wBAAwBoF;QAC9C,IAAIA,UAAU,CAACE,eAAe;YAC5B,MAAM,IAAIpF,YAAY,kBAAkB,kBAAkB;gBAACuB,QAAQ;YAAG;QACxE;QAEA,MAAM8D,SAAS,MAAM9E,gCAAgC;YACnDkE,SAASrD,eAAeC;YACxB4D;YACA,GAAIG,gBAAgB;gBAACF,QAAQE;YAAa,IAAI,CAAC,CAAC;QAClD;QACA,OAAO;YACLE,QAAQD,OAAOC,MAAM,CAACC,GAAG,CAAChC;YAC1BiC,aAAaH,OAAOI,UAAU,GAAG1F,wBAAwBsF,OAAOI,UAAU,IAAI;QAChF;IACF;AACF;AAEA,MAAMC,kBAAkBzF,YAAY;IAClC8D,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNc,aAAa5F;QACbgF,UAAU;YAAC,KAAK/E;QAA8B;IAChD;IACAgF,YAAY;QAACpC;QAA8Bf,gCAAgC;KAAU;IACrFoD,cAAcZ;IACda,SAAS,OAAOlD;QACd,MAAM,EAACgB,EAAE,EAAEC,SAASd,MAAM,EAAEyB,KAAK,EAAC,GAAG5B,QAAQ8D,KAAK;QAClD,MAAMQ,WAAWtD,MAAMb;QACvB,MAAMiD,UAAUrD,eAAeC;QAC/B,MAAM2B,OAAO2C,WACT,MAAMtF,6BAA6B;YAACoE;YAASpC,IAAIsD;QAAQ,KACzD1C,QACE,MAAM5C,6BAA6B;YAACoE;YAASxB;QAAK,KAClD2C;QACN,IAAI,CAAC5C,MAAM,MAAM,IAAIhC,kBAAkB2E,YAAY1C,SAAS;QAC5D,OAAOF,8BAA8BC;IACvC;AACF;AAEA,MAAM6C,aAAa5F,YAAY;IAC7B8D,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAM3E;QACN4E,UAAU;YAAC,KAAK3E;QAA4B;IAC9C;IACA6E,cAAcZ;IACda,SAAS,OAAOlD,SAASmD;QACvB,MAAMC,UAAUrD,eAAeC;QAC/B,MAAMe,QAAQ,MAAM9B,uBAAuB;YACzCmE;YACAjD,QAAQH,QAAQ8C,IAAI,CAAC7B,OAAO;YAC5BC,MAAMlB,QAAQ8C,IAAI,CAAC5B,IAAI;YACvBuD,QAAQzE,QAAQ8C,IAAI,CAAC2B,MAAM;YAC3BlB,gBAAgBnD,sBAAsBJ;YACtCwD,eAAexD,QAAQgB,EAAE;QAC3B;QACAmC,MAAMM,IAAI,CAAC;QACX,OAAO3C,gBAAgBC;IACzB;AACF;AAEA,MAAM2D,cAAc9F,YAAY;IAC9B8D,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACN8B,QAAQ9F,EAAE+F,MAAM,CAAC;YAACxC,SAASvD,EAAEgG,MAAM,GAAGC,IAAI;QAAE;QAC5ChC,MAAMvE;QACNwE,UAAU;YAAC,KAAKvE;QAA8B;IAChD;IACAyE,cAAcZ;IACda,SAAS,OAAOlD;QACd,MAAMe,QAAQ,MAAM5B,yBAAyB;YAC3CiE,SAASrD,eAAeC;YACxBoC,SAASpC,QAAQ2E,MAAM,CAACvC,OAAO;YAC/BqC,QAAQzE,QAAQ8C,IAAI,CAAC2B,MAAM;YAC3BlB,gBAAgBnD,sBAAsBJ;YACtCwD,eAAexD,QAAQgB,EAAE;QAC3B;QACA,OAAOF,gBAAgBC;IACzB;AACF;AAEA,OAAO,MAAMgE,uBAAmC;IAC9CC,QAAQ;IACRC,MAAMnH;IACNoH,QAAQ;QAACzC;QAAgBiB;QAAWc;QAAYE;KAAY;AAC9D,EAAE;AAEF,OAAO,MAAMS,2BAAuC;IAClDH,QAAQ;IACRC,MAAMnH;IACNoH,QAAQ;QAACb;KAAgB;AAC3B,EAAE"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\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 revokeAdministratorGrant,\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 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 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],\n};\n"],"names":["AUTH_USER","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","listAdministratorGrantSummaries","revokeAdministratorGrant","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","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","correlationId","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","result","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","grantRoute","reason","revokeRoute","params","object","string","uuid","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","administrationUserRoutes"],"mappings":"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"}
@@ -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;AAC1F,OAAO,EACL,KAAK,mBAAmB,EAMzB,MAAM,qBAAqB,CAAC;AAsB7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgFD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,IACzD,SAAS,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,IAC3D,SAAS,cAAc,EAAE,OAAO,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":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EACL,KAAK,mBAAmB,EAMzB,MAAM,qBAAqB,CAAC;AAyB7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgFD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,IACzD,SAAS,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,IAC3D,SAAS,cAAc,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E"}
@@ -27,6 +27,12 @@ const policies = {
27
27
  windowSeconds: 15 * 60
28
28
  }
29
29
  },
30
+ 'bootstrap-state': {
31
+ ip: {
32
+ limit: 60,
33
+ windowSeconds: 5 * 60
34
+ }
35
+ },
30
36
  lookup: {
31
37
  ip: {
32
38
  limit: 60,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {\n type AuthRateLimitAction,\n AuthRateLimitExceededError,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n AuthRateLimitUnavailableError,\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 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 try {\n await checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n });\n } catch (error) {\n if (error instanceof AuthRateLimitExceededError) {\n params.request.log.warn(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n retryAfterSeconds: error.retryAfterSeconds,\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n 'Auth rate limit blocked request',\n );\n params.reply.header('Retry-After', String(error.retryAfterSeconds));\n throw new ClientError('Rate limit exceeded', 'rate-limited', {\n status: 429,\n details: {retry_after_seconds: error.retryAfterSeconds},\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n });\n }\n\n if (error instanceof AuthRateLimitUnavailableError) {\n params.request.log.error(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n err: error,\n },\n 'Auth rate limiter unavailable',\n );\n throw new ClientError(\n 'Authentication rate limiter unavailable',\n 'auth-rate-limit-unavailable',\n {\n status: 503,\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n },\n );\n }\n\n throw error;\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","AuthRateLimitExceededError","AuthRateLimitUnavailableError","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","routeName","request","routeOptions","url","split","enforceRateLimit","params","policy","action","scope","identifier","error","log","warn","route","retryAfterSeconds","identifierHmacPrefix","reply","header","String","status","details","retry_after_seconds","data","cause","err","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {\n type AuthRateLimitAction,\n AuthRateLimitExceededError,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n AuthRateLimitUnavailableError,\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 try {\n await checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n });\n } catch (error) {\n if (error instanceof AuthRateLimitExceededError) {\n params.request.log.warn(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n retryAfterSeconds: error.retryAfterSeconds,\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n 'Auth rate limit blocked request',\n );\n params.reply.header('Retry-After', String(error.retryAfterSeconds));\n throw new ClientError('Rate limit exceeded', 'rate-limited', {\n status: 429,\n details: {retry_after_seconds: error.retryAfterSeconds},\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n });\n }\n\n if (error instanceof AuthRateLimitUnavailableError) {\n params.request.log.error(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n err: error,\n },\n 'Auth rate limiter unavailable',\n );\n throw new ClientError(\n 'Authentication rate limiter unavailable',\n 'auth-rate-limit-unavailable',\n {\n status: 503,\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n },\n );\n }\n\n throw error;\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","AuthRateLimitExceededError","AuthRateLimitUnavailableError","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","routeName","request","routeOptions","url","split","enforceRateLimit","params","policy","action","scope","identifier","error","log","warn","route","retryAfterSeconds","identifierHmacPrefix","reply","header","String","status","details","retry_after_seconds","data","cause","err","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler"],"mappings":"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"}