@shipfox/api-auth 18.0.0 → 20.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 +17 -2
- package/CHANGELOG.md +47 -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/cimd.d.ts +39 -0
- package/dist/core/cimd.d.ts.map +1 -0
- package/dist/core/cimd.js +392 -0
- package/dist/core/cimd.js.map +1 -0
- 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 +17 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +32 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/oauth-client-resolver.d.ts +54 -0
- package/dist/core/oauth-client-resolver.d.ts.map +1 -0
- package/dist/core/oauth-client-resolver.js +154 -0
- package/dist/core/oauth-client-resolver.js.map +1 -0
- package/dist/core/oauth-client.d.ts +39 -0
- package/dist/core/oauth-client.d.ts.map +1 -0
- package/dist/core/oauth-client.js +236 -0
- package/dist/core/oauth-client.js.map +1 -0
- 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-retention.d.ts +10 -0
- package/dist/db/agent-access-retention.d.ts.map +1 -0
- package/dist/db/agent-access-retention.js +11 -0
- package/dist/db/agent-access-retention.js.map +1 -0
- package/dist/db/agent-access.d.ts +255 -0
- package/dist/db/agent-access.d.ts.map +1 -0
- package/dist/db/agent-access.js +705 -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 +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -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/oauth.d.ts +9 -0
- package/dist/presentation/routes/oauth.d.ts.map +1 -0
- package/dist/presentation/routes/oauth.js +150 -0
- package/dist/presentation/routes/oauth.js.map +1 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +34 -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/temporal/activities/agent-access-retention.d.ts +13 -0
- package/dist/temporal/activities/agent-access-retention.d.ts.map +1 -0
- package/dist/temporal/activities/agent-access-retention.js +58 -0
- package/dist/temporal/activities/agent-access-retention.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +5 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +8 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/constants.d.ts +2 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +3 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/agent-access-retention-cron.d.ts +2 -0
- package/dist/temporal/workflows/agent-access-retention-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/agent-access-retention-cron.js +22 -0
- package/dist/temporal/workflows/agent-access-retention-cron.js.map +1 -0
- package/dist/temporal/workflows/index.bundle.js +25227 -0
- package/dist/temporal/workflows/index.bundle.meta.json +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- 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 +11 -8
- 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/cimd.test.ts +213 -0
- package/src/core/cimd.ts +532 -0
- package/src/core/entities/agent-access.ts +80 -0
- package/src/core/errors.ts +48 -0
- package/src/core/oauth-client-resolver.test.ts +198 -0
- package/src/core/oauth-client-resolver.ts +253 -0
- package/src/core/oauth-client.test.ts +157 -0
- package/src/core/oauth-client.ts +331 -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-retention.ts +10 -0
- package/src/db/agent-access.test.ts +1064 -0
- package/src/db/agent-access.ts +1491 -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.test.ts +27 -0
- package/src/index.ts +82 -0
- package/src/metrics/instance.ts +6 -1
- 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/oauth.test.ts +133 -0
- package/src/presentation/routes/oauth.ts +144 -0
- package/src/presentation/routes/rate-limit.ts +16 -0
- package/src/presentation/routes/registration/signup.ts +52 -41
- package/src/temporal/activities/agent-access-retention.ts +76 -0
- package/src/temporal/activities/index.ts +5 -0
- package/src/temporal/constants.ts +1 -0
- package/src/temporal/workflows/agent-access-retention-cron.ts +23 -0
- package/src/temporal/workflows/index.ts +1 -0
- 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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
2
|
+
export interface CreateOAuthRoutesOptions {
|
|
3
|
+
/** Validated here and deliberately injected until production composition. */
|
|
4
|
+
apiPublicUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createOAuthMetadataRoutes(options: CreateOAuthRoutesOptions): RouteGroup;
|
|
7
|
+
export declare function createOAuthClientIdentificationRoutes(): RouteGroup;
|
|
8
|
+
export declare function createOAuthRoutes(options: CreateOAuthRoutesOptions): RouteGroup;
|
|
9
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/oauth.ts"],"names":[],"mappings":"AAOA,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAWhF,MAAM,WAAW,wBAAwB;IACvC,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;CACtB;AAoGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CASvF;AAED,wBAAgB,qCAAqC,IAAI,UAAU,CAElE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CAO/E"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { oauthAuthorizationServerMetadataSchema, oauthDynamicClientRegistrationRequestSchema, oauthDynamicClientRegistrationResponseSchema, oauthProtectedResourceMetadataSchema } from '@shipfox/api-auth-dto';
|
|
2
|
+
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
3
|
+
import { InvalidOAuthClientMetadataError, InvalidOAuthConfigurationError, OAuthMetadataFetchError, OAuthRedirectUriNotRegisteredError } from '#core/errors.js';
|
|
4
|
+
import { validateOAuthPublicOrigin } from '#core/oauth-client.js';
|
|
5
|
+
import { registerOAuthClient } from '#core/oauth-client-resolver.js';
|
|
6
|
+
import { createAuthIpRateLimitPreHandler } from './rate-limit.js';
|
|
7
|
+
function apiPublicOrigin(options) {
|
|
8
|
+
return validateOAuthPublicOrigin(options.apiPublicUrl);
|
|
9
|
+
}
|
|
10
|
+
function translateOAuthError(error) {
|
|
11
|
+
if (error instanceof InvalidOAuthClientMetadataError || error instanceof OAuthMetadataFetchError || error instanceof OAuthRedirectUriNotRegisteredError) {
|
|
12
|
+
throw new ClientError('OAuth client metadata is invalid', 'invalid-client-metadata', {
|
|
13
|
+
status: 400
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (error instanceof InvalidOAuthConfigurationError) {
|
|
17
|
+
throw new ClientError('OAuth configuration is invalid', 'oauth-configuration-invalid', {
|
|
18
|
+
status: 500
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
function registrationResponse(result) {
|
|
24
|
+
return {
|
|
25
|
+
client_id: result.metadata.clientId,
|
|
26
|
+
client_name: result.metadata.clientName,
|
|
27
|
+
redirect_uris: result.metadata.redirectUris,
|
|
28
|
+
grant_types: result.metadata.grantTypes,
|
|
29
|
+
response_types: result.metadata.responseTypes,
|
|
30
|
+
token_endpoint_auth_method: result.metadata.tokenEndpointAuthMethod,
|
|
31
|
+
scope: result.metadata.scope
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createProtectedResourceMetadataRoute(origin) {
|
|
35
|
+
return defineRoute({
|
|
36
|
+
method: 'GET',
|
|
37
|
+
path: '/.well-known/oauth-protected-resource',
|
|
38
|
+
description: 'Describe the protected MCP resource and its authorization server.',
|
|
39
|
+
schema: {
|
|
40
|
+
response: {
|
|
41
|
+
200: oauthProtectedResourceMetadataSchema
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
handler: ()=>({
|
|
45
|
+
resource: `${origin}/mcp`,
|
|
46
|
+
authorization_servers: [
|
|
47
|
+
origin
|
|
48
|
+
],
|
|
49
|
+
scopes_supported: [
|
|
50
|
+
'read'
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function createAuthorizationServerMetadataRoute(origin) {
|
|
56
|
+
return defineRoute({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
path: '/.well-known/oauth-authorization-server',
|
|
59
|
+
description: 'Describe the MCP OAuth authorization server.',
|
|
60
|
+
schema: {
|
|
61
|
+
response: {
|
|
62
|
+
200: oauthAuthorizationServerMetadataSchema
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
handler: ()=>({
|
|
66
|
+
issuer: origin,
|
|
67
|
+
authorization_endpoint: `${origin}/oauth/authorize`,
|
|
68
|
+
token_endpoint: `${origin}/oauth/token`,
|
|
69
|
+
registration_endpoint: `${origin}/oauth/register`,
|
|
70
|
+
response_types_supported: [
|
|
71
|
+
'code'
|
|
72
|
+
],
|
|
73
|
+
grant_types_supported: [
|
|
74
|
+
'authorization_code',
|
|
75
|
+
'refresh_token'
|
|
76
|
+
],
|
|
77
|
+
code_challenge_methods_supported: [
|
|
78
|
+
'S256'
|
|
79
|
+
],
|
|
80
|
+
token_endpoint_auth_methods_supported: [
|
|
81
|
+
'none'
|
|
82
|
+
],
|
|
83
|
+
scopes_supported: [
|
|
84
|
+
'read'
|
|
85
|
+
],
|
|
86
|
+
client_id_metadata_document_supported: true
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function createDynamicRegistrationRoute() {
|
|
91
|
+
return defineRoute({
|
|
92
|
+
method: 'POST',
|
|
93
|
+
path: '/oauth/register',
|
|
94
|
+
description: 'Register a public OAuth client for MCP access.',
|
|
95
|
+
options: {
|
|
96
|
+
bodyLimit: 32 * 1024
|
|
97
|
+
},
|
|
98
|
+
schema: {
|
|
99
|
+
body: oauthDynamicClientRegistrationRequestSchema,
|
|
100
|
+
response: {
|
|
101
|
+
201: oauthDynamicClientRegistrationResponseSchema
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
preHandler: createAuthIpRateLimitPreHandler('oauth-register'),
|
|
105
|
+
errorHandler: translateOAuthError,
|
|
106
|
+
handler: async (request, reply)=>{
|
|
107
|
+
const result = await registerOAuthClient({
|
|
108
|
+
clientName: request.body.client_name,
|
|
109
|
+
redirectUris: request.body.redirect_uris,
|
|
110
|
+
grantTypes: request.body.grant_types,
|
|
111
|
+
responseTypes: request.body.response_types,
|
|
112
|
+
tokenEndpointAuthMethod: request.body.token_endpoint_auth_method,
|
|
113
|
+
scope: request.body.scope
|
|
114
|
+
});
|
|
115
|
+
reply.code(201);
|
|
116
|
+
return registrationResponse(result);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function createOAuthMetadataRoutes(options) {
|
|
121
|
+
const origin = apiPublicOrigin(options);
|
|
122
|
+
return {
|
|
123
|
+
prefix: '',
|
|
124
|
+
routes: [
|
|
125
|
+
createProtectedResourceMetadataRoute(origin),
|
|
126
|
+
createAuthorizationServerMetadataRoute(origin)
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function createOAuthClientIdentificationRoutes() {
|
|
131
|
+
return {
|
|
132
|
+
prefix: '',
|
|
133
|
+
routes: [
|
|
134
|
+
createDynamicRegistrationRoute()
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function createOAuthRoutes(options) {
|
|
139
|
+
const metadataRoutes = createOAuthMetadataRoutes(options);
|
|
140
|
+
const clientIdentificationRoutes = createOAuthClientIdentificationRoutes();
|
|
141
|
+
return {
|
|
142
|
+
prefix: '',
|
|
143
|
+
routes: [
|
|
144
|
+
...metadataRoutes.routes,
|
|
145
|
+
...clientIdentificationRoutes.routes
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/oauth.ts"],"sourcesContent":["import {\n type OAuthDynamicClientRegistrationResponseDto,\n oauthAuthorizationServerMetadataSchema,\n oauthDynamicClientRegistrationRequestSchema,\n oauthDynamicClientRegistrationResponseSchema,\n oauthProtectedResourceMetadataSchema,\n} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport {\n InvalidOAuthClientMetadataError,\n InvalidOAuthConfigurationError,\n OAuthMetadataFetchError,\n OAuthRedirectUriNotRegisteredError,\n} from '#core/errors.js';\nimport {validateOAuthPublicOrigin} from '#core/oauth-client.js';\nimport {registerOAuthClient} from '#core/oauth-client-resolver.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nexport interface CreateOAuthRoutesOptions {\n /** Validated here and deliberately injected until production composition. */\n apiPublicUrl: string;\n}\n\nfunction apiPublicOrigin(options: CreateOAuthRoutesOptions): string {\n return validateOAuthPublicOrigin(options.apiPublicUrl);\n}\n\nfunction translateOAuthError(error: unknown): never {\n if (\n error instanceof InvalidOAuthClientMetadataError ||\n error instanceof OAuthMetadataFetchError ||\n error instanceof OAuthRedirectUriNotRegisteredError\n ) {\n throw new ClientError('OAuth client metadata is invalid', 'invalid-client-metadata', {\n status: 400,\n });\n }\n if (error instanceof InvalidOAuthConfigurationError) {\n throw new ClientError('OAuth configuration is invalid', 'oauth-configuration-invalid', {\n status: 500,\n });\n }\n throw error;\n}\n\nfunction registrationResponse(\n result: Awaited<ReturnType<typeof registerOAuthClient>>,\n): OAuthDynamicClientRegistrationResponseDto {\n return {\n client_id: result.metadata.clientId,\n client_name: result.metadata.clientName,\n redirect_uris: result.metadata.redirectUris,\n grant_types: result.metadata.grantTypes,\n response_types: result.metadata.responseTypes,\n token_endpoint_auth_method: result.metadata.tokenEndpointAuthMethod,\n scope: result.metadata.scope,\n };\n}\n\nfunction createProtectedResourceMetadataRoute(origin: string) {\n return defineRoute({\n method: 'GET',\n path: '/.well-known/oauth-protected-resource',\n description: 'Describe the protected MCP resource and its authorization server.',\n schema: {response: {200: oauthProtectedResourceMetadataSchema}},\n handler: () => ({\n resource: `${origin}/mcp`,\n authorization_servers: [origin],\n scopes_supported: ['read'],\n }),\n });\n}\n\nfunction createAuthorizationServerMetadataRoute(origin: string) {\n return defineRoute({\n method: 'GET',\n path: '/.well-known/oauth-authorization-server',\n description: 'Describe the MCP OAuth authorization server.',\n schema: {response: {200: oauthAuthorizationServerMetadataSchema}},\n handler: () => ({\n issuer: origin,\n authorization_endpoint: `${origin}/oauth/authorize`,\n token_endpoint: `${origin}/oauth/token`,\n registration_endpoint: `${origin}/oauth/register`,\n response_types_supported: ['code'],\n grant_types_supported: ['authorization_code', 'refresh_token'],\n code_challenge_methods_supported: ['S256'],\n token_endpoint_auth_methods_supported: ['none'],\n scopes_supported: ['read'],\n client_id_metadata_document_supported: true,\n }),\n });\n}\n\nfunction createDynamicRegistrationRoute() {\n return defineRoute({\n method: 'POST',\n path: '/oauth/register',\n description: 'Register a public OAuth client for MCP access.',\n options: {bodyLimit: 32 * 1024},\n schema: {\n body: oauthDynamicClientRegistrationRequestSchema,\n response: {201: oauthDynamicClientRegistrationResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('oauth-register'),\n errorHandler: translateOAuthError,\n handler: async (request, reply) => {\n const result = await registerOAuthClient({\n clientName: request.body.client_name,\n redirectUris: request.body.redirect_uris,\n grantTypes: request.body.grant_types,\n responseTypes: request.body.response_types,\n tokenEndpointAuthMethod: request.body.token_endpoint_auth_method,\n scope: request.body.scope,\n });\n reply.code(201);\n return registrationResponse(result);\n },\n });\n}\n\nexport function createOAuthMetadataRoutes(options: CreateOAuthRoutesOptions): RouteGroup {\n const origin = apiPublicOrigin(options);\n return {\n prefix: '',\n routes: [\n createProtectedResourceMetadataRoute(origin),\n createAuthorizationServerMetadataRoute(origin),\n ],\n };\n}\n\nexport function createOAuthClientIdentificationRoutes(): RouteGroup {\n return {prefix: '', routes: [createDynamicRegistrationRoute()]};\n}\n\nexport function createOAuthRoutes(options: CreateOAuthRoutesOptions): RouteGroup {\n const metadataRoutes = createOAuthMetadataRoutes(options);\n const clientIdentificationRoutes = createOAuthClientIdentificationRoutes();\n return {\n prefix: '',\n routes: [...metadataRoutes.routes, ...clientIdentificationRoutes.routes],\n };\n}\n"],"names":["oauthAuthorizationServerMetadataSchema","oauthDynamicClientRegistrationRequestSchema","oauthDynamicClientRegistrationResponseSchema","oauthProtectedResourceMetadataSchema","ClientError","defineRoute","InvalidOAuthClientMetadataError","InvalidOAuthConfigurationError","OAuthMetadataFetchError","OAuthRedirectUriNotRegisteredError","validateOAuthPublicOrigin","registerOAuthClient","createAuthIpRateLimitPreHandler","apiPublicOrigin","options","apiPublicUrl","translateOAuthError","error","status","registrationResponse","result","client_id","metadata","clientId","client_name","clientName","redirect_uris","redirectUris","grant_types","grantTypes","response_types","responseTypes","token_endpoint_auth_method","tokenEndpointAuthMethod","scope","createProtectedResourceMetadataRoute","origin","method","path","description","schema","response","handler","resource","authorization_servers","scopes_supported","createAuthorizationServerMetadataRoute","issuer","authorization_endpoint","token_endpoint","registration_endpoint","response_types_supported","grant_types_supported","code_challenge_methods_supported","token_endpoint_auth_methods_supported","client_id_metadata_document_supported","createDynamicRegistrationRoute","bodyLimit","body","preHandler","errorHandler","request","reply","code","createOAuthMetadataRoutes","prefix","routes","createOAuthClientIdentificationRoutes","createOAuthRoutes","metadataRoutes","clientIdentificationRoutes"],"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":"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;AAmD7B,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"}
|