@shipfox/api-auth 9.3.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +48 -0
- package/README.md +14 -3
- package/dist/core/administration.d.ts +19 -2
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +37 -4
- package/dist/core/administration.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/db/admin-grants.d.ts +25 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +51 -14
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +2 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +113 -10
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +12 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/core/administration.ts +50 -4
- package/src/core/entities/administrator-read-model.ts +23 -0
- package/src/db/admin-grants.test.ts +12 -2
- package/src/db/admin-grants.ts +85 -39
- package/src/db/admin-users.ts +58 -0
- package/src/index.test.ts +4 -1
- package/src/index.ts +7 -2
- package/src/metrics/instance.ts +6 -1
- package/src/presentation/routes/administration.test.ts +432 -20
- package/src/presentation/routes/administration.ts +109 -10
- package/src/presentation/routes/rate-limit.ts +6 -0
- package/test/routes.ts +11 -2
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { and, eq, isNull } from 'drizzle-orm';
|
|
2
|
+
import { highestAdminRole } from '#core/admin-role-model.js';
|
|
3
|
+
import { db } from './db.js';
|
|
4
|
+
import { adminGrants } from './schema/admin-grants.js';
|
|
5
|
+
import { users } from './schema/users.js';
|
|
6
|
+
export async function findAdministratorUser(params) {
|
|
7
|
+
const identifier = 'id' in params ? eq(users.id, params.id) : eq(users.email, params.email);
|
|
8
|
+
const rows = await db().select({
|
|
9
|
+
id: users.id,
|
|
10
|
+
email: users.email,
|
|
11
|
+
name: users.name,
|
|
12
|
+
emailVerifiedAt: users.emailVerifiedAt,
|
|
13
|
+
status: users.status,
|
|
14
|
+
createdAt: users.createdAt,
|
|
15
|
+
adminRole: adminGrants.role
|
|
16
|
+
}).from(users).leftJoin(adminGrants, and(eq(adminGrants.userId, users.id), isNull(adminGrants.revokedAt), eq(users.status, 'active'))).where(identifier);
|
|
17
|
+
const first = rows[0];
|
|
18
|
+
if (!first) return undefined;
|
|
19
|
+
return {
|
|
20
|
+
id: first.id,
|
|
21
|
+
email: first.email,
|
|
22
|
+
name: first.name,
|
|
23
|
+
emailVerifiedAt: first.emailVerifiedAt,
|
|
24
|
+
status: first.status,
|
|
25
|
+
createdAt: first.createdAt,
|
|
26
|
+
adminRole: highestAdminRole(rows.flatMap(({ adminRole })=>adminRole ? [
|
|
27
|
+
adminRole
|
|
28
|
+
] : []))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=admin-users.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/db/admin-users.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport {and, eq, isNull} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {UserStatus} from '#core/entities/user.js';\nimport {db} from './db.js';\nimport {adminGrants} from './schema/admin-grants.js';\nimport {users} from './schema/users.js';\n\nexport interface AdministratorUserRecord {\n id: string;\n email: string;\n name: string | null;\n emailVerifiedAt: Date | null;\n status: UserStatus;\n createdAt: Date;\n adminRole: AdminRole | null;\n}\n\ntype AdministratorUserLookup = {id: string; email?: never} | {email: string; id?: never};\n\nexport async function findAdministratorUser(\n params: AdministratorUserLookup,\n): Promise<AdministratorUserRecord | undefined> {\n const identifier = 'id' in params ? eq(users.id, params.id) : eq(users.email, params.email);\n const rows = await db()\n .select({\n id: users.id,\n email: users.email,\n name: users.name,\n emailVerifiedAt: users.emailVerifiedAt,\n status: users.status,\n createdAt: users.createdAt,\n adminRole: adminGrants.role,\n })\n .from(users)\n .leftJoin(\n adminGrants,\n and(\n eq(adminGrants.userId, users.id),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .where(identifier);\n\n const first = rows[0];\n if (!first) return undefined;\n\n return {\n id: first.id,\n email: first.email,\n name: first.name,\n emailVerifiedAt: first.emailVerifiedAt,\n status: first.status,\n createdAt: first.createdAt,\n adminRole: highestAdminRole(rows.flatMap(({adminRole}) => (adminRole ? [adminRole] : []))),\n };\n}\n"],"names":["and","eq","isNull","highestAdminRole","db","adminGrants","users","findAdministratorUser","params","identifier","id","email","rows","select","name","emailVerifiedAt","status","createdAt","adminRole","role","from","leftJoin","userId","revokedAt","where","first","undefined","flatMap"],"mappings":"AACA,SAAQA,GAAG,EAAEC,EAAE,EAAEC,MAAM,QAAO,cAAc;AAC5C,SAAQC,gBAAgB,QAAO,4BAA4B;AAE3D,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,WAAW,QAAO,2BAA2B;AACrD,SAAQC,KAAK,QAAO,oBAAoB;AAcxC,OAAO,eAAeC,sBACpBC,MAA+B;IAE/B,MAAMC,aAAa,QAAQD,SAASP,GAAGK,MAAMI,EAAE,EAAEF,OAAOE,EAAE,IAAIT,GAAGK,MAAMK,KAAK,EAAEH,OAAOG,KAAK;IAC1F,MAAMC,OAAO,MAAMR,KAChBS,MAAM,CAAC;QACNH,IAAIJ,MAAMI,EAAE;QACZC,OAAOL,MAAMK,KAAK;QAClBG,MAAMR,MAAMQ,IAAI;QAChBC,iBAAiBT,MAAMS,eAAe;QACtCC,QAAQV,MAAMU,MAAM;QACpBC,WAAWX,MAAMW,SAAS;QAC1BC,WAAWb,YAAYc,IAAI;IAC7B,GACCC,IAAI,CAACd,OACLe,QAAQ,CACPhB,aACAL,IACEC,GAAGI,YAAYiB,MAAM,EAAEhB,MAAMI,EAAE,GAC/BR,OAAOG,YAAYkB,SAAS,GAC5BtB,GAAGK,MAAMU,MAAM,EAAE,YAGpBQ,KAAK,CAACf;IAET,MAAMgB,QAAQb,IAAI,CAAC,EAAE;IACrB,IAAI,CAACa,OAAO,OAAOC;IAEnB,OAAO;QACLhB,IAAIe,MAAMf,EAAE;QACZC,OAAOc,MAAMd,KAAK;QAClBG,MAAMW,MAAMX,IAAI;QAChBC,iBAAiBU,MAAMV,eAAe;QACtCC,QAAQS,MAAMT,MAAM;QACpBC,WAAWQ,MAAMR,SAAS;QAC1BC,WAAWf,iBAAiBS,KAAKe,OAAO,CAAC,CAAC,EAACT,SAAS,EAAC,GAAMA,YAAY;gBAACA;aAAU,GAAG,EAAE;IACzF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ShipfoxModule } from '@shipfox/node-module';
|
|
|
2
2
|
import type { SignupPolicy } from '#core/ports.js';
|
|
3
3
|
export type { AdminRole, JobLeaseTokenClaims, RunnerSessionTokenClaims } from '@shipfox/api-auth-dto';
|
|
4
4
|
export { ADMIN_ROLES, getCurrentAdminRole, hasMinimumAdminRole, highestAdminRole, requireAdminRole, revokeAdminGrant, } from '#core/admin-role.js';
|
|
5
|
-
export { bootstrapFirstAdminOwner, grantAdministratorRole,
|
|
5
|
+
export { bootstrapFirstAdminOwner, grantAdministratorRole, revokeAdministratorGrant, } from '#core/administration.js';
|
|
6
6
|
export type { CreateSessionForUserError, CreateSessionForUserParams, CreateSessionForUserResult, ProvisionUserParams, } from '#core/auth.js';
|
|
7
7
|
export { createSessionForUser, provisionUser } from '#core/auth.js';
|
|
8
8
|
export type { EmailOwner, FindUserByEmailParams } from '#core/email-owner.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAGxD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAGxD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAqBjD,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,uBAAuB,CAAC;AACpG,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,oBAAoB,EAAE,aAAa,EAAC,MAAM,eAAe,CAAC;AAClE,YAAY,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAC,eAAe,EAAC,MAAM,sBAAsB,CAAC;AACrD,YAAY,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EAAC,IAAI,EAAE,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,EACrB,+BAA+B,EAC/B,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,6BAA6B,EAC7B,kCAAkC,GACnC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,8BAA8B,EAC9B,KAAK,gBAAgB,EACrB,KAAK,MAAM,GACZ,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAC,0BAA0B,EAAC,MAAM,wCAAwC,CAAC;AAClF,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAC,6BAA6B,EAAC,MAAM,2CAA2C,CAAC;AAIxF,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,OAAO,0CAA0C,EAAE,2BAA2B,CAAC;IAC3F,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,YAA8C,GAC/C,EAAE,uBAAuB,GAAG,aAAa,CAiBzC"}
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createLeaseTokenAuthMethod } from '#presentation/auth/lease-token-auth.
|
|
|
11
11
|
import { createRunnerSessionAuthMethod } from '#presentation/auth/runner-session-auth.js';
|
|
12
12
|
import { createAuthE2eRoutes } from '#presentation/e2eRoutes/index.js';
|
|
13
13
|
import { createAuthInterModulePresentation } from '#presentation/inter-module.js';
|
|
14
|
-
import { administrationRoutes } from '#presentation/routes/administration.js';
|
|
14
|
+
import { administrationBootstrapRoutes, administrationRoutes, administrationUserRoutes } from '#presentation/routes/administration.js';
|
|
15
15
|
import { buildAuthRoutes } from '#presentation/routes/index.js';
|
|
16
16
|
import { onPasswordResetSendRequested } from '#presentation/subscribers/index.js';
|
|
17
17
|
import { passwordLoginMethods } from './login-methods.js';
|
|
@@ -20,7 +20,7 @@ const authPublisherEventSchemas = {
|
|
|
20
20
|
...administrationActionEventSchemas
|
|
21
21
|
};
|
|
22
22
|
export { ADMIN_ROLES, getCurrentAdminRole, hasMinimumAdminRole, highestAdminRole, requireAdminRole, revokeAdminGrant } from '#core/admin-role.js';
|
|
23
|
-
export { bootstrapFirstAdminOwner, grantAdministratorRole,
|
|
23
|
+
export { bootstrapFirstAdminOwner, grantAdministratorRole, revokeAdministratorGrant } from '#core/administration.js';
|
|
24
24
|
export { createSessionForUser, provisionUser } from '#core/auth.js';
|
|
25
25
|
export { findUserByEmail } from '#core/email-owner.js';
|
|
26
26
|
export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, AuthDependencyUnavailableError, EmailNotVerifiedError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, SignupNotAllowedError, UserNotFoundError } from '#core/errors.js';
|
|
@@ -48,7 +48,9 @@ export function createAuthModule({ workspaces, signupPolicy = createEnvironmentS
|
|
|
48
48
|
loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
|
|
49
49
|
routes: [
|
|
50
50
|
buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),
|
|
51
|
-
|
|
51
|
+
administrationBootstrapRoutes,
|
|
52
|
+
administrationRoutes,
|
|
53
|
+
administrationUserRoutes
|
|
52
54
|
],
|
|
53
55
|
e2eRoutes: [
|
|
54
56
|
createAuthE2eRoutes(workspaces)
|
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 {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 {administrationRoutes} 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
|
|
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 administrationBootstrapRoutes,\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 administrationBootstrapRoutes,\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","administrationBootstrapRoutes","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,6BAA6B,EAC7BC,oBAAoB,EACpBC,wBAAwB,QACnB,yCAAyC;AAChD,SAAQC,eAAe,QAAO,gCAAgC;AAC9D,SAAQC,4BAA4B,QAAO,qCAAqC;AAChF,SAAQC,oBAAoB,QAAO,qBAAqB;AAExD,MAAMC,4BAA4B;IAAC,GAAGnB,gBAAgB;IAAE,GAAGC,gCAAgC;AAAA;AAG3F,SACEmB,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,SACE5C,6BAA6B,EAC7B6C,kCAAkC,QAC7B,yBAAyB;AAChC,SAEEzC,mBAAmB,EACnB0C,8BAA8B,QAGzB,iCAAiC;AACxC,SAAQzC,0BAA0B,QAAO,yCAAyC;AAClF,SACE0C,gBAAgB,EAChBC,uBAAuB,EACvBC,qBAAqB,EACrBC,qBAAqB,QAChB,uCAAuC;AAC9C,SAAQ5C,6BAA6B,QAAO,4CAA4C;AAExF,MAAM6C,aAAarD;AAOnB,OAAO,SAASsD,iBAAiB,EAC/BC,UAAU,EACVC,eAAetD,+BAA+B,EACtB;IACxB,OAAO;QACLuD,MAAM;QACNC,UAAU;YAACvD;YAAIC;YAAgBuD,mBAAmB;QAAM;QACxDC,MAAM;YAACtD;YAAuBC;YAA8BC;SAAgC;QAC5FqD,cAAc7C,qBAAqBf,OAAO6D,qBAAqB;QAC/DC,QAAQ;YACNjD,gBAAgBb,OAAO6D,qBAAqB,EAAEP,YAAYC;YAC1D7C;YACAC;YACAC;SACD;QACDmD,WAAW;YAACvD,oBAAoB8C;SAAY;QAC5CU,YAAY;YAAC;gBAACR,MAAM;gBAAQS,OAAO7D;gBAAYF;gBAAIgE,cAAclD;YAAyB;SAAE;QAC5FmD,aAAa;YAACf,WAAWxD,oCAAoCkB;SAA8B;QAC3FsD,0BAA0B;YAAC3D;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' | 'bootstrap';
|
|
4
|
+
export type AuthRateLimitAction = 'login' | 'email-send' | 'bootstrap' | 'bootstrap-state' | '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,
|
|
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,GAC3B,OAAO,GACP,YAAY,GACZ,WAAW,GACX,iBAAiB,GACjB,QAAQ,CAAC;AACb,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
|
|
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 =\n | 'login'\n | 'email-send'\n | 'bootstrap'\n | 'bootstrap-state'\n | '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;AAc5D,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,3 +1,5 @@
|
|
|
1
1
|
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
2
2
|
export declare const administrationRoutes: RouteGroup;
|
|
3
|
+
export declare const administrationBootstrapRoutes: RouteGroup;
|
|
4
|
+
export declare const administrationUserRoutes: RouteGroup;
|
|
3
5
|
//# sourceMappingURL=administration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/administration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/administration.ts"],"names":[],"mappings":"AAeA,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAsQhF,eAAO,MAAM,oBAAoB,EAAE,UAIlC,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,UAI3C,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAItC,CAAC"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { AUTH_USER } from '@shipfox/api-auth-context';
|
|
2
|
-
import { bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, listAdminGrantsResponseSchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema } from '@shipfox/api-auth-dto';
|
|
2
|
+
import { adminBootstrapStateSchema, administratorUserLookupQuerySchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema } from '@shipfox/api-auth-dto';
|
|
3
|
+
import { decodeTimestampIdCursor, encodeTimestampIdCursor } from '@shipfox/node-drizzle';
|
|
3
4
|
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
4
5
|
import { z } from 'zod';
|
|
5
|
-
import {
|
|
6
|
+
import { requireAdminRole } from '#core/admin-role.js';
|
|
7
|
+
import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, listAdministratorGrantSummaries, revokeAdministratorGrant } from '#core/administration.js';
|
|
6
8
|
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
7
9
|
import { getClientContext } from '#presentation/auth/jwt-auth.js';
|
|
8
10
|
import { createAuthIpRateLimitPreHandler } from './rate-limit.js';
|
|
@@ -26,6 +28,12 @@ function requireIdempotencyKey(request) {
|
|
|
26
28
|
}
|
|
27
29
|
return key;
|
|
28
30
|
}
|
|
31
|
+
async function requireAdministratorObserver(request) {
|
|
32
|
+
await requireAdminRole({
|
|
33
|
+
userId: requireActorId(request),
|
|
34
|
+
minimumRole: 'admin-observer'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
29
37
|
function toAdminGrantDto(grant) {
|
|
30
38
|
return {
|
|
31
39
|
id: grant.id,
|
|
@@ -36,9 +44,29 @@ function toAdminGrantDto(grant) {
|
|
|
36
44
|
updated_at: grant.updatedAt.toISOString()
|
|
37
45
|
};
|
|
38
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
|
+
}
|
|
39
67
|
function translateAdministrationError(error) {
|
|
40
68
|
if (error instanceof AdminRoleRequiredError) {
|
|
41
|
-
throw new ClientError('Administrator
|
|
69
|
+
throw new ClientError('Administrator role required', 'forbidden', {
|
|
42
70
|
status: 403,
|
|
43
71
|
details: {
|
|
44
72
|
required_role: error.minimumRole
|
|
@@ -106,21 +134,82 @@ const bootstrapRoute = defineRoute({
|
|
|
106
134
|
return toAdminGrantDto(grant);
|
|
107
135
|
}
|
|
108
136
|
});
|
|
137
|
+
const bootstrapStateRoute = defineRoute({
|
|
138
|
+
method: 'GET',
|
|
139
|
+
path: '/bootstrap-state',
|
|
140
|
+
description: 'Read whether first administrator owner bootstrap is available.',
|
|
141
|
+
schema: {
|
|
142
|
+
response: {
|
|
143
|
+
200: adminBootstrapStateSchema
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),
|
|
147
|
+
errorHandler: translateAdministrationError,
|
|
148
|
+
handler: async ()=>({
|
|
149
|
+
state: await getAdminBootstrapState()
|
|
150
|
+
})
|
|
151
|
+
});
|
|
109
152
|
const listRoute = defineRoute({
|
|
110
153
|
method: 'GET',
|
|
111
154
|
path: '/',
|
|
112
|
-
description: 'List local administrator
|
|
155
|
+
description: 'List bounded local administrator grant summaries.',
|
|
113
156
|
schema: {
|
|
157
|
+
querystring: listAdminGrantsQuerySchema,
|
|
114
158
|
response: {
|
|
115
159
|
200: listAdminGrantsResponseSchema
|
|
116
160
|
}
|
|
117
161
|
},
|
|
118
162
|
errorHandler: translateAdministrationError,
|
|
119
|
-
handler: async (request)=>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
163
|
+
handler: async (request)=>{
|
|
164
|
+
const { limit, cursor } = request.query;
|
|
165
|
+
const decodedCursor = decodeTimestampIdCursor(cursor);
|
|
166
|
+
if (cursor && !decodedCursor) {
|
|
167
|
+
throw new ClientError('Invalid cursor', 'invalid-cursor', {
|
|
168
|
+
status: 400
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const result = await listAdministratorGrantSummaries({
|
|
172
|
+
actorId: requireActorId(request),
|
|
173
|
+
limit,
|
|
174
|
+
...decodedCursor ? {
|
|
175
|
+
cursor: decodedCursor
|
|
176
|
+
} : {}
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
grants: result.grants.map(toAdministratorGrantSummaryDto),
|
|
180
|
+
next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const userLookupRoute = defineRoute({
|
|
185
|
+
method: 'GET',
|
|
186
|
+
path: '/',
|
|
187
|
+
description: 'Find one administrator-safe user summary by exact ID or email.',
|
|
188
|
+
schema: {
|
|
189
|
+
querystring: administratorUserLookupQuerySchema,
|
|
190
|
+
response: {
|
|
191
|
+
200: administratorUserSummarySchema
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
preHandler: [
|
|
195
|
+
requireAdministratorObserver,
|
|
196
|
+
createAuthIpRateLimitPreHandler('lookup')
|
|
197
|
+
],
|
|
198
|
+
errorHandler: translateAdministrationError,
|
|
199
|
+
handler: async (request)=>{
|
|
200
|
+
const { id, user_id: userId, email } = request.query;
|
|
201
|
+
const lookupId = id ?? userId;
|
|
202
|
+
const actorId = requireActorId(request);
|
|
203
|
+
const user = lookupId ? await findAdministratorUserSummary({
|
|
204
|
+
actorId,
|
|
205
|
+
id: lookupId
|
|
206
|
+
}) : email ? await findAdministratorUserSummary({
|
|
207
|
+
actorId,
|
|
208
|
+
email
|
|
209
|
+
}) : undefined;
|
|
210
|
+
if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');
|
|
211
|
+
return toAdministratorUserSummaryDto(user);
|
|
212
|
+
}
|
|
124
213
|
});
|
|
125
214
|
const grantRoute = defineRoute({
|
|
126
215
|
method: 'POST',
|
|
@@ -173,7 +262,7 @@ const revokeRoute = defineRoute({
|
|
|
173
262
|
}
|
|
174
263
|
});
|
|
175
264
|
export const administrationRoutes = {
|
|
176
|
-
prefix: '/admin/
|
|
265
|
+
prefix: '/admin/auth/admin-grants',
|
|
177
266
|
auth: AUTH_USER,
|
|
178
267
|
routes: [
|
|
179
268
|
bootstrapRoute,
|
|
@@ -182,5 +271,19 @@ export const administrationRoutes = {
|
|
|
182
271
|
revokeRoute
|
|
183
272
|
]
|
|
184
273
|
};
|
|
274
|
+
export const administrationBootstrapRoutes = {
|
|
275
|
+
prefix: '/admin/auth',
|
|
276
|
+
auth: AUTH_USER,
|
|
277
|
+
routes: [
|
|
278
|
+
bootstrapStateRoute
|
|
279
|
+
]
|
|
280
|
+
};
|
|
281
|
+
export const administrationUserRoutes = {
|
|
282
|
+
prefix: '/admin/auth/users',
|
|
283
|
+
auth: AUTH_USER,
|
|
284
|
+
routes: [
|
|
285
|
+
userLookupRoute
|
|
286
|
+
]
|
|
287
|
+
};
|
|
185
288
|
|
|
186
289
|
//# sourceMappingURL=administration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsResponseSchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {\n bootstrapFirstAdminOwner,\n grantAdministratorRole,\n listAdministratorGrants,\n revokeAdministratorGrant,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.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\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 translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator owner 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 local administrator grants.',\n schema: {response: {200: listAdminGrantsResponseSchema}},\n errorHandler: translateAdministrationError,\n handler: async (request) => ({\n grants: (await listAdministratorGrants({actorId: requireActorId(request)})).map(\n toAdminGrantDto,\n ),\n }),\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = {\n prefix: '/admin/v1/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n};\n"],"names":["AUTH_USER","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsResponseSchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","ClientError","defineRoute","z","bootstrapFirstAdminOwner","grantAdministratorRole","listAdministratorGrants","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","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","translateAdministrationError","error","details","required_role","minimumRole","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","correlationId","code","listRoute","grants","map","grantRoute","reason","revokeRoute","params","object","grantId","string","uuid","administrationRoutes","prefix","auth","routes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {AUTH_USER} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n} from '@shipfox/api-auth-dto';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n listAdministratorGrantSummaries,\n revokeAdministratorGrant,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = {\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n};\n\nexport const administrationBootstrapRoutes: RouteGroup = {\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n};\n\nexport const administrationUserRoutes: RouteGroup = {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute],\n};\n"],"names":["AUTH_USER","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","listAdministratorGrantSummaries","revokeAdministratorGrant","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","getClientContext","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","correlationId","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","result","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","grantRoute","reason","revokeRoute","params","object","string","uuid","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","administrationUserRoutes"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EACL,KAAK,mBAAmB,EAMzB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EACL,KAAK,mBAAmB,EAMzB,MAAM,qBAAqB,CAAC;AAyB7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgFD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,IACzD,SAAS,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,IAC3D,SAAS,cAAc,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E"}
|
|
@@ -26,6 +26,18 @@ const policies = {
|
|
|
26
26
|
limit: 5,
|
|
27
27
|
windowSeconds: 15 * 60
|
|
28
28
|
}
|
|
29
|
+
},
|
|
30
|
+
'bootstrap-state': {
|
|
31
|
+
ip: {
|
|
32
|
+
limit: 60,
|
|
33
|
+
windowSeconds: 5 * 60
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
lookup: {
|
|
37
|
+
ip: {
|
|
38
|
+
limit: 60,
|
|
39
|
+
windowSeconds: 5 * 60
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
};
|
|
31
43
|
function routeName(request) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {\n type AuthRateLimitAction,\n AuthRateLimitExceededError,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n AuthRateLimitUnavailableError,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n};\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","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":"AAAA,SAAQA,WAAW,QAA+C,wBAAwB;AAC1F,SAEEC,0BAA0B,EAG1BC,6BAA6B,EAC7BC,kBAAkB,QACb,sBAAsB;AAE7B,MAAMC,WAGF;IACFC,OAAO;QACLC,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;QACrCC,OAAO;YAACF,OAAO;YAAIC,eAAe,KAAK;QAAE;IAC3C;IACA,cAAc;QACZF,IAAI;YAACC,OAAO;YAAIC,eAAe,KAAK;QAAE;QACtCC,OAAO;YAACF,OAAO;YAAGC,eAAe,KAAK;QAAE;IAC1C;IACAE,WAAW;QACTJ,IAAI;YAACC,OAAO;YAAGC,eAAe,KAAK;QAAE;IACvC;AACF;AAMA,
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {\n type AuthRateLimitAction,\n AuthRateLimitExceededError,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n AuthRateLimitUnavailableError,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n try {\n await checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n });\n } catch (error) {\n if (error instanceof AuthRateLimitExceededError) {\n params.request.log.warn(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n retryAfterSeconds: error.retryAfterSeconds,\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n 'Auth rate limit blocked request',\n );\n params.reply.header('Retry-After', String(error.retryAfterSeconds));\n throw new ClientError('Rate limit exceeded', 'rate-limited', {\n status: 429,\n details: {retry_after_seconds: error.retryAfterSeconds},\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n });\n }\n\n if (error instanceof AuthRateLimitUnavailableError) {\n params.request.log.error(\n {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n err: error,\n },\n 'Auth rate limiter unavailable',\n );\n throw new ClientError(\n 'Authentication rate limiter unavailable',\n 'auth-rate-limit-unavailable',\n {\n status: 503,\n data: {\n action: error.action,\n scope: error.scope,\n route: routeName(params.request),\n identifierHmacPrefix: error.identifierHmacPrefix,\n },\n cause: error,\n },\n );\n }\n\n throw error;\n }\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n"],"names":["ClientError","AuthRateLimitExceededError","AuthRateLimitUnavailableError","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","routeName","request","routeOptions","url","split","enforceRateLimit","params","policy","action","scope","identifier","error","log","warn","route","retryAfterSeconds","identifierHmacPrefix","reply","header","String","status","details","retry_after_seconds","data","cause","err","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler"],"mappings":"AAAA,SAAQA,WAAW,QAA+C,wBAAwB;AAC1F,SAEEC,0BAA0B,EAG1BC,6BAA6B,EAC7BC,kBAAkB,QACb,sBAAsB;AAE7B,MAAMC,WAGF;IACFC,OAAO;QACLC,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;QACrCC,OAAO;YAACF,OAAO;YAAIC,eAAe,KAAK;QAAE;IAC3C;IACA,cAAc;QACZF,IAAI;YAACC,OAAO;YAAIC,eAAe,KAAK;QAAE;QACtCC,OAAO;YAACF,OAAO;YAAGC,eAAe,KAAK;QAAE;IAC1C;IACAE,WAAW;QACTJ,IAAI;YAACC,OAAO;YAAGC,eAAe,KAAK;QAAE;IACvC;IACA,mBAAmB;QACjBF,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;IACvC;IACAG,QAAQ;QACNL,IAAI;YAACC,OAAO;YAAIC,eAAe,IAAI;QAAE;IACvC;AACF;AAMA,SAASI,UAAUC,OAAuB;IACxC,OAAOA,QAAQC,YAAY,CAACC,GAAG,IAAIF,QAAQE,GAAG,CAACC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI;AAClE;AAEA,eAAeC,iBAAiBC,MAM/B;IACC,MAAMC,SAASf,QAAQ,CAACc,OAAOE,MAAM,CAAC,CAACF,OAAOG,KAAK,CAAC;IACpD,IAAI,CAACF,QAAQ;IAEb,IAAI;QACF,MAAMhB,mBAAmB;YACvBiB,QAAQF,OAAOE,MAAM;YACrBC,OAAOH,OAAOG,KAAK;YACnBC,YAAYJ,OAAOI,UAAU;YAC7B,GAAGH,MAAM;QACX;IACF,EAAE,OAAOI,OAAO;QACd,IAAIA,iBAAiBtB,4BAA4B;YAC/CiB,OAAOL,OAAO,CAACW,GAAG,CAACC,IAAI,CACrB;gBACEL,QAAQG,MAAMH,MAAM;gBACpBC,OAAOE,MAAMF,KAAK;gBAClBK,OAAOd,UAAUM,OAAOL,OAAO;gBAC/Bc,mBAAmBJ,MAAMI,iBAAiB;gBAC1CC,sBAAsBL,MAAMK,oBAAoB;YAClD,GACA;YAEFV,OAAOW,KAAK,CAACC,MAAM,CAAC,eAAeC,OAAOR,MAAMI,iBAAiB;YACjE,MAAM,IAAI3B,YAAY,uBAAuB,gBAAgB;gBAC3DgC,QAAQ;gBACRC,SAAS;oBAACC,qBAAqBX,MAAMI,iBAAiB;gBAAA;gBACtDQ,MAAM;oBACJf,QAAQG,MAAMH,MAAM;oBACpBC,OAAOE,MAAMF,KAAK;oBAClBK,OAAOd,UAAUM,OAAOL,OAAO;oBAC/Be,sBAAsBL,MAAMK,oBAAoB;gBAClD;gBACAQ,OAAOb;YACT;QACF;QAEA,IAAIA,iBAAiBrB,+BAA+B;YAClDgB,OAAOL,OAAO,CAACW,GAAG,CAACD,KAAK,CACtB;gBACEH,QAAQG,MAAMH,MAAM;gBACpBC,OAAOE,MAAMF,KAAK;gBAClBK,OAAOd,UAAUM,OAAOL,OAAO;gBAC/Be,sBAAsBL,MAAMK,oBAAoB;gBAChDS,KAAKd;YACP,GACA;YAEF,MAAM,IAAIvB,YACR,2CACA,+BACA;gBACEgC,QAAQ;gBACRG,MAAM;oBACJf,QAAQG,MAAMH,MAAM;oBACpBC,OAAOE,MAAMF,KAAK;oBAClBK,OAAOd,UAAUM,OAAOL,OAAO;oBAC/Be,sBAAsBL,MAAMK,oBAAoB;gBAClD;gBACAQ,OAAOb;YACT;QAEJ;QAEA,MAAMA;IACR;AACF;AAEA,OAAO,SAASe,8BAA8BlB,MAA2B;IACvE,OAAO,OAAOP,SAA4CgB;QACxD,MAAMZ,iBAAiB;YACrBJ;YACAgB;YACAT;YACAC,OAAO;YACPC,YAAYT,QAAQP,EAAE;QACxB;QAEA,MAAMW,iBAAiB;YACrBJ;YACAgB;YACAT;YACAC,OAAO;YACPC,YAAYT,QAAQ0B,IAAI,CAAC9B,KAAK;QAChC;IACF;AACF;AAEA,OAAO,SAAS+B,gCAAgCpB,MAA2B;IACzE,OAAO,OAAOP,SAAyBgB;QACrC,MAAMZ,iBAAiB;YACrBJ;YACAgB;YACAT;YACAC,OAAO;YACPC,YAAYT,QAAQP,EAAE;QACxB;IACF;AACF"}
|