@shipfox/api-auth 17.0.0 → 19.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +44 -0
- package/README.md +24 -9
- package/dist/core/administration.d.ts +15 -0
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +44 -2
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +5 -3
- package/dist/core/auth.js.map +1 -1
- package/dist/core/entities/agent-access.d.ts +75 -0
- package/dist/core/entities/agent-access.d.ts.map +1 -0
- package/dist/core/entities/agent-access.js +3 -0
- package/dist/core/entities/agent-access.js.map +1 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/runner-session-token.d.ts.map +1 -1
- package/dist/core/runner-session-token.js +2 -1
- package/dist/core/runner-session-token.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts.map +1 -1
- package/dist/db/admin-user-moderation.js +30 -26
- package/dist/db/admin-user-moderation.js.map +1 -1
- package/dist/db/admin-user-summary.d.ts +22 -0
- package/dist/db/admin-user-summary.d.ts.map +1 -1
- package/dist/db/admin-user-summary.js +75 -1
- package/dist/db/admin-user-summary.js.map +1 -1
- package/dist/db/admin-users.d.ts +11 -0
- package/dist/db/admin-users.d.ts.map +1 -1
- package/dist/db/admin-users.js +4 -1
- package/dist/db/admin-users.js.map +1 -1
- package/dist/db/agent-access.d.ts +159 -0
- package/dist/db/agent-access.d.ts.map +1 -0
- package/dist/db/agent-access.js +267 -0
- package/dist/db/agent-access.js.map +1 -0
- package/dist/db/db.d.ts +2604 -238
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +7 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/agent-access.d.ts +1204 -0
- package/dist/db/schema/agent-access.d.ts.map +1 -0
- package/dist/db/schema/agent-access.js +267 -0
- package/dist/db/schema/agent-access.js.map +1 -0
- package/dist/db/schema/users.d.ts.map +1 -1
- package/dist/db/schema/users.js +8 -3
- package/dist/db/schema/users.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +123 -20
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.js +6 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +10 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
- package/dist/presentation/routes/registration/signup.js +59 -50
- package/dist/presentation/routes/registration/signup.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0003_brainy_luckman.sql +6 -0
- package/drizzle/0004_yummy_runaways.sql +110 -0
- package/drizzle/meta/0003_snapshot.json +844 -0
- package/drizzle/meta/0004_snapshot.json +1751 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +7 -7
- package/src/core/admin-role.test.ts +70 -0
- package/src/core/administration.ts +78 -1
- package/src/core/auth.ts +3 -5
- package/src/core/entities/agent-access.ts +80 -0
- package/src/core/errors.ts +7 -0
- package/src/core/runner-session-token.test.ts +2 -0
- package/src/core/runner-session-token.ts +1 -0
- package/src/db/admin-user-moderation.ts +53 -45
- package/src/db/admin-user-summary.test.ts +228 -0
- package/src/db/admin-user-summary.ts +143 -1
- package/src/db/admin-users.ts +21 -1
- package/src/db/agent-access.test.ts +370 -0
- package/src/db/agent-access.ts +629 -0
- package/src/db/db.ts +14 -0
- package/src/db/schema/agent-access.ts +273 -0
- package/src/db/schema/users.ts +9 -3
- package/src/index.ts +6 -0
- package/src/metrics/instance.ts +1 -0
- package/src/presentation/routes/administration.test.ts +359 -2
- package/src/presentation/routes/administration.ts +142 -20
- package/src/presentation/routes/email-verification/verify-email-resend.ts +7 -1
- package/src/presentation/routes/rate-limit.ts +4 -0
- package/src/presentation/routes/registration/signup.ts +52 -41
- package/test/globalSetup.ts +1 -1
- package/test/routes.ts +3 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
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
|
+
import { adminBootstrapStateSchema, administratorUserDirectoryQuerySchema, administratorUserDirectoryResponseSchema, administratorUserLookupQuerySchema, administratorUserMutationResponseSchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, impersonateResponseSchema, impersonateUserBodySchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, reactivateAdministratorUserBodySchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema, revokeAdministratorUserSessionsBodySchema, revokeAdministratorUserSessionsResponseSchema, suspendAdministratorUserBodySchema } from '@shipfox/api-auth-dto';
|
|
4
4
|
import { decodeTimestampIdCursor, encodeTimestampIdCursor } from '@shipfox/node-drizzle';
|
|
5
5
|
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { requireAdminRole } from '#core/admin-role.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';
|
|
8
|
+
import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, impersonateUser, listAdministratorGrantSummaries, listAdministratorUsers, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
|
|
9
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, EmailNotVerifiedError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, InvalidAdministratorUserDirectoryFilterError, InvalidCredentialsError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
10
10
|
import { getClientContext } from '#presentation/auth/jwt-auth.js';
|
|
11
11
|
import { toUserDto } from '#presentation/dto/user.js';
|
|
12
12
|
import { createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler } from './rate-limit.js';
|
|
@@ -72,7 +72,50 @@ function toAdministratorUserMutationDto(result) {
|
|
|
72
72
|
correlation_id: result.correlationId
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
function directoryResultCountBucket(count) {
|
|
76
|
+
if (count === 0) return '0';
|
|
77
|
+
if (count <= 10) return '1-10';
|
|
78
|
+
if (count <= 50) return '11-50';
|
|
79
|
+
return '51-100';
|
|
80
|
+
}
|
|
81
|
+
function logAdministratorUserDirectoryRead(params) {
|
|
82
|
+
try {
|
|
83
|
+
params.request.log.info({
|
|
84
|
+
actorId: params.actorId,
|
|
85
|
+
requiredRole: 'admin-observer',
|
|
86
|
+
targetType: 'user-directory',
|
|
87
|
+
requestId: params.request.id,
|
|
88
|
+
result: params.outcome,
|
|
89
|
+
outcome: params.outcome,
|
|
90
|
+
durationMs: params.durationMs,
|
|
91
|
+
resultCountBucket: directoryResultCountBucket(params.resultCount),
|
|
92
|
+
filterPresence: params.filterPresence,
|
|
93
|
+
nextPagePresent: params.nextPagePresent
|
|
94
|
+
}, 'Listed administrator user directory');
|
|
95
|
+
} catch {
|
|
96
|
+
// Logging must not change the directory read outcome.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function translateImpersonationEligibilityError(error) {
|
|
100
|
+
// The mint primitive re-checks login eligibility on the row it reads, and a
|
|
101
|
+
// concurrent suspension or unverification between the in-transaction ladder
|
|
102
|
+
// read and that read surfaces these errors. Map them to the same documented
|
|
103
|
+
// client error instead of a generic 500.
|
|
104
|
+
if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
|
|
105
|
+
return new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
|
|
106
|
+
status: 403
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (error instanceof ImpersonationExpiredError) {
|
|
110
|
+
return new ClientError('Impersonation session has expired', 'impersonation-expired', {
|
|
111
|
+
status: 410
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
75
116
|
function translateAdministrationError(error) {
|
|
117
|
+
const impersonationEligibilityError = translateImpersonationEligibilityError(error);
|
|
118
|
+
if (impersonationEligibilityError) throw impersonationEligibilityError;
|
|
76
119
|
if (error instanceof AdminRoleRequiredError) {
|
|
77
120
|
throw new ClientError('Administrator role required', 'forbidden', {
|
|
78
121
|
status: 403,
|
|
@@ -81,6 +124,11 @@ function translateAdministrationError(error) {
|
|
|
81
124
|
}
|
|
82
125
|
});
|
|
83
126
|
}
|
|
127
|
+
if (error instanceof InvalidAdministratorUserDirectoryFilterError) {
|
|
128
|
+
throw new ClientError(error.message, 'validation-error', {
|
|
129
|
+
status: 400
|
|
130
|
+
});
|
|
131
|
+
}
|
|
84
132
|
if (error instanceof InvalidAdminBootstrapTokenError) {
|
|
85
133
|
throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {
|
|
86
134
|
status: 403
|
|
@@ -136,20 +184,6 @@ function translateAdministrationError(error) {
|
|
|
136
184
|
status: 403
|
|
137
185
|
});
|
|
138
186
|
}
|
|
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
|
-
}
|
|
153
187
|
throw error;
|
|
154
188
|
}
|
|
155
189
|
const bootstrapRoute = defineRoute({
|
|
@@ -242,17 +276,85 @@ const userLookupRoute = defineRoute({
|
|
|
242
276
|
const { id, user_id: userId, email } = request.query;
|
|
243
277
|
const lookupId = id ?? userId;
|
|
244
278
|
const actorId = requireActorId(request);
|
|
245
|
-
|
|
279
|
+
let user;
|
|
280
|
+
if (lookupId) user = await findAdministratorUserSummary({
|
|
246
281
|
actorId,
|
|
247
282
|
id: lookupId
|
|
248
|
-
})
|
|
283
|
+
});
|
|
284
|
+
else if (email) user = await findAdministratorUserSummary({
|
|
249
285
|
actorId,
|
|
250
286
|
email
|
|
251
|
-
})
|
|
287
|
+
});
|
|
252
288
|
if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');
|
|
253
289
|
return toAdministratorUserSummaryDto(user);
|
|
254
290
|
}
|
|
255
291
|
});
|
|
292
|
+
const userDirectoryRoute = defineRoute({
|
|
293
|
+
method: 'GET',
|
|
294
|
+
path: '/directory',
|
|
295
|
+
description: 'List bounded administrator-safe user summaries for directory browsing.',
|
|
296
|
+
schema: {
|
|
297
|
+
querystring: administratorUserDirectoryQuerySchema,
|
|
298
|
+
response: {
|
|
299
|
+
200: administratorUserDirectoryResponseSchema
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
preHandler: createAuthActorRateLimitPreHandler('directory'),
|
|
303
|
+
errorHandler: translateAdministrationError,
|
|
304
|
+
handler: async (request)=>{
|
|
305
|
+
const actorId = requireActorId(request);
|
|
306
|
+
const startedAt = performance.now();
|
|
307
|
+
const { search, status, impersonation_eligible: impersonationEligible, limit, cursor } = request.query;
|
|
308
|
+
let resultCount = 0;
|
|
309
|
+
let nextPagePresent = false;
|
|
310
|
+
let outcome = 'failed';
|
|
311
|
+
try {
|
|
312
|
+
const decodedCursor = decodeTimestampIdCursor(cursor);
|
|
313
|
+
if (cursor && !decodedCursor) {
|
|
314
|
+
throw new ClientError('Invalid cursor', 'invalid-cursor', {
|
|
315
|
+
status: 400
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const result = await listAdministratorUsers({
|
|
319
|
+
actorId,
|
|
320
|
+
limit,
|
|
321
|
+
...decodedCursor ? {
|
|
322
|
+
cursor: decodedCursor
|
|
323
|
+
} : {},
|
|
324
|
+
...search !== undefined ? {
|
|
325
|
+
search
|
|
326
|
+
} : {},
|
|
327
|
+
...status !== undefined ? {
|
|
328
|
+
status
|
|
329
|
+
} : {},
|
|
330
|
+
...impersonationEligible !== undefined ? {
|
|
331
|
+
eligible: impersonationEligible
|
|
332
|
+
} : {}
|
|
333
|
+
});
|
|
334
|
+
resultCount = result.users.length;
|
|
335
|
+
nextPagePresent = result.nextCursor !== null;
|
|
336
|
+
outcome = 'succeeded';
|
|
337
|
+
return {
|
|
338
|
+
users: result.users.map(toAdministratorUserSummaryDto),
|
|
339
|
+
next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null
|
|
340
|
+
};
|
|
341
|
+
} finally{
|
|
342
|
+
logAdministratorUserDirectoryRead({
|
|
343
|
+
request,
|
|
344
|
+
actorId,
|
|
345
|
+
outcome,
|
|
346
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
347
|
+
resultCount,
|
|
348
|
+
filterPresence: {
|
|
349
|
+
search: search !== undefined,
|
|
350
|
+
status: status !== undefined,
|
|
351
|
+
impersonationEligible: impersonationEligible !== undefined
|
|
352
|
+
},
|
|
353
|
+
nextPagePresent
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
256
358
|
const suspendUserRoute = defineRoute({
|
|
257
359
|
method: 'POST',
|
|
258
360
|
path: '/:userId/suspend',
|
|
@@ -465,6 +567,7 @@ export function createAdministrationUserRoutes(workspaces) {
|
|
|
465
567
|
prefix: '/admin/auth/users',
|
|
466
568
|
auth: AUTH_USER,
|
|
467
569
|
routes: [
|
|
570
|
+
userDirectoryRoute,
|
|
468
571
|
userLookupRoute,
|
|
469
572
|
suspendUserRoute,
|
|
470
573
|
reactivateUserRoute,
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 administratorUserDirectoryQuerySchema,\n administratorUserDirectoryResponseSchema,\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 listAdministratorUsers,\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 InvalidAdministratorUserDirectoryFilterError,\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 directoryResultCountBucket(count: number): '0' | '1-10' | '11-50' | '51-100' {\n if (count === 0) return '0';\n if (count <= 10) return '1-10';\n if (count <= 50) return '11-50';\n return '51-100';\n}\n\nfunction logAdministratorUserDirectoryRead(params: {\n request: FastifyRequest;\n actorId: string;\n outcome: 'succeeded' | 'failed';\n durationMs: number;\n resultCount: number;\n filterPresence: {\n search: boolean;\n status: boolean;\n impersonationEligible: boolean;\n };\n nextPagePresent: boolean;\n}): void {\n try {\n params.request.log.info(\n {\n actorId: params.actorId,\n requiredRole: 'admin-observer',\n targetType: 'user-directory',\n requestId: params.request.id,\n result: params.outcome,\n outcome: params.outcome,\n durationMs: params.durationMs,\n resultCountBucket: directoryResultCountBucket(params.resultCount),\n filterPresence: params.filterPresence,\n nextPagePresent: params.nextPagePresent,\n },\n 'Listed administrator user directory',\n );\n } catch {\n // Logging must not change the directory read outcome.\n }\n}\n\nfunction translateImpersonationEligibilityError(error: unknown): ClientError | undefined {\n // The mint primitive re-checks login eligibility on the row it reads, and a\n // concurrent suspension or unverification between the in-transaction ladder\n // read and that read surfaces these errors. Map them to the same documented\n // client error instead of a generic 500.\n if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {\n return new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n if (error instanceof ImpersonationExpiredError) {\n return new ClientError('Impersonation session has expired', 'impersonation-expired', {\n status: 410,\n });\n }\n return undefined;\n}\n\nfunction translateAdministrationError(error: unknown): never {\n const impersonationEligibilityError = translateImpersonationEligibilityError(error);\n if (impersonationEligibilityError) throw impersonationEligibilityError;\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 InvalidAdministratorUserDirectoryFilterError) {\n throw new ClientError(error.message, 'validation-error', {status: 400});\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 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 let user: Awaited<ReturnType<typeof findAdministratorUserSummary>>;\n if (lookupId) user = await findAdministratorUserSummary({actorId, id: lookupId});\n else if (email) user = await findAdministratorUserSummary({actorId, email});\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst userDirectoryRoute = defineRoute({\n method: 'GET',\n path: '/directory',\n description: 'List bounded administrator-safe user summaries for directory browsing.',\n schema: {\n querystring: administratorUserDirectoryQuerySchema,\n response: {200: administratorUserDirectoryResponseSchema},\n },\n preHandler: createAuthActorRateLimitPreHandler('directory'),\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const actorId = requireActorId(request);\n const startedAt = performance.now();\n const {\n search,\n status,\n impersonation_eligible: impersonationEligible,\n limit,\n cursor,\n } = request.query;\n let resultCount = 0;\n let nextPagePresent = false;\n let outcome: 'succeeded' | 'failed' = 'failed';\n\n try {\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 listAdministratorUsers({\n actorId,\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n ...(search !== undefined ? {search} : {}),\n ...(status !== undefined ? {status} : {}),\n ...(impersonationEligible !== undefined ? {eligible: impersonationEligible} : {}),\n });\n resultCount = result.users.length;\n nextPagePresent = result.nextCursor !== null;\n outcome = 'succeeded';\n\n return {\n users: result.users.map(toAdministratorUserSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n } finally {\n logAdministratorUserDirectoryRead({\n request,\n actorId,\n outcome,\n durationMs: Math.round(performance.now() - startedAt),\n resultCount,\n filterPresence: {\n search: search !== undefined,\n status: status !== undefined,\n impersonationEligible: impersonationEligible !== undefined,\n },\n nextPagePresent,\n });\n }\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: [\n userDirectoryRoute,\n userLookupRoute,\n suspendUserRoute,\n reactivateUserRoute,\n revokeUserSessionsRoute,\n ],\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","administratorUserDirectoryQuerySchema","administratorUserDirectoryResponseSchema","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","listAdministratorUsers","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","CannotImpersonateAdministratorError","CannotImpersonateSelfError","EmailNotVerifiedError","ImpersonationDisabledError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","InvalidAdminBootstrapTokenError","InvalidAdministratorUserDirectoryFilterError","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","directoryResultCountBucket","count","logAdministratorUserDirectoryRead","params","log","info","actorId","requiredRole","targetType","requestId","outcome","durationMs","resultCountBucket","resultCount","filterPresence","nextPagePresent","translateImpersonationEligibilityError","error","undefined","translateAdministrationError","impersonationEligibilityError","details","required_role","message","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","userDirectoryRoute","startedAt","performance","now","search","impersonation_eligible","impersonationEligible","eligible","users","Math","round","suspendUserRoute","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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-email-resend.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify-email-resend.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,sBAAsB,iDAiCjC,CAAC"}
|
|
@@ -3,6 +3,11 @@ import { EmailChallengeError } from '@shipfox/api-email-challenges';
|
|
|
3
3
|
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
4
4
|
import { resendEmailVerification } from '#core/auth.js';
|
|
5
5
|
import { createAuthRateLimitPreHandler } from '#presentation/routes/rate-limit.js';
|
|
6
|
+
function emailChallengeStatus(code) {
|
|
7
|
+
if (code === 'limited') return 429;
|
|
8
|
+
if (code === 'expired') return 410;
|
|
9
|
+
return 400;
|
|
10
|
+
}
|
|
6
11
|
export const verifyEmailResendRoute = defineRoute({
|
|
7
12
|
method: 'POST',
|
|
8
13
|
path: '/verify-email/resend',
|
|
@@ -17,7 +22,7 @@ export const verifyEmailResendRoute = defineRoute({
|
|
|
17
22
|
errorHandler: (error)=>{
|
|
18
23
|
if (error instanceof EmailChallengeError) {
|
|
19
24
|
throw new ClientError(error.message, `email-challenge-${error.code}`, {
|
|
20
|
-
status: error.code
|
|
25
|
+
status: emailChallengeStatus(error.code),
|
|
21
26
|
...error.retryAt ? {
|
|
22
27
|
details: {
|
|
23
28
|
retry_at: error.retryAt.toISOString()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"sourcesContent":["import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';\nimport {EmailChallengeError} from '@shipfox/api-email-challenges';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {resendEmailVerification} from '#core/auth.js';\nimport {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';\n\nexport const verifyEmailResendRoute = defineRoute({\n method: 'POST',\n path: '/verify-email/resend',\n description: 'Send a new email verification code if the account can be verified.',\n schema: {\n body: verifyEmailResendBodySchema,\n response: {\n 200: verifyEmailResendResponseSchema,\n },\n },\n preHandler: createAuthRateLimitPreHandler('email-send'),\n errorHandler: (error) => {\n if (error instanceof EmailChallengeError) {\n throw new ClientError(error.message, `email-challenge-${error.code}`, {\n status: error.code
|
|
1
|
+
{"version":3,"sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"sourcesContent":["import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';\nimport {EmailChallengeError} from '@shipfox/api-email-challenges';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {resendEmailVerification} from '#core/auth.js';\nimport {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';\n\nfunction emailChallengeStatus(code: EmailChallengeError['code']): 400 | 410 | 429 {\n if (code === 'limited') return 429;\n if (code === 'expired') return 410;\n return 400;\n}\n\nexport const verifyEmailResendRoute = defineRoute({\n method: 'POST',\n path: '/verify-email/resend',\n description: 'Send a new email verification code if the account can be verified.',\n schema: {\n body: verifyEmailResendBodySchema,\n response: {\n 200: verifyEmailResendResponseSchema,\n },\n },\n preHandler: createAuthRateLimitPreHandler('email-send'),\n errorHandler: (error) => {\n if (error instanceof EmailChallengeError) {\n throw new ClientError(error.message, `email-challenge-${error.code}`, {\n status: emailChallengeStatus(error.code),\n ...(error.retryAt ? {details: {retry_at: error.retryAt.toISOString()}} : {}),\n });\n }\n throw error;\n },\n handler: async (request, reply) => {\n const {email, challenge_id} = request.body;\n\n const result = await resendEmailVerification({\n email,\n challengeId: challenge_id,\n sourceIp: request.ip,\n });\n\n reply.code(200).send({\n next_resend_available_at: result.nextResendAvailableAt.toISOString(),\n });\n },\n});\n"],"names":["verifyEmailResendBodySchema","verifyEmailResendResponseSchema","EmailChallengeError","ClientError","defineRoute","resendEmailVerification","createAuthRateLimitPreHandler","emailChallengeStatus","code","verifyEmailResendRoute","method","path","description","schema","body","response","preHandler","errorHandler","error","message","status","retryAt","details","retry_at","toISOString","handler","request","reply","email","challenge_id","result","challengeId","sourceIp","ip","send","next_resend_available_at","nextResendAvailableAt"],"mappings":"AAAA,SAAQA,2BAA2B,EAAEC,+BAA+B,QAAO,wBAAwB;AACnG,SAAQC,mBAAmB,QAAO,gCAAgC;AAClE,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,uBAAuB,QAAO,gBAAgB;AACtD,SAAQC,6BAA6B,QAAO,qCAAqC;AAEjF,SAASC,qBAAqBC,IAAiC;IAC7D,IAAIA,SAAS,WAAW,OAAO;IAC/B,IAAIA,SAAS,WAAW,OAAO;IAC/B,OAAO;AACT;AAEA,OAAO,MAAMC,yBAAyBL,YAAY;IAChDM,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAMd;QACNe,UAAU;YACR,KAAKd;QACP;IACF;IACAe,YAAYV,8BAA8B;IAC1CW,cAAc,CAACC;QACb,IAAIA,iBAAiBhB,qBAAqB;YACxC,MAAM,IAAIC,YAAYe,MAAMC,OAAO,EAAE,CAAC,gBAAgB,EAAED,MAAMV,IAAI,EAAE,EAAE;gBACpEY,QAAQb,qBAAqBW,MAAMV,IAAI;gBACvC,GAAIU,MAAMG,OAAO,GAAG;oBAACC,SAAS;wBAACC,UAAUL,MAAMG,OAAO,CAACG,WAAW;oBAAE;gBAAC,IAAI,CAAC,CAAC;YAC7E;QACF;QACA,MAAMN;IACR;IACAO,SAAS,OAAOC,SAASC;QACvB,MAAM,EAACC,KAAK,EAAEC,YAAY,EAAC,GAAGH,QAAQZ,IAAI;QAE1C,MAAMgB,SAAS,MAAMzB,wBAAwB;YAC3CuB;YACAG,aAAaF;YACbG,UAAUN,QAAQO,EAAE;QACtB;QAEAN,MAAMnB,IAAI,CAAC,KAAK0B,IAAI,CAAC;YACnBC,0BAA0BL,OAAOM,qBAAqB,CAACZ,WAAW;QACpE;IACF;AACF,GAAG"}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAuC7B,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"}
|
|
@@ -41,6 +41,16 @@ const policies = {
|
|
|
41
41
|
windowSeconds: 5 * 60
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
+
directory: {
|
|
45
|
+
ip: {
|
|
46
|
+
limit: 60,
|
|
47
|
+
windowSeconds: 5 * 60
|
|
48
|
+
},
|
|
49
|
+
actor: {
|
|
50
|
+
limit: 60,
|
|
51
|
+
windowSeconds: 5 * 60
|
|
52
|
+
}
|
|
53
|
+
},
|
|
44
54
|
impersonate: {
|
|
45
55
|
// Bounds mint and probing attempts: the IP bucket bounds aggregate source
|
|
46
56
|
// traffic, and the actor bucket is a per-actor cap that prevents an actor
|
|
@@ -1 +1 @@
|
|
|
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","
|
|
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 directory: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n actor: {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","directory","actor","impersonate","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":"signup.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/registration/signup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAC;AAW1F,OAAO,EAAmC,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"signup.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/registration/signup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAC;AAW1F,OAAO,EAAmC,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAuDnF,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,2BAA2B,EACvC,YAAY,CAAC,EAAE,YAAY,mDA0D5B"}
|
|
@@ -6,6 +6,64 @@ import { EmailTakenError, InvitationEmailMismatchError, SignupNotAllowedError, T
|
|
|
6
6
|
import { SIGNUP_DENIAL_MESSAGE_MAX_LENGTH } from '#core/ports.js';
|
|
7
7
|
import { setRefreshTokenCookie } from '#presentation/auth/refresh-cookie.js';
|
|
8
8
|
import { toUserDto } from '#presentation/dto/user.js';
|
|
9
|
+
function handleEmailChallengeError(error) {
|
|
10
|
+
if (!(error instanceof EmailChallengeError)) return;
|
|
11
|
+
throw new ClientError(error.message, `email-challenge-${error.code}`, {
|
|
12
|
+
status: error.code === 'limited' ? 429 : 400,
|
|
13
|
+
...error.retryAt ? {
|
|
14
|
+
details: {
|
|
15
|
+
retry_at: error.retryAt.toISOString()
|
|
16
|
+
}
|
|
17
|
+
} : {}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function handleSignupPolicyError(error) {
|
|
21
|
+
if (error instanceof EmailTakenError) {
|
|
22
|
+
throw new ClientError('Email already registered', 'email-taken', {
|
|
23
|
+
status: 409
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (error instanceof SignupNotAllowedError) {
|
|
27
|
+
const message = error.message.slice(0, SIGNUP_DENIAL_MESSAGE_MAX_LENGTH);
|
|
28
|
+
throw new ClientError(message, 'signup-not-allowed', {
|
|
29
|
+
status: 403,
|
|
30
|
+
details: {
|
|
31
|
+
message,
|
|
32
|
+
...error.format ? {
|
|
33
|
+
format: error.format
|
|
34
|
+
} : {}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function handleInvitationError(error) {
|
|
40
|
+
if (error instanceof TokenInvalidError) {
|
|
41
|
+
throw new ClientError('Invitation token is invalid', 'invitation-token-invalid', {
|
|
42
|
+
status: 410
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (error instanceof TokenAlreadyUsedError) {
|
|
46
|
+
throw new ClientError('Invitation has already been accepted', 'invitation-token-used', {
|
|
47
|
+
status: 410
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof TokenExpiredError) {
|
|
51
|
+
throw new ClientError('Invitation has expired', 'invitation-token-expired', {
|
|
52
|
+
status: 410
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (error instanceof InvitationEmailMismatchError) {
|
|
56
|
+
throw new ClientError('Signup email does not match the invitation', 'invitation-email-mismatch', {
|
|
57
|
+
status: 403
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function handleSignupError(error) {
|
|
62
|
+
handleEmailChallengeError(error);
|
|
63
|
+
handleSignupPolicyError(error);
|
|
64
|
+
handleInvitationError(error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
9
67
|
export function createSignupRoute(workspaces, signupPolicy) {
|
|
10
68
|
return defineRoute({
|
|
11
69
|
method: 'POST',
|
|
@@ -17,56 +75,7 @@ export function createSignupRoute(workspaces, signupPolicy) {
|
|
|
17
75
|
201: signupResponseSchema
|
|
18
76
|
}
|
|
19
77
|
},
|
|
20
|
-
errorHandler:
|
|
21
|
-
if (error instanceof EmailChallengeError) {
|
|
22
|
-
throw new ClientError(error.message, `email-challenge-${error.code}`, {
|
|
23
|
-
status: error.code === 'limited' ? 429 : 400,
|
|
24
|
-
...error.retryAt ? {
|
|
25
|
-
details: {
|
|
26
|
-
retry_at: error.retryAt.toISOString()
|
|
27
|
-
}
|
|
28
|
-
} : {}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (error instanceof EmailTakenError) {
|
|
32
|
-
throw new ClientError('Email already registered', 'email-taken', {
|
|
33
|
-
status: 409
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
if (error instanceof SignupNotAllowedError) {
|
|
37
|
-
const message = error.message.slice(0, SIGNUP_DENIAL_MESSAGE_MAX_LENGTH);
|
|
38
|
-
throw new ClientError(message, 'signup-not-allowed', {
|
|
39
|
-
status: 403,
|
|
40
|
-
details: {
|
|
41
|
-
message,
|
|
42
|
-
...error.format ? {
|
|
43
|
-
format: error.format
|
|
44
|
-
} : {}
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (error instanceof TokenInvalidError) {
|
|
49
|
-
throw new ClientError('Invitation token is invalid', 'invitation-token-invalid', {
|
|
50
|
-
status: 410
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (error instanceof TokenAlreadyUsedError) {
|
|
54
|
-
throw new ClientError('Invitation has already been accepted', 'invitation-token-used', {
|
|
55
|
-
status: 410
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
if (error instanceof TokenExpiredError) {
|
|
59
|
-
throw new ClientError('Invitation has expired', 'invitation-token-expired', {
|
|
60
|
-
status: 410
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
if (error instanceof InvitationEmailMismatchError) {
|
|
64
|
-
throw new ClientError('Signup email does not match the invitation', 'invitation-email-mismatch', {
|
|
65
|
-
status: 403
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
throw error;
|
|
69
|
-
},
|
|
78
|
+
errorHandler: handleSignupError,
|
|
70
79
|
handler: async (request, reply)=>{
|
|
71
80
|
const { email, password, name, invitation_token } = request.body;
|
|
72
81
|
if (invitation_token === undefined) {
|