@shipfox/api-auth 9.2.0 → 10.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 +56 -0
- package/README.md +32 -8
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/administration.d.ts +43 -0
- package/dist/core/administration.d.ts.map +1 -0
- package/dist/core/administration.js +152 -0
- package/dist/core/administration.js.map +1 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +2 -5
- package/dist/core/auth.js.map +1 -1
- package/dist/core/entities/administrator-read-model.d.ts +21 -0
- package/dist/core/entities/administrator-read-model.d.ts.map +1 -0
- package/dist/core/entities/administrator-read-model.js +3 -0
- package/dist/core/entities/administrator-read-model.js.map +1 -0
- package/dist/core/errors.d.ts +15 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/db/admin-grants.d.ts +34 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +177 -5
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-users.d.ts +21 -0
- package/dist/db/admin-users.d.ts.map +1 -0
- package/dist/db/admin-users.js +32 -0
- package/dist/db/admin-users.js.map +1 -0
- package/dist/db/db.d.ts +256 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +142 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -0
- package/dist/db/schema/admin-command-results.js +21 -0
- package/dist/db/schema/admin-command-results.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- 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 +4 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -0
- package/dist/presentation/routes/administration.js +267 -0
- package/dist/presentation/routes/administration.js.map +1 -0
- package/dist/presentation/routes/rate-limit.d.ts +1 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +26 -1
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_burly_whizzer.sql +12 -0
- package/drizzle/meta/0002_snapshot.json +791 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +10 -9
- package/src/config.ts +4 -0
- package/src/core/administration.ts +213 -0
- package/src/core/auth-default-signup-policy.test.ts +65 -0
- package/src/core/auth.test.ts +5 -0
- package/src/core/auth.ts +2 -3
- package/src/core/entities/administrator-read-model.ts +23 -0
- package/src/core/errors.ts +35 -0
- package/src/db/admin-grants.test.ts +1 -7
- package/src/db/admin-grants.ts +310 -5
- package/src/db/admin-users.ts +58 -0
- package/src/db/db.ts +2 -0
- package/src/db/schema/admin-command-results.ts +41 -0
- package/src/index.test.ts +6 -2
- package/src/index.ts +23 -2
- package/src/metrics/instance.ts +1 -1
- package/src/presentation/auth/refresh-cookie.test.ts +4 -0
- package/src/presentation/e2eRoutes/index.test.ts +1 -0
- package/src/presentation/routes/administration.test.ts +537 -0
- package/src/presentation/routes/administration.ts +276 -0
- package/src/presentation/routes/index.test.ts +1 -0
- package/src/presentation/routes/rate-limit.ts +26 -2
- package/test/globalSetup.ts +6 -2
- package/test/routes.ts +10 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AUTH_PASSWORD_RESET_SEND_REQUESTED, authEventSchemas } from '@shipfox/api-auth-dto';
|
|
2
|
+
import { administrationActionEventSchemas } from '@shipfox/api-common-dto';
|
|
2
3
|
import { subscriberFactory } from '@shipfox/node-module';
|
|
3
4
|
import { config } from '#config.js';
|
|
4
5
|
import { createEnvironmentSignupPolicy } from '#core/signup-policy.js';
|
|
@@ -10,13 +11,19 @@ import { createLeaseTokenAuthMethod } from '#presentation/auth/lease-token-auth.
|
|
|
10
11
|
import { createRunnerSessionAuthMethod } from '#presentation/auth/runner-session-auth.js';
|
|
11
12
|
import { createAuthE2eRoutes } from '#presentation/e2eRoutes/index.js';
|
|
12
13
|
import { createAuthInterModulePresentation } from '#presentation/inter-module.js';
|
|
14
|
+
import { administrationRoutes, administrationUserRoutes } from '#presentation/routes/administration.js';
|
|
13
15
|
import { buildAuthRoutes } from '#presentation/routes/index.js';
|
|
14
16
|
import { onPasswordResetSendRequested } from '#presentation/subscribers/index.js';
|
|
15
17
|
import { passwordLoginMethods } from './login-methods.js';
|
|
18
|
+
const authPublisherEventSchemas = {
|
|
19
|
+
...authEventSchemas,
|
|
20
|
+
...administrationActionEventSchemas
|
|
21
|
+
};
|
|
16
22
|
export { ADMIN_ROLES, getCurrentAdminRole, hasMinimumAdminRole, highestAdminRole, requireAdminRole, revokeAdminGrant } from '#core/admin-role.js';
|
|
23
|
+
export { bootstrapFirstAdminOwner, grantAdministratorRole, revokeAdministratorGrant } from '#core/administration.js';
|
|
17
24
|
export { createSessionForUser, provisionUser } from '#core/auth.js';
|
|
18
25
|
export { findUserByEmail } from '#core/email-owner.js';
|
|
19
|
-
export { AdminRoleRequiredError, AuthDependencyUnavailableError, EmailNotVerifiedError, InvalidCredentialsError, LastAdminOwnerError, SignupNotAllowedError, UserNotFoundError } from '#core/errors.js';
|
|
26
|
+
export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, AuthDependencyUnavailableError, EmailNotVerifiedError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, SignupNotAllowedError, UserNotFoundError } from '#core/errors.js';
|
|
20
27
|
export { issueJobLeaseToken, jobLeaseParamsFrom, verifyJobLeaseToken } from '#core/job-lease-token.js';
|
|
21
28
|
export { issueRunnerSessionToken, verifyRunnerSessionToken } from '#core/runner-session-token.js';
|
|
22
29
|
export { createEnvironmentSignupPolicy, DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE } from '#core/signup-policy.js';
|
|
@@ -40,7 +47,9 @@ export function createAuthModule({ workspaces, signupPolicy = createEnvironmentS
|
|
|
40
47
|
],
|
|
41
48
|
loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
|
|
42
49
|
routes: [
|
|
43
|
-
buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy)
|
|
50
|
+
buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),
|
|
51
|
+
administrationRoutes,
|
|
52
|
+
administrationUserRoutes
|
|
44
53
|
],
|
|
45
54
|
e2eRoutes: [
|
|
46
55
|
createAuthE2eRoutes(workspaces)
|
|
@@ -50,7 +59,7 @@ export function createAuthModule({ workspaces, signupPolicy = createEnvironmentS
|
|
|
50
59
|
name: 'auth',
|
|
51
60
|
table: authOutbox,
|
|
52
61
|
db,
|
|
53
|
-
eventSchemas:
|
|
62
|
+
eventSchemas: authPublisherEventSchemas
|
|
54
63
|
}
|
|
55
64
|
],
|
|
56
65
|
subscribers: [
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n AUTH_PASSWORD_RESET_SEND_REQUESTED,\n type AuthEventMap,\n authEventSchemas,\n} from '@shipfox/api-auth-dto';\nimport type {ShipfoxModule} from '@shipfox/node-module';\nimport {subscriberFactory} from '@shipfox/node-module';\nimport {config} from '#config.js';\nimport type {SignupPolicy} from '#core/ports.js';\nimport {createEnvironmentSignupPolicy} from '#core/signup-policy.js';\nimport {db} from '#db/db.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {authOutbox} from '#db/schema/outbox.js';\nimport {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';\nimport {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';\nimport {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';\nimport {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';\nimport {createAuthInterModulePresentation} from '#presentation/inter-module.js';\nimport {buildAuthRoutes} from '#presentation/routes/index.js';\nimport {onPasswordResetSendRequested} from '#presentation/subscribers/index.js';\nimport {passwordLoginMethods} from './login-methods.js';\n\nexport type {AdminRole, JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';\nexport {\n ADMIN_ROLES,\n getCurrentAdminRole,\n hasMinimumAdminRole,\n highestAdminRole,\n requireAdminRole,\n revokeAdminGrant,\n} from '#core/admin-role.js';\nexport type {\n CreateSessionForUserError,\n CreateSessionForUserParams,\n CreateSessionForUserResult,\n ProvisionUserParams,\n} from '#core/auth.js';\nexport {createSessionForUser, provisionUser} from '#core/auth.js';\nexport type {EmailOwner, FindUserByEmailParams} from '#core/email-owner.js';\nexport {findUserByEmail} from '#core/email-owner.js';\nexport type {AdminGrant} from '#core/entities/admin-grant.js';\nexport type {User, UserStatus} from '#core/entities/user.js';\nexport {\n AdminRoleRequiredError,\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n InvalidCredentialsError,\n LastAdminOwnerError,\n SignupNotAllowedError,\n UserNotFoundError,\n} from '#core/errors.js';\nexport {\n issueJobLeaseToken,\n jobLeaseParamsFrom,\n verifyJobLeaseToken,\n} from '#core/job-lease-token.js';\nexport type {SignupPolicy} from '#core/ports.js';\nexport {\n issueRunnerSessionToken,\n verifyRunnerSessionToken,\n} from '#core/runner-session-token.js';\nexport {\n createEnvironmentSignupPolicy,\n DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,\n} from '#core/signup-policy.js';\nexport {\n type AuthenticatedSessionContext,\n createJwtAuthMethod,\n getAuthenticatedSessionContext,\n type RefreshSessionId,\n type UserId,\n} from '#presentation/auth/jwt-auth.js';\nexport {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';\nexport {\n authCookiePlugin,\n clearRefreshTokenCookie,\n getRefreshTokenCookie,\n setRefreshTokenCookie,\n} from '#presentation/auth/refresh-cookie.js';\nexport {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';\n\nconst subscriber = subscriberFactory<AuthEventMap>();\n\nexport interface CreateAuthModuleOptions {\n workspaces: import('@shipfox/api-workspaces-dto/inter-module').WorkspacesInterModuleClient;\n signupPolicy?: SignupPolicy;\n}\n\nexport function createAuthModule({\n workspaces,\n signupPolicy = createEnvironmentSignupPolicy(),\n}: CreateAuthModuleOptions): ShipfoxModule {\n return {\n name: 'auth',\n database: {db, migrationsPath, databaseNamespace: 'auth'},\n auth: [createJwtAuthMethod(), createLeaseTokenAuthMethod(), createRunnerSessionAuthMethod()],\n loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),\n routes: [buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy)],\n e2eRoutes: [createAuthE2eRoutes(workspaces)],\n publishers: [{name: 'auth', table: authOutbox, db, eventSchemas:
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n AUTH_PASSWORD_RESET_SEND_REQUESTED,\n type AuthEventMap,\n authEventSchemas,\n} from '@shipfox/api-auth-dto';\nimport {administrationActionEventSchemas} from '@shipfox/api-common-dto';\nimport type {ShipfoxModule} from '@shipfox/node-module';\nimport {subscriberFactory} from '@shipfox/node-module';\nimport {config} from '#config.js';\nimport type {SignupPolicy} from '#core/ports.js';\nimport {createEnvironmentSignupPolicy} from '#core/signup-policy.js';\nimport {db} from '#db/db.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {authOutbox} from '#db/schema/outbox.js';\nimport {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';\nimport {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';\nimport {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';\nimport {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';\nimport {createAuthInterModulePresentation} from '#presentation/inter-module.js';\nimport {\n administrationRoutes,\n administrationUserRoutes,\n} from '#presentation/routes/administration.js';\nimport {buildAuthRoutes} from '#presentation/routes/index.js';\nimport {onPasswordResetSendRequested} from '#presentation/subscribers/index.js';\nimport {passwordLoginMethods} from './login-methods.js';\n\nconst authPublisherEventSchemas = {...authEventSchemas, ...administrationActionEventSchemas};\n\nexport type {AdminRole, JobLeaseTokenClaims, RunnerSessionTokenClaims} from '@shipfox/api-auth-dto';\nexport {\n ADMIN_ROLES,\n getCurrentAdminRole,\n hasMinimumAdminRole,\n highestAdminRole,\n requireAdminRole,\n revokeAdminGrant,\n} from '#core/admin-role.js';\nexport {\n bootstrapFirstAdminOwner,\n grantAdministratorRole,\n revokeAdministratorGrant,\n} from '#core/administration.js';\nexport type {\n CreateSessionForUserError,\n CreateSessionForUserParams,\n CreateSessionForUserResult,\n ProvisionUserParams,\n} from '#core/auth.js';\nexport {createSessionForUser, provisionUser} from '#core/auth.js';\nexport type {EmailOwner, FindUserByEmailParams} from '#core/email-owner.js';\nexport {findUserByEmail} from '#core/email-owner.js';\nexport type {AdminGrant} from '#core/entities/admin-grant.js';\nexport type {User, UserStatus} from '#core/entities/user.js';\nexport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n InvalidAdminBootstrapTokenError,\n InvalidCredentialsError,\n LastAdminOwnerError,\n SignupNotAllowedError,\n UserNotFoundError,\n} from '#core/errors.js';\nexport {\n issueJobLeaseToken,\n jobLeaseParamsFrom,\n verifyJobLeaseToken,\n} from '#core/job-lease-token.js';\nexport type {SignupPolicy} from '#core/ports.js';\nexport {\n issueRunnerSessionToken,\n verifyRunnerSessionToken,\n} from '#core/runner-session-token.js';\nexport {\n createEnvironmentSignupPolicy,\n DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE,\n} from '#core/signup-policy.js';\nexport {\n type AuthenticatedSessionContext,\n createJwtAuthMethod,\n getAuthenticatedSessionContext,\n type RefreshSessionId,\n type UserId,\n} from '#presentation/auth/jwt-auth.js';\nexport {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js';\nexport {\n authCookiePlugin,\n clearRefreshTokenCookie,\n getRefreshTokenCookie,\n setRefreshTokenCookie,\n} from '#presentation/auth/refresh-cookie.js';\nexport {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';\n\nconst subscriber = subscriberFactory<AuthEventMap>();\n\nexport interface CreateAuthModuleOptions {\n workspaces: import('@shipfox/api-workspaces-dto/inter-module').WorkspacesInterModuleClient;\n signupPolicy?: SignupPolicy;\n}\n\nexport function createAuthModule({\n workspaces,\n signupPolicy = createEnvironmentSignupPolicy(),\n}: CreateAuthModuleOptions): ShipfoxModule {\n return {\n name: 'auth',\n database: {db, migrationsPath, databaseNamespace: 'auth'},\n auth: [createJwtAuthMethod(), createLeaseTokenAuthMethod(), createRunnerSessionAuthMethod()],\n loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),\n routes: [\n buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),\n administrationRoutes,\n administrationUserRoutes,\n ],\n e2eRoutes: [createAuthE2eRoutes(workspaces)],\n publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authPublisherEventSchemas}],\n subscribers: [subscriber(AUTH_PASSWORD_RESET_SEND_REQUESTED, onPasswordResetSendRequested)],\n interModulePresentations: [createAuthInterModulePresentation()],\n };\n}\n"],"names":["AUTH_PASSWORD_RESET_SEND_REQUESTED","authEventSchemas","administrationActionEventSchemas","subscriberFactory","config","createEnvironmentSignupPolicy","db","migrationsPath","authOutbox","createJwtAuthMethod","createLeaseTokenAuthMethod","createRunnerSessionAuthMethod","createAuthE2eRoutes","createAuthInterModulePresentation","administrationRoutes","administrationUserRoutes","buildAuthRoutes","onPasswordResetSendRequested","passwordLoginMethods","authPublisherEventSchemas","ADMIN_ROLES","getCurrentAdminRole","hasMinimumAdminRole","highestAdminRole","requireAdminRole","revokeAdminGrant","bootstrapFirstAdminOwner","grantAdministratorRole","revokeAdministratorGrant","createSessionForUser","provisionUser","findUserByEmail","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","AuthDependencyUnavailableError","EmailNotVerifiedError","InvalidAdminBootstrapTokenError","InvalidCredentialsError","LastAdminOwnerError","SignupNotAllowedError","UserNotFoundError","issueJobLeaseToken","jobLeaseParamsFrom","verifyJobLeaseToken","issueRunnerSessionToken","verifyRunnerSessionToken","DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE","getAuthenticatedSessionContext","authCookiePlugin","clearRefreshTokenCookie","getRefreshTokenCookie","setRefreshTokenCookie","subscriber","createAuthModule","workspaces","signupPolicy","name","database","databaseNamespace","auth","loginMethods","AUTH_PASSWORD_ENABLED","routes","e2eRoutes","publishers","table","eventSchemas","subscribers","interModulePresentations"],"mappings":"AAAA,SACEA,kCAAkC,EAElCC,gBAAgB,QACX,wBAAwB;AAC/B,SAAQC,gCAAgC,QAAO,0BAA0B;AAEzE,SAAQC,iBAAiB,QAAO,uBAAuB;AACvD,SAAQC,MAAM,QAAO,aAAa;AAElC,SAAQC,6BAA6B,QAAO,yBAAyB;AACrE,SAAQC,EAAE,QAAO,YAAY;AAC7B,SAAQC,cAAc,QAAO,oBAAoB;AACjD,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,mBAAmB,QAAO,iCAAiC;AACnE,SAAQC,0BAA0B,QAAO,yCAAyC;AAClF,SAAQC,6BAA6B,QAAO,4CAA4C;AACxF,SAAQC,mBAAmB,QAAO,mCAAmC;AACrE,SAAQC,iCAAiC,QAAO,gCAAgC;AAChF,SACEC,oBAAoB,EACpBC,wBAAwB,QACnB,yCAAyC;AAChD,SAAQC,eAAe,QAAO,gCAAgC;AAC9D,SAAQC,4BAA4B,QAAO,qCAAqC;AAChF,SAAQC,oBAAoB,QAAO,qBAAqB;AAExD,MAAMC,4BAA4B;IAAC,GAAGlB,gBAAgB;IAAE,GAAGC,gCAAgC;AAAA;AAG3F,SACEkB,WAAW,EACXC,mBAAmB,EACnBC,mBAAmB,EACnBC,gBAAgB,EAChBC,gBAAgB,EAChBC,gBAAgB,QACX,sBAAsB;AAC7B,SACEC,wBAAwB,EACxBC,sBAAsB,EACtBC,wBAAwB,QACnB,0BAA0B;AAOjC,SAAQC,oBAAoB,EAAEC,aAAa,QAAO,gBAAgB;AAElE,SAAQC,eAAe,QAAO,uBAAuB;AAGrD,SACEC,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,sBAAsB,EACtBC,8BAA8B,EAC9BC,qBAAqB,EACrBC,+BAA+B,EAC/BC,uBAAuB,EACvBC,mBAAmB,EACnBC,qBAAqB,EACrBC,iBAAiB,QACZ,kBAAkB;AACzB,SACEC,kBAAkB,EAClBC,kBAAkB,EAClBC,mBAAmB,QACd,2BAA2B;AAElC,SACEC,uBAAuB,EACvBC,wBAAwB,QACnB,gCAAgC;AACvC,SACE3C,6BAA6B,EAC7B4C,kCAAkC,QAC7B,yBAAyB;AAChC,SAEExC,mBAAmB,EACnByC,8BAA8B,QAGzB,iCAAiC;AACxC,SAAQxC,0BAA0B,QAAO,yCAAyC;AAClF,SACEyC,gBAAgB,EAChBC,uBAAuB,EACvBC,qBAAqB,EACrBC,qBAAqB,QAChB,uCAAuC;AAC9C,SAAQ3C,6BAA6B,QAAO,4CAA4C;AAExF,MAAM4C,aAAapD;AAOnB,OAAO,SAASqD,iBAAiB,EAC/BC,UAAU,EACVC,eAAerD,+BAA+B,EACtB;IACxB,OAAO;QACLsD,MAAM;QACNC,UAAU;YAACtD;YAAIC;YAAgBsD,mBAAmB;QAAM;QACxDC,MAAM;YAACrD;YAAuBC;YAA8BC;SAAgC;QAC5FoD,cAAc7C,qBAAqBd,OAAO4D,qBAAqB;QAC/DC,QAAQ;YACNjD,gBAAgBZ,OAAO4D,qBAAqB,EAAEP,YAAYC;YAC1D5C;YACAC;SACD;QACDmD,WAAW;YAACtD,oBAAoB6C;SAAY;QAC5CU,YAAY;YAAC;gBAACR,MAAM;gBAAQS,OAAO5D;gBAAYF;gBAAI+D,cAAclD;YAAyB;SAAE;QAC5FmD,aAAa;YAACf,WAAWvD,oCAAoCiB;SAA8B;QAC3FsD,0BAA0B;YAAC1D;SAAoC;IACjE;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type AuthTokenType = 'session' | 'job_lease' | 'runner_session';
|
|
2
2
|
export type AuthTokenVerificationOutcome = 'ok' | 'rejected';
|
|
3
3
|
export type AuthTokenRefreshOutcome = 'rotated' | 'grace' | 'rejected';
|
|
4
|
-
export type AuthRateLimitAction = 'login' | 'email-send';
|
|
4
|
+
export type AuthRateLimitAction = 'login' | 'email-send' | 'bootstrap' | 'lookup';
|
|
5
5
|
export type AuthRateLimitScope = 'ip' | 'email';
|
|
6
6
|
export type AuthRateLimitOutcome = 'allowed' | 'blocked' | 'unavailable';
|
|
7
7
|
export declare function recordTokenIssued(tokenType: AuthTokenType): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/metrics/instance.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,gBAAgB,CAAC;AACvE,MAAM,MAAM,4BAA4B,GAAG,IAAI,GAAG,UAAU,CAAC;AAC7D,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/metrics/instance.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,gBAAgB,CAAC;AACvE,MAAM,MAAM,4BAA4B,GAAG,IAAI,GAAG,UAAU,CAAC;AAC7D,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;AAClF,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC;AAChD,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAsCzE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAEhE;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,4BAA4B,GACpC,IAAI,CAEN;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAE3E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,MAAM,EAAE,mBAAmB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,CAAC;IAC1B,OAAO,EAAE,oBAAoB,CAAC;CAC/B,GAAG,IAAI,CAQP;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/metrics/instance.ts"],"sourcesContent":["import {instanceMetrics} from '@shipfox/node-opentelemetry';\n\nexport type AuthTokenType = 'session' | 'job_lease' | 'runner_session';\nexport type AuthTokenVerificationOutcome = 'ok' | 'rejected';\nexport type AuthTokenRefreshOutcome = 'rotated' | 'grace' | 'rejected';\nexport type AuthRateLimitAction = 'login' | 'email-send';\nexport type AuthRateLimitScope = 'ip' | 'email';\nexport type AuthRateLimitOutcome = 'allowed' | 'blocked' | 'unavailable';\n\nconst meter = instanceMetrics.getMeter('auth');\n\nconst tokenIssuedCount = meter.createCounter<{token_type: AuthTokenType}>('auth_token_issued', {\n description: 'Tokens issued by token type',\n});\n\nconst tokenVerifiedCount = meter.createCounter<{\n token_type: AuthTokenType;\n outcome: AuthTokenVerificationOutcome;\n}>('auth_token_verified', {description: 'Token verification attempts by token type and outcome'});\n\nconst tokenRefreshedCount = meter.createCounter<{outcome: AuthTokenRefreshOutcome}>(\n 'auth_token_refreshed',\n {description: 'Refresh-token exchanges by outcome'},\n);\n\nconst rateLimitCheckCount = meter.createCounter<{\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n outcome: AuthRateLimitOutcome;\n}>('auth_rate_limit_checks', {\n description: 'Authentication rate limit checks by action, scope, and outcome',\n});\n\nconst rateLimitPruneFailureCount = meter.createCounter('auth_rate_limit_prune_failures', {\n description: 'Authentication rate limit prune failures',\n});\n\nfunction recordMetric(record: () => void): void {\n try {\n record();\n } catch {\n // Metrics must not affect authentication outcomes.\n }\n}\n\nexport function recordTokenIssued(tokenType: AuthTokenType): void {\n recordMetric(() => tokenIssuedCount.add(1, {token_type: tokenType}));\n}\n\nexport function recordTokenVerified(\n tokenType: AuthTokenType,\n outcome: AuthTokenVerificationOutcome,\n): void {\n recordMetric(() => tokenVerifiedCount.add(1, {token_type: tokenType, outcome}));\n}\n\nexport function recordTokenRefreshed(outcome: AuthTokenRefreshOutcome): void {\n recordMetric(() => tokenRefreshedCount.add(1, {outcome}));\n}\n\nexport function recordAuthRateLimitCheck(params: {\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n outcome: AuthRateLimitOutcome;\n}): void {\n recordMetric(() =>\n rateLimitCheckCount.add(1, {\n action: params.action,\n scope: params.scope,\n outcome: params.outcome,\n }),\n );\n}\n\nexport function recordAuthRateLimitPruneFailure(): void {\n recordMetric(() => rateLimitPruneFailureCount.add(1));\n}\n"],"names":["instanceMetrics","meter","getMeter","tokenIssuedCount","createCounter","description","tokenVerifiedCount","tokenRefreshedCount","rateLimitCheckCount","rateLimitPruneFailureCount","recordMetric","record","recordTokenIssued","tokenType","add","token_type","recordTokenVerified","outcome","recordTokenRefreshed","recordAuthRateLimitCheck","params","action","scope","recordAuthRateLimitPruneFailure"],"mappings":"AAAA,SAAQA,eAAe,QAAO,8BAA8B;AAS5D,MAAMC,QAAQD,gBAAgBE,QAAQ,CAAC;AAEvC,MAAMC,mBAAmBF,MAAMG,aAAa,CAA8B,qBAAqB;IAC7FC,aAAa;AACf;AAEA,MAAMC,qBAAqBL,MAAMG,aAAa,CAG3C,uBAAuB;IAACC,aAAa;AAAuD;AAE/F,MAAME,sBAAsBN,MAAMG,aAAa,CAC7C,wBACA;IAACC,aAAa;AAAoC;AAGpD,MAAMG,sBAAsBP,MAAMG,aAAa,CAI5C,0BAA0B;IAC3BC,aAAa;AACf;AAEA,MAAMI,6BAA6BR,MAAMG,aAAa,CAAC,kCAAkC;IACvFC,aAAa;AACf;AAEA,SAASK,aAAaC,MAAkB;IACtC,IAAI;QACFA;IACF,EAAE,OAAM;IACN,mDAAmD;IACrD;AACF;AAEA,OAAO,SAASC,kBAAkBC,SAAwB;IACxDH,aAAa,IAAMP,iBAAiBW,GAAG,CAAC,GAAG;YAACC,YAAYF;QAAS;AACnE;AAEA,OAAO,SAASG,oBACdH,SAAwB,EACxBI,OAAqC;IAErCP,aAAa,IAAMJ,mBAAmBQ,GAAG,CAAC,GAAG;YAACC,YAAYF;YAAWI;QAAO;AAC9E;AAEA,OAAO,SAASC,qBAAqBD,OAAgC;IACnEP,aAAa,IAAMH,oBAAoBO,GAAG,CAAC,GAAG;YAACG;QAAO;AACxD;AAEA,OAAO,SAASE,yBAAyBC,MAIxC;IACCV,aAAa,IACXF,oBAAoBM,GAAG,CAAC,GAAG;YACzBO,QAAQD,OAAOC,MAAM;YACrBC,OAAOF,OAAOE,KAAK;YACnBL,SAASG,OAAOH,OAAO;QACzB;AAEJ;AAEA,OAAO,SAASM;IACdb,aAAa,IAAMD,2BAA2BK,GAAG,CAAC;AACpD"}
|
|
1
|
+
{"version":3,"sources":["../../src/metrics/instance.ts"],"sourcesContent":["import {instanceMetrics} from '@shipfox/node-opentelemetry';\n\nexport type AuthTokenType = 'session' | 'job_lease' | 'runner_session';\nexport type AuthTokenVerificationOutcome = 'ok' | 'rejected';\nexport type AuthTokenRefreshOutcome = 'rotated' | 'grace' | 'rejected';\nexport type AuthRateLimitAction = 'login' | 'email-send' | 'bootstrap' | 'lookup';\nexport type AuthRateLimitScope = 'ip' | 'email';\nexport type AuthRateLimitOutcome = 'allowed' | 'blocked' | 'unavailable';\n\nconst meter = instanceMetrics.getMeter('auth');\n\nconst tokenIssuedCount = meter.createCounter<{token_type: AuthTokenType}>('auth_token_issued', {\n description: 'Tokens issued by token type',\n});\n\nconst tokenVerifiedCount = meter.createCounter<{\n token_type: AuthTokenType;\n outcome: AuthTokenVerificationOutcome;\n}>('auth_token_verified', {description: 'Token verification attempts by token type and outcome'});\n\nconst tokenRefreshedCount = meter.createCounter<{outcome: AuthTokenRefreshOutcome}>(\n 'auth_token_refreshed',\n {description: 'Refresh-token exchanges by outcome'},\n);\n\nconst rateLimitCheckCount = meter.createCounter<{\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n outcome: AuthRateLimitOutcome;\n}>('auth_rate_limit_checks', {\n description: 'Authentication rate limit checks by action, scope, and outcome',\n});\n\nconst rateLimitPruneFailureCount = meter.createCounter('auth_rate_limit_prune_failures', {\n description: 'Authentication rate limit prune failures',\n});\n\nfunction recordMetric(record: () => void): void {\n try {\n record();\n } catch {\n // Metrics must not affect authentication outcomes.\n }\n}\n\nexport function recordTokenIssued(tokenType: AuthTokenType): void {\n recordMetric(() => tokenIssuedCount.add(1, {token_type: tokenType}));\n}\n\nexport function recordTokenVerified(\n tokenType: AuthTokenType,\n outcome: AuthTokenVerificationOutcome,\n): void {\n recordMetric(() => tokenVerifiedCount.add(1, {token_type: tokenType, outcome}));\n}\n\nexport function recordTokenRefreshed(outcome: AuthTokenRefreshOutcome): void {\n recordMetric(() => tokenRefreshedCount.add(1, {outcome}));\n}\n\nexport function recordAuthRateLimitCheck(params: {\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n outcome: AuthRateLimitOutcome;\n}): void {\n recordMetric(() =>\n rateLimitCheckCount.add(1, {\n action: params.action,\n scope: params.scope,\n outcome: params.outcome,\n }),\n );\n}\n\nexport function recordAuthRateLimitPruneFailure(): void {\n recordMetric(() => rateLimitPruneFailureCount.add(1));\n}\n"],"names":["instanceMetrics","meter","getMeter","tokenIssuedCount","createCounter","description","tokenVerifiedCount","tokenRefreshedCount","rateLimitCheckCount","rateLimitPruneFailureCount","recordMetric","record","recordTokenIssued","tokenType","add","token_type","recordTokenVerified","outcome","recordTokenRefreshed","recordAuthRateLimitCheck","params","action","scope","recordAuthRateLimitPruneFailure"],"mappings":"AAAA,SAAQA,eAAe,QAAO,8BAA8B;AAS5D,MAAMC,QAAQD,gBAAgBE,QAAQ,CAAC;AAEvC,MAAMC,mBAAmBF,MAAMG,aAAa,CAA8B,qBAAqB;IAC7FC,aAAa;AACf;AAEA,MAAMC,qBAAqBL,MAAMG,aAAa,CAG3C,uBAAuB;IAACC,aAAa;AAAuD;AAE/F,MAAME,sBAAsBN,MAAMG,aAAa,CAC7C,wBACA;IAACC,aAAa;AAAoC;AAGpD,MAAMG,sBAAsBP,MAAMG,aAAa,CAI5C,0BAA0B;IAC3BC,aAAa;AACf;AAEA,MAAMI,6BAA6BR,MAAMG,aAAa,CAAC,kCAAkC;IACvFC,aAAa;AACf;AAEA,SAASK,aAAaC,MAAkB;IACtC,IAAI;QACFA;IACF,EAAE,OAAM;IACN,mDAAmD;IACrD;AACF;AAEA,OAAO,SAASC,kBAAkBC,SAAwB;IACxDH,aAAa,IAAMP,iBAAiBW,GAAG,CAAC,GAAG;YAACC,YAAYF;QAAS;AACnE;AAEA,OAAO,SAASG,oBACdH,SAAwB,EACxBI,OAAqC;IAErCP,aAAa,IAAMJ,mBAAmBQ,GAAG,CAAC,GAAG;YAACC,YAAYF;YAAWI;QAAO;AAC9E;AAEA,OAAO,SAASC,qBAAqBD,OAAgC;IACnEP,aAAa,IAAMH,oBAAoBO,GAAG,CAAC,GAAG;YAACG;QAAO;AACxD;AAEA,OAAO,SAASE,yBAAyBC,MAIxC;IACCV,aAAa,IACXF,oBAAoBM,GAAG,CAAC,GAAG;YACzBO,QAAQD,OAAOC,MAAM;YACrBC,OAAOF,OAAOE,KAAK;YACnBL,SAASG,OAAOH,OAAO;QACzB;AAEJ;AAEA,OAAO,SAASM;IACdb,aAAa,IAAMD,2BAA2BK,GAAG,CAAC;AACpD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/administration.ts"],"names":[],"mappings":"AAcA,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AA2PhF,eAAO,MAAM,oBAAoB,EAAE,UAIlC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAItC,CAAC"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { AUTH_USER } from '@shipfox/api-auth-context';
|
|
2
|
+
import { administratorUserLookupQuerySchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema } from '@shipfox/api-auth-dto';
|
|
3
|
+
import { decodeTimestampIdCursor, encodeTimestampIdCursor } from '@shipfox/node-drizzle';
|
|
4
|
+
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { requireAdminRole } from '#core/admin-role.js';
|
|
7
|
+
import { bootstrapFirstAdminOwner, findAdministratorUserSummary, grantAdministratorRole, listAdministratorGrantSummaries, revokeAdministratorGrant } from '#core/administration.js';
|
|
8
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
9
|
+
import { getClientContext } from '#presentation/auth/jwt-auth.js';
|
|
10
|
+
import { createAuthIpRateLimitPreHandler } from './rate-limit.js';
|
|
11
|
+
const idempotencyKeyMaxLength = 256;
|
|
12
|
+
function requireActorId(request) {
|
|
13
|
+
const client = getClientContext(request);
|
|
14
|
+
if (!client) {
|
|
15
|
+
throw new ClientError('Authentication required', 'unauthorized', {
|
|
16
|
+
status: 401
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return client.userId;
|
|
20
|
+
}
|
|
21
|
+
function requireIdempotencyKey(request) {
|
|
22
|
+
const value = request.headers['idempotency-key'];
|
|
23
|
+
const key = Array.isArray(value) ? value[0] : value;
|
|
24
|
+
if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {
|
|
25
|
+
throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {
|
|
26
|
+
status: 400
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
async function requireAdministratorObserver(request) {
|
|
32
|
+
await requireAdminRole({
|
|
33
|
+
userId: requireActorId(request),
|
|
34
|
+
minimumRole: 'admin-observer'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function toAdminGrantDto(grant) {
|
|
38
|
+
return {
|
|
39
|
+
id: grant.id,
|
|
40
|
+
user_id: grant.userId,
|
|
41
|
+
role: grant.role,
|
|
42
|
+
revoked_at: grant.revokedAt?.toISOString() ?? null,
|
|
43
|
+
created_at: grant.createdAt.toISOString(),
|
|
44
|
+
updated_at: grant.updatedAt.toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function toAdministratorUserSummaryDto(user) {
|
|
48
|
+
return {
|
|
49
|
+
id: user.id,
|
|
50
|
+
email: user.email,
|
|
51
|
+
name: user.name,
|
|
52
|
+
status: user.status,
|
|
53
|
+
email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,
|
|
54
|
+
created_at: user.createdAt.toISOString(),
|
|
55
|
+
admin_role: user.adminRole
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function toAdministratorGrantSummaryDto(grant) {
|
|
59
|
+
return {
|
|
60
|
+
grant_id: grant.grantId,
|
|
61
|
+
role: grant.role,
|
|
62
|
+
created_at: grant.createdAt.toISOString(),
|
|
63
|
+
revoked_at: grant.revokedAt?.toISOString() ?? null,
|
|
64
|
+
user: grant.user
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function translateAdministrationError(error) {
|
|
68
|
+
if (error instanceof AdminRoleRequiredError) {
|
|
69
|
+
throw new ClientError('Administrator role required', 'forbidden', {
|
|
70
|
+
status: 403,
|
|
71
|
+
details: {
|
|
72
|
+
required_role: error.minimumRole
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (error instanceof InvalidAdminBootstrapTokenError) {
|
|
77
|
+
throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {
|
|
78
|
+
status: 403
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof AdminBootstrapClosedError) {
|
|
82
|
+
throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {
|
|
83
|
+
status: 409
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (error instanceof AdminGrantAlreadyExistsError) {
|
|
87
|
+
throw new ClientError('Administrator grant already exists', 'grant-already-exists', {
|
|
88
|
+
status: 409
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof AdminGrantNotFoundError) {
|
|
92
|
+
throw new ClientError('Administrator grant not found', 'not-found', {
|
|
93
|
+
status: 404
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof UserNotFoundError) {
|
|
97
|
+
throw new ClientError('User not found', 'not-found', {
|
|
98
|
+
status: 404
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (error instanceof LastAdminOwnerError) {
|
|
102
|
+
throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {
|
|
103
|
+
status: 409
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (error instanceof AdminIdempotencyKeyReuseError) {
|
|
107
|
+
throw new ClientError('Idempotency-Key was already used for a different command', 'idempotency-key-reused', {
|
|
108
|
+
status: 409
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
const bootstrapRoute = defineRoute({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
path: '/bootstrap',
|
|
116
|
+
description: 'Claim the first administrator owner role with the deployment bootstrap token.',
|
|
117
|
+
schema: {
|
|
118
|
+
body: bootstrapAdminOwnerBodySchema,
|
|
119
|
+
response: {
|
|
120
|
+
201: bootstrapAdminOwnerResponseSchema
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
preHandler: createAuthIpRateLimitPreHandler('bootstrap'),
|
|
124
|
+
errorHandler: translateAdministrationError,
|
|
125
|
+
handler: async (request, reply)=>{
|
|
126
|
+
const actorId = requireActorId(request);
|
|
127
|
+
const grant = await bootstrapFirstAdminOwner({
|
|
128
|
+
actorId,
|
|
129
|
+
bootstrapToken: request.body.bootstrap_token,
|
|
130
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
131
|
+
correlationId: request.id
|
|
132
|
+
});
|
|
133
|
+
reply.code(201);
|
|
134
|
+
return toAdminGrantDto(grant);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const listRoute = defineRoute({
|
|
138
|
+
method: 'GET',
|
|
139
|
+
path: '/',
|
|
140
|
+
description: 'List bounded local administrator grant summaries.',
|
|
141
|
+
schema: {
|
|
142
|
+
querystring: listAdminGrantsQuerySchema,
|
|
143
|
+
response: {
|
|
144
|
+
200: listAdminGrantsResponseSchema
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
errorHandler: translateAdministrationError,
|
|
148
|
+
handler: async (request)=>{
|
|
149
|
+
const { limit, cursor } = request.query;
|
|
150
|
+
const decodedCursor = decodeTimestampIdCursor(cursor);
|
|
151
|
+
if (cursor && !decodedCursor) {
|
|
152
|
+
throw new ClientError('Invalid cursor', 'invalid-cursor', {
|
|
153
|
+
status: 400
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const result = await listAdministratorGrantSummaries({
|
|
157
|
+
actorId: requireActorId(request),
|
|
158
|
+
limit,
|
|
159
|
+
...decodedCursor ? {
|
|
160
|
+
cursor: decodedCursor
|
|
161
|
+
} : {}
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
grants: result.grants.map(toAdministratorGrantSummaryDto),
|
|
165
|
+
next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const userLookupRoute = defineRoute({
|
|
170
|
+
method: 'GET',
|
|
171
|
+
path: '/',
|
|
172
|
+
description: 'Find one administrator-safe user summary by exact ID or email.',
|
|
173
|
+
schema: {
|
|
174
|
+
querystring: administratorUserLookupQuerySchema,
|
|
175
|
+
response: {
|
|
176
|
+
200: administratorUserSummarySchema
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
preHandler: [
|
|
180
|
+
requireAdministratorObserver,
|
|
181
|
+
createAuthIpRateLimitPreHandler('lookup')
|
|
182
|
+
],
|
|
183
|
+
errorHandler: translateAdministrationError,
|
|
184
|
+
handler: async (request)=>{
|
|
185
|
+
const { id, user_id: userId, email } = request.query;
|
|
186
|
+
const lookupId = id ?? userId;
|
|
187
|
+
const actorId = requireActorId(request);
|
|
188
|
+
const user = lookupId ? await findAdministratorUserSummary({
|
|
189
|
+
actorId,
|
|
190
|
+
id: lookupId
|
|
191
|
+
}) : email ? await findAdministratorUserSummary({
|
|
192
|
+
actorId,
|
|
193
|
+
email
|
|
194
|
+
}) : undefined;
|
|
195
|
+
if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');
|
|
196
|
+
return toAdministratorUserSummaryDto(user);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const grantRoute = defineRoute({
|
|
200
|
+
method: 'POST',
|
|
201
|
+
path: '/',
|
|
202
|
+
description: 'Grant a local administrator role to an active user.',
|
|
203
|
+
schema: {
|
|
204
|
+
body: grantAdminRoleBodySchema,
|
|
205
|
+
response: {
|
|
206
|
+
201: grantAdminRoleResponseSchema
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
errorHandler: translateAdministrationError,
|
|
210
|
+
handler: async (request, reply)=>{
|
|
211
|
+
const actorId = requireActorId(request);
|
|
212
|
+
const grant = await grantAdministratorRole({
|
|
213
|
+
actorId,
|
|
214
|
+
userId: request.body.user_id,
|
|
215
|
+
role: request.body.role,
|
|
216
|
+
reason: request.body.reason,
|
|
217
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
218
|
+
correlationId: request.id
|
|
219
|
+
});
|
|
220
|
+
reply.code(201);
|
|
221
|
+
return toAdminGrantDto(grant);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
const revokeRoute = defineRoute({
|
|
225
|
+
method: 'DELETE',
|
|
226
|
+
path: '/:grantId',
|
|
227
|
+
description: 'Revoke a local administrator grant.',
|
|
228
|
+
schema: {
|
|
229
|
+
params: z.object({
|
|
230
|
+
grantId: z.string().uuid()
|
|
231
|
+
}),
|
|
232
|
+
body: revokeAdminGrantBodySchema,
|
|
233
|
+
response: {
|
|
234
|
+
200: revokeAdminGrantResponseSchema
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
errorHandler: translateAdministrationError,
|
|
238
|
+
handler: async (request)=>{
|
|
239
|
+
const grant = await revokeAdministratorGrant({
|
|
240
|
+
actorId: requireActorId(request),
|
|
241
|
+
grantId: request.params.grantId,
|
|
242
|
+
reason: request.body.reason,
|
|
243
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
244
|
+
correlationId: request.id
|
|
245
|
+
});
|
|
246
|
+
return toAdminGrantDto(grant);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
export const administrationRoutes = {
|
|
250
|
+
prefix: '/admin/auth/admin-grants',
|
|
251
|
+
auth: AUTH_USER,
|
|
252
|
+
routes: [
|
|
253
|
+
bootstrapRoute,
|
|
254
|
+
listRoute,
|
|
255
|
+
grantRoute,
|
|
256
|
+
revokeRoute
|
|
257
|
+
]
|
|
258
|
+
};
|
|
259
|
+
export const administrationUserRoutes = {
|
|
260
|
+
prefix: '/admin/auth/users',
|
|
261
|
+
auth: AUTH_USER,
|
|
262
|
+
routes: [
|
|
263
|
+
userLookupRoute
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=administration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n administratorUserLookupQuerySchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n} from '@shipfox/api-auth-dto';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n grantAdministratorRole,\n listAdministratorGrantSummaries,\n revokeAdministratorGrant,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = {\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n};\n\nexport const administrationUserRoutes: RouteGroup = {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute],\n};\n"],"names":["AUTH_USER","administratorUserLookupQuerySchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","grantAdministratorRole","listAdministratorGrantSummaries","revokeAdministratorGrant","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","getClientContext","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","correlationId","code","listRoute","querystring","limit","cursor","query","decodedCursor","result","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","grantRoute","reason","revokeRoute","params","object","string","uuid","administrationRoutes","prefix","auth","routes","administrationUserRoutes"],"mappings":"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"}
|
|
@@ -6,5 +6,6 @@ interface EmailBody {
|
|
|
6
6
|
export declare function createAuthRateLimitPreHandler(action: AuthRateLimitAction): (request: FastifyRequest<{
|
|
7
7
|
Body: EmailBody;
|
|
8
8
|
}>, reply: FastifyReply) => Promise<void>;
|
|
9
|
+
export declare function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
9
10
|
export {};
|
|
10
11
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EACL,KAAK,mBAAmB,EAMzB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EACL,KAAK,mBAAmB,EAMzB,MAAM,qBAAqB,CAAC;AAsB7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgFD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,IACzD,SAAS,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,IAC3D,SAAS,cAAc,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E"}
|
|
@@ -20,18 +20,32 @@ const policies = {
|
|
|
20
20
|
limit: 3,
|
|
21
21
|
windowSeconds: 60 * 60
|
|
22
22
|
}
|
|
23
|
+
},
|
|
24
|
+
bootstrap: {
|
|
25
|
+
ip: {
|
|
26
|
+
limit: 5,
|
|
27
|
+
windowSeconds: 15 * 60
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
lookup: {
|
|
31
|
+
ip: {
|
|
32
|
+
limit: 60,
|
|
33
|
+
windowSeconds: 5 * 60
|
|
34
|
+
}
|
|
23
35
|
}
|
|
24
36
|
};
|
|
25
37
|
function routeName(request) {
|
|
26
38
|
return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';
|
|
27
39
|
}
|
|
28
40
|
async function enforceRateLimit(params) {
|
|
41
|
+
const policy = policies[params.action][params.scope];
|
|
42
|
+
if (!policy) return;
|
|
29
43
|
try {
|
|
30
44
|
await checkAuthRateLimit({
|
|
31
45
|
action: params.action,
|
|
32
46
|
scope: params.scope,
|
|
33
47
|
identifier: params.identifier,
|
|
34
|
-
...
|
|
48
|
+
...policy
|
|
35
49
|
});
|
|
36
50
|
} catch (error) {
|
|
37
51
|
if (error instanceof AuthRateLimitExceededError) {
|
|
@@ -97,5 +111,16 @@ export function createAuthRateLimitPreHandler(action) {
|
|
|
97
111
|
});
|
|
98
112
|
};
|
|
99
113
|
}
|
|
114
|
+
export function createAuthIpRateLimitPreHandler(action) {
|
|
115
|
+
return async (request, reply)=>{
|
|
116
|
+
await enforceRateLimit({
|
|
117
|
+
request,
|
|
118
|
+
reply,
|
|
119
|
+
action,
|
|
120
|
+
scope: 'ip',
|
|
121
|
+
identifier: request.ip
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}
|
|
100
125
|
|
|
101
126
|
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {\n type AuthRateLimitAction,\n AuthRateLimitExceededError,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n AuthRateLimitUnavailableError,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {\n type AuthRateLimitAction,\n AuthRateLimitExceededError,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n AuthRateLimitUnavailableError,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n try {\n await checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n });\n } catch (error) {\n if (error instanceof AuthRateLimitExceededError) {\n params.request.log.warn(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n retryAfterSeconds: error.retryAfterSeconds,\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n 'Auth rate limit blocked request',\n );\n params.reply.header('Retry-After', String(error.retryAfterSeconds));\n throw new ClientError('Rate limit exceeded', 'rate-limited', {\n status: 429,\n details: {retry_after_seconds: error.retryAfterSeconds},\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n });\n }\n\n if (error instanceof AuthRateLimitUnavailableError) {\n params.request.log.error(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n err: error,\n },\n 'Auth rate limiter unavailable',\n );\n throw new ClientError(\n 'Authentication rate limiter unavailable',\n 'auth-rate-limit-unavailable',\n {\n status: 503,\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n },\n );\n }\n\n throw error;\n }\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n"],"names":["ClientError","AuthRateLimitExceededError","AuthRateLimitUnavailableError","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","routeName","request","routeOptions","url","split","enforceRateLimit","params","policy","action","scope","identifier","error","log","warn","route","retryAfterSeconds","identifierHmacPrefix","reply","header","String","status","details","retry_after_seconds","data","cause","err","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler"],"mappings":"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"}
|