@shipfox/api-auth 10.1.0 → 11.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 +30 -0
- package/README.md +42 -11
- package/dist/core/administration.d.ts +17 -1
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +107 -14
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +27 -10
- package/dist/core/auth.js.map +1 -1
- package/dist/core/jwt.d.ts +10 -0
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js +3 -2
- package/dist/core/jwt.js.map +1 -1
- package/dist/db/admin-command.d.ts +18 -0
- package/dist/db/admin-command.d.ts.map +1 -0
- package/dist/db/admin-command.js +37 -0
- package/dist/db/admin-command.js.map +1 -0
- package/dist/db/admin-grants.d.ts +1 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +12 -30
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts +29 -0
- package/dist/db/admin-user-moderation.d.ts.map +1 -0
- package/dist/db/admin-user-moderation.js +156 -0
- package/dist/db/admin-user-moderation.js.map +1 -0
- package/dist/db/admin-user-summary.d.ts +14 -0
- package/dist/db/admin-user-summary.d.ts.map +1 -0
- package/dist/db/admin-user-summary.js +31 -0
- package/dist/db/admin-user-summary.js.map +1 -0
- package/dist/db/admin-users.d.ts +2 -11
- package/dist/db/admin-users.d.ts.map +1 -1
- package/dist/db/admin-users.js +2 -27
- package/dist/db/admin-users.js.map +1 -1
- package/dist/db/refresh-tokens.d.ts +6 -4
- package/dist/db/refresh-tokens.d.ts.map +1 -1
- package/dist/db/refresh-tokens.js +31 -6
- package/dist/db/refresh-tokens.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +19 -2
- package/dist/db/schema/admin-command-results.d.ts.map +1 -1
- package/dist/db/schema/admin-command-results.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.d.ts +1 -0
- package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.js +4 -3
- package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts +1 -4
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +23 -19
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +94 -3
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/administration.ts +137 -15
- package/src/core/auth.test.ts +29 -10
- package/src/core/auth.ts +36 -14
- package/src/core/jwt.test.ts +20 -4
- package/src/core/jwt.ts +2 -1
- package/src/db/admin-command.ts +85 -0
- package/src/db/admin-grants.ts +19 -57
- package/src/db/admin-user-moderation.ts +230 -0
- package/src/db/admin-user-summary.ts +51 -0
- package/src/db/admin-users.ts +4 -49
- package/src/db/refresh-tokens.test.ts +0 -19
- package/src/db/refresh-tokens.ts +54 -23
- package/src/db/schema/admin-command-results.ts +23 -2
- package/src/index.ts +3 -0
- package/src/presentation/auth/bearer-token-auth.test.ts +17 -0
- package/src/presentation/auth/bearer-token-auth.ts +9 -2
- package/src/presentation/auth/jwt-auth.test.ts +34 -9
- package/src/presentation/auth/jwt-auth.ts +19 -13
- package/src/presentation/routes/administration.test.ts +253 -2
- package/src/presentation/routes/administration.ts +88 -1
- package/src/presentation/routes/password/change-password.test.ts +6 -0
- package/src/presentation/routes/password/password-reset-confirm.test.ts +14 -0
- package/src/presentation/routes/registration/signup.test.ts +2 -2
- package/src/presentation/routes/session/logout.test.ts +6 -0
- package/src/presentation/routes/session/refresh.test.ts +11 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/dist/core/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/auth.ts"],"sourcesContent":["import {type AdminRole, emailSchema} from '@shipfox/api-auth-dto';\nimport {\n confirmEmailChallenge,\n consumeEmailChallengeProof,\n createEmailChallenge,\n resendEmailChallenge,\n} from '@shipfox/api-email-challenges';\nimport {\n type WorkspacesInterModuleClient,\n workspacesInterModuleContract,\n} from '@shipfox/api-workspaces-dto/inter-module';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {userAccessTokenKey} from '@shipfox/node-auth-root-key';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {consumePasswordReset, createPasswordReset} from '#db/password-resets.js';\nimport {\n createRefreshToken,\n findActiveRefreshTokenByHash,\n findRefreshTokenByHash,\n revokeRefreshSession,\n revokeRefreshTokensForUser,\n rotateRefreshToken,\n} from '#db/refresh-tokens.js';\nimport {\n createUser as createDbUser,\n findUserByEmail,\n findUserById,\n markEmailVerified,\n provisionUser as provisionDbUser,\n updateUserPassword,\n} from '#db/users.js';\nimport {type AuthTokenRefreshOutcome, recordTokenRefreshed} from '#metrics/index.js';\nimport {getCurrentAdminRole} from './admin-role.js';\nimport type {RefreshToken} from './entities/refresh-token.js';\nimport type {User} from './entities/user.js';\nimport {\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n EmailTakenError,\n InvalidCredentialsError,\n InvitationEmailMismatchError,\n SignupNotAllowedError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n UserNotFoundError,\n} from './errors.js';\nimport {signUserToken} from './jwt.js';\nimport {hashPassword, verifyPassword} from './password.js';\nimport type {SignupPolicy} from './ports.js';\nimport {createEnvironmentSignupPolicy} from './signup-policy.js';\n\nconst RESET_TTL_HOURS = 1;\nconst PASSWORD_VERIFICATION_PURPOSE = 'password-verification';\nconst DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE =\n 'This Shipfox deployment does not accept new accounts right now.';\n\nconst defaultSignupPolicy: SignupPolicy = createEnvironmentSignupPolicy();\n\nasync function assertSignupAllowed(params: {\n signupPolicy?: SignupPolicy | undefined;\n email: string;\n emailVerified: boolean;\n source: string;\n}): Promise<void> {\n const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({\n email: params.email,\n emailVerified: params.emailVerified,\n source: params.source,\n });\n\n if (!result.allowed) {\n throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);\n }\n}\n\nlet dummyHashCache: string | undefined;\nasync function getDummyHash(): Promise<string> {\n if (!dummyHashCache) {\n dummyHashCache = await hashPassword({password: 'dummy-for-timing-parity'});\n }\n return dummyHashCache;\n}\n\nfunction hoursFromNow(hours: number): Date {\n return new Date(Date.now() + hours * 60 * 60 * 1000);\n}\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction recordRefreshOutcome(outcome: AuthTokenRefreshOutcome): void {\n recordTokenRefreshed(outcome);\n}\n\nasync function signAccessToken(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n refreshSessionId: string,\n): Promise<string> {\n const memberships = await workspaces\n .listMembershipsForTokenClaims({userId: user.id})\n .catch((error: unknown) => {\n throw new AuthDependencyUnavailableError('workspaces', error);\n });\n return await signUserToken({\n userId: user.id,\n email: user.email,\n name: user.name,\n memberships: memberships.memberships,\n refreshSessionId,\n secret: userAccessTokenKey(),\n expiresIn: config.AUTH_JWT_EXPIRES_IN,\n });\n}\n\nfunction isWithinRotationGrace(refreshToken: RefreshToken): boolean {\n if (!refreshToken.rotatedAt) return false;\n const graceMs = config.AUTH_REFRESH_ROTATION_GRACE_SECONDS * 1000;\n return Date.now() - refreshToken.rotatedAt.getTime() <= graceMs;\n}\n\nasync function createRefreshSession(\n user: User,\n refreshSessionId: string,\n): Promise<{refreshToken: string; refreshSessionId: string}> {\n const refreshToken = generateOpaqueToken('refreshToken');\n const session = await createRefreshToken({\n sessionId: refreshSessionId,\n userId: user.id,\n hashedToken: hashOpaqueToken(refreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n return {refreshToken, refreshSessionId: session.sessionId};\n}\n\nasync function createSessionTokens(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n): Promise<{token: string; refreshToken: string; adminRole: AdminRole | null}> {\n const refreshSessionId = crypto.randomUUID();\n const token = await signAccessToken(user, workspaces, refreshSessionId);\n const adminRole = await getCurrentAdminRole({userId: user.id});\n const {refreshToken} = await createRefreshSession(user, refreshSessionId);\n return {token, refreshToken, adminRole};\n}\n\nfunction passwordResetLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;\n}\n\nfunction invitationErrorFromKnownError(\n error: unknown,\n method: 'preflightInvitationAcceptance' | 'acceptInvitation',\n): Error {\n const contractMethod = workspacesInterModuleContract.methods[method];\n if (!isInterModuleKnownError(contractMethod, error)) return error as Error;\n\n if (error.code === 'invitation-token-invalid')\n return new TokenInvalidError('Invitation token is invalid');\n if (error.code === 'invitation-token-used') return new TokenAlreadyUsedError();\n if (error.code === 'invitation-token-expired') return new TokenExpiredError();\n return new InvitationEmailMismatchError();\n}\n\nfunction invitationErrorMessage(\n code:\n | 'invitation-token-invalid'\n | 'invitation-token-used'\n | 'invitation-token-expired'\n | 'invitation-email-mismatch',\n): string {\n if (code === 'invitation-token-invalid') return 'Invitation token is invalid';\n if (code === 'invitation-token-used') return 'Invitation has already been accepted';\n if (code === 'invitation-token-expired') return 'Invitation has expired';\n return 'Signup email does not match the invitation';\n}\n\nexport interface SignupParams {\n email: string;\n password: string;\n name?: string | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\nexport interface ProvisionUserParams {\n email: string;\n name?: string | null | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\n/**\n * Creates a verified, password-less user for an external identity provider.\n * Existing users are returned unchanged, including their password and profile.\n */\nexport async function provisionUser(params: ProvisionUserParams): Promise<User> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) return existing;\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: true,\n source: 'external-identity',\n });\n\n return await provisionDbUser({\n email,\n name: params.name ?? null,\n });\n}\n\nexport type SignupResult = User & {\n emailChallenge: {id: string; nextResendAvailableAt: Date};\n};\n\nexport async function signup(params: SignupParams & {sourceIp?: string}): Promise<SignupResult> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) {\n const canResumeVerification =\n existing.status === 'active' &&\n existing.emailVerifiedAt === null &&\n existing.hashedPassword !== null &&\n (await verifyPassword({password: params.password, hash: existing.hashedPassword}));\n if (canResumeVerification) {\n const emailChallenge = await createEmailChallenge({\n email: existing.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: existing.id,\n idempotencyKey: existing.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n return {...existing, emailChallenge};\n }\n throw new EmailTakenError(email);\n }\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: false,\n source: 'password',\n });\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email,\n hashedPassword,\n name: params.name ?? null,\n signedUp: {viaInvitation: false},\n });\n\n const emailChallenge = await createEmailChallenge({\n email: user.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n idempotencyKey: user.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n\n return {...user, emailChallenge};\n}\n\nexport interface SignupWithInvitationParams extends SignupParams {\n invitationToken: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface SignupWithInvitationResult extends LoginResult {\n membership: {id: string; userId: string; workspaceId: string} | null;\n acceptError?: {code: string; message: string};\n}\n\nexport async function signupWithInvitation(\n params: SignupWithInvitationParams,\n): Promise<SignupWithInvitationResult> {\n // Step 1: Pre-validate the invitation BEFORE any user write so an invalid\n // token never produces an orphan user. Re-validation happens again inside\n // acceptWorkspaceInvitation (race-safe) after the user is created.\n try {\n await params.workspaces.preflightInvitationAcceptance({\n token: params.invitationToken,\n email: params.email,\n });\n } catch (error) {\n throw invitationErrorFromKnownError(error, 'preflightInvitationAcceptance');\n }\n\n // Step 2: Create the user as verified. The invitation email is the proof of\n // ownership, and this avoids a second auth-table write just to mark verified.\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: new Date(),\n signedUp: {viaInvitation: true},\n });\n\n // Step 3: Accept the invitation through the workspaces module boundary.\n // This is intentionally not one database transaction with auth; treat it like\n // calling another service. If it fails after user creation, return a verified\n // signed-in user and an explicit retryable accept error.\n let membership: SignupWithInvitationResult['membership'] = null;\n let acceptError: SignupWithInvitationResult['acceptError'];\n try {\n const result = await params.workspaces.acceptInvitation({\n token: params.invitationToken,\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n membership = {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n };\n } catch (error) {\n if (isInterModuleKnownError(workspacesInterModuleContract.methods.acceptInvitation, error)) {\n acceptError = {code: error.code, message: invitationErrorMessage(error.code)};\n } else {\n acceptError = {\n code: 'AcceptFailed',\n message: 'Could not accept the invitation; please retry from the invite link.',\n };\n }\n }\n\n // Step 4: Issue session. signAccessToken reads memberships through the\n // workspaces module API, so a successful accept is reflected in the JWT.\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n if (acceptError) {\n return {token, refreshToken, user, membership, acceptError, adminRole};\n }\n return {token, refreshToken, user, membership, adminRole};\n}\n\nexport interface CreateUserParams extends SignupParams {\n verified: boolean;\n}\n\nexport async function createUser(params: CreateUserParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: params.verified ? new Date() : null,\n });\n\n return user;\n}\n\nexport interface LoginParams {\n email: string;\n password: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface LoginResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function login(params: LoginParams): Promise<LoginResult> {\n const user = await findUserByEmail({email: params.email});\n\n if (!user) {\n await verifyPassword({password: params.password, hash: await getDummyHash()});\n throw new InvalidCredentialsError();\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.password,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok || user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface CreateSessionForUserParams {\n userId?: string | undefined;\n email?: string | undefined;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface CreateSessionForUserResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport type CreateSessionForUserError =\n | AuthDependencyUnavailableError\n | EmailNotVerifiedError\n | InvalidCredentialsError\n | UserNotFoundError;\n\nexport async function createSessionForUser(\n params: CreateSessionForUserParams,\n): Promise<CreateSessionForUserResult> {\n const user = params.userId\n ? await findUserById({id: params.userId})\n : params.email\n ? await findUserByEmail({email: emailSchema.parse(params.email)})\n : undefined;\n\n if (!user) {\n throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');\n }\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n if (user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface RefreshAccessTokenResult {\n token: string;\n /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */\n refreshToken: string | undefined;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function refreshAccessToken(params: {\n refreshToken: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<RefreshAccessTokenResult> {\n const currentHashedToken = hashOpaqueToken(params.refreshToken);\n const current = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!current) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n const user = await findUserById({id: current.userId});\n if (user?.status !== 'active') {\n await revokeRefreshSession({sessionId: current.sessionId, userId: current.userId});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const adminRole = await getCurrentAdminRole({userId: user.id});\n\n // Within the grace window a rotated token means a concurrent refresh (e.g. a\n // second tab); past it, reuse of a retired token means a compromised session.\n if (current.rotatedAt) {\n if (isWithinRotationGrace(current)) {\n const token = await signAccessToken(user, params.workspaces, current.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n await revokeRefreshTokensForUser({userId: user.id});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token reused after rotation');\n }\n\n // Losing the CAS requires a state check: another request may have rotated,\n // revoked, or expired the token before this refresh could claim it.\n const nextRefreshToken = generateOpaqueToken('refreshToken');\n const rotated = await rotateRefreshToken({\n id: current.id,\n currentHashedToken,\n nextHashedToken: hashOpaqueToken(nextRefreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!rotated) {\n const latest = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!latest || !isWithinRotationGrace(latest)) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const token = await signAccessToken(user, params.workspaces, latest.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n\n const token = await signAccessToken(user, params.workspaces, current.sessionId);\n recordRefreshOutcome('rotated');\n return {token, refreshToken: nextRefreshToken, user, adminRole};\n}\n\nexport interface ConfirmEmailVerificationResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport interface ResendEmailVerificationResult {\n nextResendAvailableAt: Date;\n}\n\nexport async function confirmEmailVerification(params: {\n email: string;\n challengeId: string;\n code: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user) throw new TokenInvalidError('Verification code is invalid or expired');\n await confirmEmailChallenge({id: params.challengeId, code: params.code, continuation: user.id});\n await consumeEmailChallengeProof({\n id: params.challengeId,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n });\n\n const verifiedUser = await markEmailVerified({userId: user.id});\n if (verifiedUser?.status !== 'active') {\n throw new TokenInvalidError('Verification code is invalid or expired');\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(\n verifiedUser,\n params.workspaces,\n );\n\n return {token, refreshToken, user: verifiedUser, adminRole};\n}\n\nexport async function resendEmailVerification(params: {\n email: string;\n challengeId: string;\n sourceIp: string;\n}): Promise<ResendEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user || user.emailVerifiedAt || user.status !== 'active') {\n return {nextResendAvailableAt: new Date()};\n }\n const result = await resendEmailChallenge({\n id: params.challengeId,\n continuation: user.id,\n sourceIp: params.sourceIp,\n });\n\n return {nextResendAvailableAt: result.nextResendAvailableAt};\n}\n\nexport async function requestPasswordReset(params: {email: string}): Promise<void> {\n const user = await findUserByEmail({email: params.email});\n if (user?.status !== 'active' || user.hashedPassword === null) {\n return;\n }\n\n const rawToken = generateOpaqueToken('passwordReset');\n await createPasswordReset({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(RESET_TTL_HOURS),\n sendEmail: {\n email: user.email,\n resetLink: passwordResetLink(rawToken),\n expiresInHours: RESET_TTL_HOURS,\n },\n });\n}\n\nexport interface ConfirmPasswordResetResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function confirmPasswordReset(params: {\n token: string;\n newPassword: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmPasswordResetResult> {\n const consumed = await consumePasswordReset({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const existingUser = await findUserById({id: consumed.userId});\n if (existingUser?.status !== 'active' || existingUser.hashedPassword === null) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const user = await updateUserPassword({userId: consumed.userId, hashedPassword});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n await revokeRefreshTokensForUser({userId: consumed.userId});\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport async function changePassword(params: {\n userId: string;\n currentPassword: string;\n newPassword: string;\n refreshToken?: string | undefined;\n}): Promise<void> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.currentPassword,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok) {\n throw new InvalidCredentialsError();\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const currentRefreshSession = params.refreshToken\n ? await findActiveRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)})\n : undefined;\n const exceptRefreshTokenId =\n currentRefreshSession?.userId === user.id ? currentRefreshSession.id : undefined;\n\n await updateUserPassword({userId: user.id, hashedPassword});\n await revokeRefreshTokensForUser({userId: user.id, exceptRefreshTokenId});\n}\n\nexport async function logout(params: {refreshToken?: string | undefined}): Promise<void> {\n if (!params.refreshToken) return;\n const refreshToken = await findRefreshTokenByHash({\n hashedToken: hashOpaqueToken(params.refreshToken),\n });\n if (!refreshToken) return;\n await revokeRefreshSession({sessionId: refreshToken.sessionId, userId: refreshToken.userId});\n}\n\nexport interface GetCurrentUserResult {\n user: User;\n}\n\nexport async function getCurrentUser(params: {userId: string}): Promise<GetCurrentUserResult> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n return {user};\n}\n"],"names":["emailSchema","confirmEmailChallenge","consumeEmailChallengeProof","createEmailChallenge","resendEmailChallenge","workspacesInterModuleContract","isInterModuleKnownError","userAccessTokenKey","generateOpaqueToken","hashOpaqueToken","config","consumePasswordReset","createPasswordReset","createRefreshToken","findActiveRefreshTokenByHash","findRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","rotateRefreshToken","createUser","createDbUser","findUserByEmail","findUserById","markEmailVerified","provisionUser","provisionDbUser","updateUserPassword","recordTokenRefreshed","getCurrentAdminRole","AuthDependencyUnavailableError","EmailNotVerifiedError","EmailTakenError","InvalidCredentialsError","InvitationEmailMismatchError","SignupNotAllowedError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","UserNotFoundError","signUserToken","hashPassword","verifyPassword","createEnvironmentSignupPolicy","RESET_TTL_HOURS","PASSWORD_VERIFICATION_PURPOSE","DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE","defaultSignupPolicy","assertSignupAllowed","params","result","signupPolicy","isSignupAllowed","email","emailVerified","source","allowed","message","dummyHashCache","getDummyHash","password","hoursFromNow","hours","Date","now","daysFromNow","days","recordRefreshOutcome","outcome","signAccessToken","user","workspaces","refreshSessionId","memberships","listMembershipsForTokenClaims","userId","id","catch","error","name","secret","expiresIn","AUTH_JWT_EXPIRES_IN","isWithinRotationGrace","refreshToken","rotatedAt","graceMs","AUTH_REFRESH_ROTATION_GRACE_SECONDS","getTime","createRefreshSession","session","sessionId","hashedToken","expiresAt","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","createSessionTokens","crypto","randomUUID","token","adminRole","passwordResetLink","rawToken","CLIENT_BASE_URL","invitationErrorFromKnownError","method","contractMethod","methods","code","invitationErrorMessage","parse","existing","signup","canResumeVerification","status","emailVerifiedAt","hashedPassword","hash","emailChallenge","purpose","continuation","idempotencyKey","sourceIp","signedUp","viaInvitation","signupWithInvitation","preflightInvitationAcceptance","invitationToken","membership","acceptError","acceptInvitation","workspaceId","verified","login","hasPassword","ok","createSessionForUser","undefined","refreshAccessToken","currentHashedToken","current","nextRefreshToken","rotated","nextHashedToken","latest","confirmEmailVerification","challengeId","verifiedUser","resendEmailVerification","nextResendAvailableAt","requestPasswordReset","sendEmail","resetLink","expiresInHours","confirmPasswordReset","consumed","existingUser","newPassword","changePassword","currentPassword","currentRefreshSession","exceptRefreshTokenId","logout","getCurrentUser"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/auth.ts"],"sourcesContent":["import {type AdminRole, emailSchema} from '@shipfox/api-auth-dto';\nimport {\n confirmEmailChallenge,\n consumeEmailChallengeProof,\n createEmailChallenge,\n resendEmailChallenge,\n} from '@shipfox/api-email-challenges';\nimport {\n type WorkspacesInterModuleClient,\n workspacesInterModuleContract,\n} from '@shipfox/api-workspaces-dto/inter-module';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {userAccessTokenKey} from '@shipfox/node-auth-root-key';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {consumePasswordReset, createPasswordReset} from '#db/password-resets.js';\nimport {\n createRefreshTokenForActiveUser,\n findActiveRefreshTokenByHash,\n findRefreshTokenByHash,\n revokeRefreshSession,\n revokeRefreshTokensForUser,\n rotateRefreshToken,\n} from '#db/refresh-tokens.js';\nimport {\n createUser as createDbUser,\n findUserByEmail,\n findUserById,\n markEmailVerified,\n provisionUser as provisionDbUser,\n updateUserPassword,\n} from '#db/users.js';\nimport {type AuthTokenRefreshOutcome, recordTokenRefreshed} from '#metrics/index.js';\nimport {getCurrentAdminRole} from './admin-role.js';\nimport type {RefreshToken} from './entities/refresh-token.js';\nimport type {User} from './entities/user.js';\nimport {\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n EmailTakenError,\n InvalidCredentialsError,\n InvitationEmailMismatchError,\n SignupNotAllowedError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n UserNotFoundError,\n} from './errors.js';\nimport {signUserToken, type TokenMembership} from './jwt.js';\nimport {hashPassword, verifyPassword} from './password.js';\nimport type {SignupPolicy} from './ports.js';\nimport {createEnvironmentSignupPolicy} from './signup-policy.js';\n\nconst RESET_TTL_HOURS = 1;\nconst PASSWORD_VERIFICATION_PURPOSE = 'password-verification';\nconst DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE =\n 'This Shipfox deployment does not accept new accounts right now.';\n\nconst defaultSignupPolicy: SignupPolicy = createEnvironmentSignupPolicy();\n\nasync function assertSignupAllowed(params: {\n signupPolicy?: SignupPolicy | undefined;\n email: string;\n emailVerified: boolean;\n source: string;\n}): Promise<void> {\n const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({\n email: params.email,\n emailVerified: params.emailVerified,\n source: params.source,\n });\n\n if (!result.allowed) {\n throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);\n }\n}\n\nlet dummyHashCache: string | undefined;\nasync function getDummyHash(): Promise<string> {\n if (!dummyHashCache) {\n dummyHashCache = await hashPassword({password: 'dummy-for-timing-parity'});\n }\n return dummyHashCache;\n}\n\nfunction hoursFromNow(hours: number): Date {\n return new Date(Date.now() + hours * 60 * 60 * 1000);\n}\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction recordRefreshOutcome(outcome: AuthTokenRefreshOutcome): void {\n recordTokenRefreshed(outcome);\n}\n\ntype TokenMemberships = TokenMembership[];\n\nasync function loadTokenMemberships(\n userId: string,\n workspaces: WorkspacesInterModuleClient,\n): Promise<TokenMemberships> {\n const memberships = await workspaces\n .listMembershipsForTokenClaims({userId})\n .catch((error: unknown) => {\n throw new AuthDependencyUnavailableError('workspaces', error);\n });\n return memberships.memberships;\n}\n\nasync function signAccessToken(\n user: User,\n memberships: TokenMemberships,\n refreshSessionId: string,\n): Promise<string> {\n return await signUserToken({\n userId: user.id,\n email: user.email,\n name: user.name,\n memberships,\n refreshSessionId,\n secret: userAccessTokenKey(),\n expiresIn: config.AUTH_JWT_EXPIRES_IN,\n });\n}\n\nfunction isWithinRotationGrace(refreshToken: RefreshToken): boolean {\n if (!refreshToken.rotatedAt) return false;\n const graceMs = config.AUTH_REFRESH_ROTATION_GRACE_SECONDS * 1000;\n return Date.now() - refreshToken.rotatedAt.getTime() <= graceMs;\n}\n\nasync function createRefreshSession(\n user: User,\n refreshSessionId: string,\n): Promise<{refreshToken: string; refreshSessionId: string}> {\n const refreshToken = generateOpaqueToken('refreshToken');\n const session = await createRefreshTokenForActiveUser({\n sessionId: refreshSessionId,\n userId: user.id,\n hashedToken: hashOpaqueToken(refreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!session) throw new InvalidCredentialsError();\n return {refreshToken, refreshSessionId: session.sessionId};\n}\n\nasync function createSessionTokens(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n): Promise<{token: string; refreshToken: string; adminRole: AdminRole | null}> {\n const refreshSessionId = crypto.randomUUID();\n const memberships = await loadTokenMemberships(user.id, workspaces);\n const token = await signAccessToken(user, memberships, refreshSessionId);\n const adminRole = await getCurrentAdminRole({userId: user.id});\n const {refreshToken} = await createRefreshSession(user, refreshSessionId);\n return {token, refreshToken, adminRole};\n}\n\nfunction passwordResetLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;\n}\n\nfunction invitationErrorFromKnownError(\n error: unknown,\n method: 'preflightInvitationAcceptance' | 'acceptInvitation',\n): Error {\n const contractMethod = workspacesInterModuleContract.methods[method];\n if (!isInterModuleKnownError(contractMethod, error)) return error as Error;\n\n if (error.code === 'invitation-token-invalid')\n return new TokenInvalidError('Invitation token is invalid');\n if (error.code === 'invitation-token-used') return new TokenAlreadyUsedError();\n if (error.code === 'invitation-token-expired') return new TokenExpiredError();\n return new InvitationEmailMismatchError();\n}\n\nfunction invitationErrorMessage(\n code:\n | 'invitation-token-invalid'\n | 'invitation-token-used'\n | 'invitation-token-expired'\n | 'invitation-email-mismatch',\n): string {\n if (code === 'invitation-token-invalid') return 'Invitation token is invalid';\n if (code === 'invitation-token-used') return 'Invitation has already been accepted';\n if (code === 'invitation-token-expired') return 'Invitation has expired';\n return 'Signup email does not match the invitation';\n}\n\nexport interface SignupParams {\n email: string;\n password: string;\n name?: string | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\nexport interface ProvisionUserParams {\n email: string;\n name?: string | null | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\n/**\n * Creates a verified, password-less user for an external identity provider.\n * Existing users are returned unchanged, including their password and profile.\n */\nexport async function provisionUser(params: ProvisionUserParams): Promise<User> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) return existing;\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: true,\n source: 'external-identity',\n });\n\n return await provisionDbUser({\n email,\n name: params.name ?? null,\n });\n}\n\nexport type SignupResult = User & {\n emailChallenge: {id: string; nextResendAvailableAt: Date};\n};\n\nexport async function signup(params: SignupParams & {sourceIp?: string}): Promise<SignupResult> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) {\n const canResumeVerification =\n existing.status === 'active' &&\n existing.emailVerifiedAt === null &&\n existing.hashedPassword !== null &&\n (await verifyPassword({password: params.password, hash: existing.hashedPassword}));\n if (canResumeVerification) {\n const emailChallenge = await createEmailChallenge({\n email: existing.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: existing.id,\n idempotencyKey: existing.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n return {...existing, emailChallenge};\n }\n throw new EmailTakenError(email);\n }\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: false,\n source: 'password',\n });\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email,\n hashedPassword,\n name: params.name ?? null,\n signedUp: {viaInvitation: false},\n });\n\n const emailChallenge = await createEmailChallenge({\n email: user.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n idempotencyKey: user.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n\n return {...user, emailChallenge};\n}\n\nexport interface SignupWithInvitationParams extends SignupParams {\n invitationToken: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface SignupWithInvitationResult extends LoginResult {\n membership: {id: string; userId: string; workspaceId: string} | null;\n acceptError?: {code: string; message: string};\n}\n\nexport async function signupWithInvitation(\n params: SignupWithInvitationParams,\n): Promise<SignupWithInvitationResult> {\n // Step 1: Pre-validate the invitation BEFORE any user write so an invalid\n // token never produces an orphan user. Re-validation happens again inside\n // acceptWorkspaceInvitation (race-safe) after the user is created.\n try {\n await params.workspaces.preflightInvitationAcceptance({\n token: params.invitationToken,\n email: params.email,\n });\n } catch (error) {\n throw invitationErrorFromKnownError(error, 'preflightInvitationAcceptance');\n }\n\n // Step 2: Create the user as verified. The invitation email is the proof of\n // ownership, and this avoids a second auth-table write just to mark verified.\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: new Date(),\n signedUp: {viaInvitation: true},\n });\n\n // Step 3: Accept the invitation through the workspaces module boundary.\n // This is intentionally not one database transaction with auth; treat it like\n // calling another service. If it fails after user creation, return a verified\n // signed-in user and an explicit retryable accept error.\n let membership: SignupWithInvitationResult['membership'] = null;\n let acceptError: SignupWithInvitationResult['acceptError'];\n try {\n const result = await params.workspaces.acceptInvitation({\n token: params.invitationToken,\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n membership = {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n };\n } catch (error) {\n if (isInterModuleKnownError(workspacesInterModuleContract.methods.acceptInvitation, error)) {\n acceptError = {code: error.code, message: invitationErrorMessage(error.code)};\n } else {\n acceptError = {\n code: 'AcceptFailed',\n message: 'Could not accept the invitation; please retry from the invite link.',\n };\n }\n }\n\n // Step 4: Issue session. createSessionTokens reads memberships through the\n // workspaces module API, so a successful accept is reflected in the JWT.\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n if (acceptError) {\n return {token, refreshToken, user, membership, acceptError, adminRole};\n }\n return {token, refreshToken, user, membership, adminRole};\n}\n\nexport interface CreateUserParams extends SignupParams {\n verified: boolean;\n}\n\nexport async function createUser(params: CreateUserParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: params.verified ? new Date() : null,\n });\n\n return user;\n}\n\nexport interface LoginParams {\n email: string;\n password: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface LoginResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function login(params: LoginParams): Promise<LoginResult> {\n const user = await findUserByEmail({email: params.email});\n\n if (!user) {\n await verifyPassword({password: params.password, hash: await getDummyHash()});\n throw new InvalidCredentialsError();\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.password,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok || user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface CreateSessionForUserParams {\n userId?: string | undefined;\n email?: string | undefined;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface CreateSessionForUserResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport type CreateSessionForUserError =\n | AuthDependencyUnavailableError\n | EmailNotVerifiedError\n | InvalidCredentialsError\n | UserNotFoundError;\n\nexport async function createSessionForUser(\n params: CreateSessionForUserParams,\n): Promise<CreateSessionForUserResult> {\n const user = params.userId\n ? await findUserById({id: params.userId})\n : params.email\n ? await findUserByEmail({email: emailSchema.parse(params.email)})\n : undefined;\n\n if (!user) {\n throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');\n }\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n if (user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface RefreshAccessTokenResult {\n token: string;\n /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */\n refreshToken: string | undefined;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function refreshAccessToken(params: {\n refreshToken: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<RefreshAccessTokenResult> {\n const currentHashedToken = hashOpaqueToken(params.refreshToken);\n const current = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!current) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n const user = await findUserById({id: current.userId});\n if (user?.status !== 'active') {\n await revokeRefreshSession({sessionId: current.sessionId, userId: current.userId});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const adminRole = await getCurrentAdminRole({userId: user.id});\n\n // Within the grace window a rotated token means a concurrent refresh (e.g. a\n // second tab); past it, reuse of a retired token means a compromised session.\n if (current.rotatedAt) {\n if (isWithinRotationGrace(current)) {\n const memberships = await loadTokenMemberships(user.id, params.workspaces);\n const token = await signAccessToken(user, memberships, current.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n await revokeRefreshTokensForUser({userId: user.id});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token reused after rotation');\n }\n\n // Keep the external membership snapshot ahead of rotation so an outage\n // cannot retire a refresh token that the caller can still retry.\n const memberships = await loadTokenMemberships(user.id, params.workspaces);\n\n // Losing the CAS requires a state check: another request may have rotated,\n // revoked, or expired the token before this refresh could claim it.\n const nextRefreshToken = generateOpaqueToken('refreshToken');\n const rotated = await rotateRefreshToken({\n userId: current.userId,\n id: current.id,\n currentHashedToken,\n nextHashedToken: hashOpaqueToken(nextRefreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!rotated) {\n const latestUser = await findUserById({id: current.userId});\n if (latestUser?.status !== 'active') {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const latest = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!latest || !isWithinRotationGrace(latest)) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const token = await signAccessToken(user, memberships, latest.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n\n const token = await signAccessToken(user, memberships, current.sessionId);\n recordRefreshOutcome('rotated');\n return {token, refreshToken: nextRefreshToken, user, adminRole};\n}\n\nexport interface ConfirmEmailVerificationResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport interface ResendEmailVerificationResult {\n nextResendAvailableAt: Date;\n}\n\nexport async function confirmEmailVerification(params: {\n email: string;\n challengeId: string;\n code: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user) throw new TokenInvalidError('Verification code is invalid or expired');\n await confirmEmailChallenge({id: params.challengeId, code: params.code, continuation: user.id});\n await consumeEmailChallengeProof({\n id: params.challengeId,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n });\n\n const verifiedUser = await markEmailVerified({userId: user.id});\n if (verifiedUser?.status !== 'active') {\n throw new TokenInvalidError('Verification code is invalid or expired');\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(\n verifiedUser,\n params.workspaces,\n );\n\n return {token, refreshToken, user: verifiedUser, adminRole};\n}\n\nexport async function resendEmailVerification(params: {\n email: string;\n challengeId: string;\n sourceIp: string;\n}): Promise<ResendEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user || user.emailVerifiedAt || user.status !== 'active') {\n return {nextResendAvailableAt: new Date()};\n }\n const result = await resendEmailChallenge({\n id: params.challengeId,\n continuation: user.id,\n sourceIp: params.sourceIp,\n });\n\n return {nextResendAvailableAt: result.nextResendAvailableAt};\n}\n\nexport async function requestPasswordReset(params: {email: string}): Promise<void> {\n const user = await findUserByEmail({email: params.email});\n if (user?.status !== 'active' || user.hashedPassword === null) {\n return;\n }\n\n const rawToken = generateOpaqueToken('passwordReset');\n await createPasswordReset({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(RESET_TTL_HOURS),\n sendEmail: {\n email: user.email,\n resetLink: passwordResetLink(rawToken),\n expiresInHours: RESET_TTL_HOURS,\n },\n });\n}\n\nexport interface ConfirmPasswordResetResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function confirmPasswordReset(params: {\n token: string;\n newPassword: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmPasswordResetResult> {\n const consumed = await consumePasswordReset({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const existingUser = await findUserById({id: consumed.userId});\n if (existingUser?.status !== 'active' || existingUser.hashedPassword === null) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const user = await updateUserPassword({userId: consumed.userId, hashedPassword});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n await revokeRefreshTokensForUser({userId: consumed.userId});\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport async function changePassword(params: {\n userId: string;\n currentPassword: string;\n newPassword: string;\n refreshToken?: string | undefined;\n}): Promise<void> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.currentPassword,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok) {\n throw new InvalidCredentialsError();\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const currentRefreshSession = params.refreshToken\n ? await findActiveRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)})\n : undefined;\n const exceptRefreshTokenId =\n currentRefreshSession?.userId === user.id ? currentRefreshSession.id : undefined;\n\n await updateUserPassword({userId: user.id, hashedPassword});\n await revokeRefreshTokensForUser({userId: user.id, exceptRefreshTokenId});\n}\n\nexport async function logout(params: {refreshToken?: string | undefined}): Promise<void> {\n if (!params.refreshToken) return;\n const refreshToken = await findRefreshTokenByHash({\n hashedToken: hashOpaqueToken(params.refreshToken),\n });\n if (!refreshToken) return;\n await revokeRefreshSession({sessionId: refreshToken.sessionId, userId: refreshToken.userId});\n}\n\nexport interface GetCurrentUserResult {\n user: User;\n}\n\nexport async function getCurrentUser(params: {userId: string}): Promise<GetCurrentUserResult> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n return {user};\n}\n"],"names":["emailSchema","confirmEmailChallenge","consumeEmailChallengeProof","createEmailChallenge","resendEmailChallenge","workspacesInterModuleContract","isInterModuleKnownError","userAccessTokenKey","generateOpaqueToken","hashOpaqueToken","config","consumePasswordReset","createPasswordReset","createRefreshTokenForActiveUser","findActiveRefreshTokenByHash","findRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","rotateRefreshToken","createUser","createDbUser","findUserByEmail","findUserById","markEmailVerified","provisionUser","provisionDbUser","updateUserPassword","recordTokenRefreshed","getCurrentAdminRole","AuthDependencyUnavailableError","EmailNotVerifiedError","EmailTakenError","InvalidCredentialsError","InvitationEmailMismatchError","SignupNotAllowedError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","UserNotFoundError","signUserToken","hashPassword","verifyPassword","createEnvironmentSignupPolicy","RESET_TTL_HOURS","PASSWORD_VERIFICATION_PURPOSE","DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE","defaultSignupPolicy","assertSignupAllowed","params","result","signupPolicy","isSignupAllowed","email","emailVerified","source","allowed","message","dummyHashCache","getDummyHash","password","hoursFromNow","hours","Date","now","daysFromNow","days","recordRefreshOutcome","outcome","loadTokenMemberships","userId","workspaces","memberships","listMembershipsForTokenClaims","catch","error","signAccessToken","user","refreshSessionId","id","name","secret","expiresIn","AUTH_JWT_EXPIRES_IN","isWithinRotationGrace","refreshToken","rotatedAt","graceMs","AUTH_REFRESH_ROTATION_GRACE_SECONDS","getTime","createRefreshSession","session","sessionId","hashedToken","expiresAt","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","createSessionTokens","crypto","randomUUID","token","adminRole","passwordResetLink","rawToken","CLIENT_BASE_URL","invitationErrorFromKnownError","method","contractMethod","methods","code","invitationErrorMessage","parse","existing","signup","canResumeVerification","status","emailVerifiedAt","hashedPassword","hash","emailChallenge","purpose","continuation","idempotencyKey","sourceIp","signedUp","viaInvitation","signupWithInvitation","preflightInvitationAcceptance","invitationToken","membership","acceptError","acceptInvitation","workspaceId","verified","login","hasPassword","ok","createSessionForUser","undefined","refreshAccessToken","currentHashedToken","current","nextRefreshToken","rotated","nextHashedToken","latestUser","latest","confirmEmailVerification","challengeId","verifiedUser","resendEmailVerification","nextResendAvailableAt","requestPasswordReset","sendEmail","resetLink","expiresInHours","confirmPasswordReset","consumed","existingUser","newPassword","changePassword","currentPassword","currentRefreshSession","exceptRefreshTokenId","logout","getCurrentUser"],"mappings":"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"}
|
package/dist/core/jwt.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export declare const tokenMembershipSchema: z.ZodObject<{
|
|
|
4
4
|
role: z.ZodEnum<{
|
|
5
5
|
admin: "admin";
|
|
6
6
|
}>;
|
|
7
|
+
workspaceStatus: z.ZodDefault<z.ZodEnum<{
|
|
8
|
+
active: "active";
|
|
9
|
+
suspended: "suspended";
|
|
10
|
+
deleted: "deleted";
|
|
11
|
+
}>>;
|
|
7
12
|
}, z.core.$strip>;
|
|
8
13
|
export type TokenMembership = z.infer<typeof tokenMembershipSchema>;
|
|
9
14
|
export declare const userTokenClaimsSchema: z.ZodObject<{
|
|
@@ -16,6 +21,11 @@ export declare const userTokenClaimsSchema: z.ZodObject<{
|
|
|
16
21
|
role: z.ZodEnum<{
|
|
17
22
|
admin: "admin";
|
|
18
23
|
}>;
|
|
24
|
+
workspaceStatus: z.ZodDefault<z.ZodEnum<{
|
|
25
|
+
active: "active";
|
|
26
|
+
suspended: "suspended";
|
|
27
|
+
deleted: "deleted";
|
|
28
|
+
}>>;
|
|
19
29
|
}, z.core.$strip>>;
|
|
20
30
|
iat: z.ZodNumber;
|
|
21
31
|
exp: z.ZodNumber;
|
package/dist/core/jwt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/core/jwt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,eAAO,MAAM,qBAAqB
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/core/jwt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,eAAO,MAAM,qBAAqB;;;;;;;;;;iBAIhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;iBAQhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;CAC7B;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAchF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAa7F"}
|
package/dist/core/jwt.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { workspaceRoleSchema } from '@shipfox/api-workspaces-dto';
|
|
1
|
+
import { workspaceRoleSchema, workspaceStatusSchema } from '@shipfox/api-workspaces-dto';
|
|
2
2
|
import { signHs256, verifyHs256 } from '@shipfox/node-jwt';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { recordTokenIssued, recordTokenVerified } from '#metrics/index.js';
|
|
5
5
|
export const tokenMembershipSchema = z.object({
|
|
6
6
|
workspaceId: z.string().uuid(),
|
|
7
|
-
role: workspaceRoleSchema
|
|
7
|
+
role: workspaceRoleSchema,
|
|
8
|
+
workspaceStatus: workspaceStatusSchema.default('active')
|
|
8
9
|
});
|
|
9
10
|
export const userTokenClaimsSchema = z.object({
|
|
10
11
|
sub: z.string().uuid(),
|
package/dist/core/jwt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/jwt.ts"],"sourcesContent":["import {workspaceRoleSchema} from '@shipfox/api-workspaces-dto';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {z} from 'zod';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\nexport const tokenMembershipSchema = z.object({\n workspaceId: z.string().uuid(),\n role: workspaceRoleSchema,\n});\n\nexport type TokenMembership = z.infer<typeof tokenMembershipSchema>;\n\nexport const userTokenClaimsSchema = z.object({\n sub: z.string().uuid(),\n refreshSessionId: z.string().uuid().optional(),\n email: z.string().email(),\n name: z.string().nullable().optional(),\n memberships: z.array(tokenMembershipSchema),\n iat: z.number().int(),\n exp: z.number().int(),\n});\n\nexport type UserTokenClaims = z.infer<typeof userTokenClaimsSchema>;\n\nexport interface SignUserTokenParams {\n refreshSessionId?: string | undefined;\n userId: string;\n email: string;\n name?: string | null | undefined;\n memberships: TokenMembership[];\n secret: string | Uint8Array;\n expiresIn: string;\n}\n\nexport interface VerifyUserTokenParams {\n token: string;\n secret: string | Uint8Array;\n}\n\nexport async function signUserToken(params: SignUserTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n email: params.email,\n name: params.name ?? null,\n memberships: params.memberships,\n refreshSessionId: params.refreshSessionId,\n },\n secret: params.secret,\n expiresIn: params.expiresIn,\n subject: params.userId,\n });\n recordTokenIssued('session');\n return token;\n}\n\nexport async function verifyUserToken(params: VerifyUserTokenParams): Promise<UserTokenClaims> {\n try {\n const claims = await verifyHs256({\n token: params.token,\n secret: params.secret,\n schema: userTokenClaimsSchema,\n });\n recordTokenVerified('session', 'ok');\n return claims;\n } catch (error) {\n recordTokenVerified('session', 'rejected');\n throw error;\n }\n}\n"],"names":["workspaceRoleSchema","signHs256","verifyHs256","z","recordTokenIssued","recordTokenVerified","tokenMembershipSchema","object","workspaceId","string","uuid","role","userTokenClaimsSchema","sub","refreshSessionId","optional","email","name","nullable","memberships","array","iat","number","int","exp","signUserToken","params","token","payload","secret","expiresIn","subject","userId","verifyUserToken","claims","schema","error"],"mappings":"AAAA,SAAQA,mBAAmB,QAAO,8BAA8B;
|
|
1
|
+
{"version":3,"sources":["../../src/core/jwt.ts"],"sourcesContent":["import {workspaceRoleSchema, workspaceStatusSchema} from '@shipfox/api-workspaces-dto';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {z} from 'zod';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\nexport const tokenMembershipSchema = z.object({\n workspaceId: z.string().uuid(),\n role: workspaceRoleSchema,\n workspaceStatus: workspaceStatusSchema.default('active'),\n});\n\nexport type TokenMembership = z.infer<typeof tokenMembershipSchema>;\n\nexport const userTokenClaimsSchema = z.object({\n sub: z.string().uuid(),\n refreshSessionId: z.string().uuid().optional(),\n email: z.string().email(),\n name: z.string().nullable().optional(),\n memberships: z.array(tokenMembershipSchema),\n iat: z.number().int(),\n exp: z.number().int(),\n});\n\nexport type UserTokenClaims = z.infer<typeof userTokenClaimsSchema>;\n\nexport interface SignUserTokenParams {\n refreshSessionId?: string | undefined;\n userId: string;\n email: string;\n name?: string | null | undefined;\n memberships: TokenMembership[];\n secret: string | Uint8Array;\n expiresIn: string;\n}\n\nexport interface VerifyUserTokenParams {\n token: string;\n secret: string | Uint8Array;\n}\n\nexport async function signUserToken(params: SignUserTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n email: params.email,\n name: params.name ?? null,\n memberships: params.memberships,\n refreshSessionId: params.refreshSessionId,\n },\n secret: params.secret,\n expiresIn: params.expiresIn,\n subject: params.userId,\n });\n recordTokenIssued('session');\n return token;\n}\n\nexport async function verifyUserToken(params: VerifyUserTokenParams): Promise<UserTokenClaims> {\n try {\n const claims = await verifyHs256({\n token: params.token,\n secret: params.secret,\n schema: userTokenClaimsSchema,\n });\n recordTokenVerified('session', 'ok');\n return claims;\n } catch (error) {\n recordTokenVerified('session', 'rejected');\n throw error;\n }\n}\n"],"names":["workspaceRoleSchema","workspaceStatusSchema","signHs256","verifyHs256","z","recordTokenIssued","recordTokenVerified","tokenMembershipSchema","object","workspaceId","string","uuid","role","workspaceStatus","default","userTokenClaimsSchema","sub","refreshSessionId","optional","email","name","nullable","memberships","array","iat","number","int","exp","signUserToken","params","token","payload","secret","expiresIn","subject","userId","verifyUserToken","claims","schema","error"],"mappings":"AAAA,SAAQA,mBAAmB,EAAEC,qBAAqB,QAAO,8BAA8B;AACvF,SAAQC,SAAS,EAAEC,WAAW,QAAO,oBAAoB;AACzD,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,iBAAiB,EAAEC,mBAAmB,QAAO,oBAAoB;AAEzE,OAAO,MAAMC,wBAAwBH,EAAEI,MAAM,CAAC;IAC5CC,aAAaL,EAAEM,MAAM,GAAGC,IAAI;IAC5BC,MAAMZ;IACNa,iBAAiBZ,sBAAsBa,OAAO,CAAC;AACjD,GAAG;AAIH,OAAO,MAAMC,wBAAwBX,EAAEI,MAAM,CAAC;IAC5CQ,KAAKZ,EAAEM,MAAM,GAAGC,IAAI;IACpBM,kBAAkBb,EAAEM,MAAM,GAAGC,IAAI,GAAGO,QAAQ;IAC5CC,OAAOf,EAAEM,MAAM,GAAGS,KAAK;IACvBC,MAAMhB,EAAEM,MAAM,GAAGW,QAAQ,GAAGH,QAAQ;IACpCI,aAAalB,EAAEmB,KAAK,CAAChB;IACrBiB,KAAKpB,EAAEqB,MAAM,GAAGC,GAAG;IACnBC,KAAKvB,EAAEqB,MAAM,GAAGC,GAAG;AACrB,GAAG;AAmBH,OAAO,eAAeE,cAAcC,MAA2B;IAC7D,MAAMC,QAAQ,MAAM5B,UAAU;QAC5B6B,SAAS;YACPZ,OAAOU,OAAOV,KAAK;YACnBC,MAAMS,OAAOT,IAAI,IAAI;YACrBE,aAAaO,OAAOP,WAAW;YAC/BL,kBAAkBY,OAAOZ,gBAAgB;QAC3C;QACAe,QAAQH,OAAOG,MAAM;QACrBC,WAAWJ,OAAOI,SAAS;QAC3BC,SAASL,OAAOM,MAAM;IACxB;IACA9B,kBAAkB;IAClB,OAAOyB;AACT;AAEA,OAAO,eAAeM,gBAAgBP,MAA6B;IACjE,IAAI;QACF,MAAMQ,SAAS,MAAMlC,YAAY;YAC/B2B,OAAOD,OAAOC,KAAK;YACnBE,QAAQH,OAAOG,MAAM;YACrBM,QAAQvB;QACV;QACAT,oBAAoB,WAAW;QAC/B,OAAO+B;IACT,EAAE,OAAOE,OAAO;QACdjC,oBAAoB,WAAW;QAC/B,MAAMiC;IACR;AACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AdministrationActionEvent } from '@shipfox/api-common-dto';
|
|
2
|
+
import type { db } from './db.js';
|
|
3
|
+
import { type AdminCommandResultDb, type StoredAdminCommandResult } from './schema/admin-command-results.js';
|
|
4
|
+
export type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
5
|
+
export interface AdminCommandTransactionParams {
|
|
6
|
+
actorId: string;
|
|
7
|
+
idempotencyKeyFingerprint: string;
|
|
8
|
+
requestFingerprint: string;
|
|
9
|
+
event: AdministrationActionEvent;
|
|
10
|
+
}
|
|
11
|
+
export declare function lockAdminCommand(tx: Tx, params: Pick<AdminCommandTransactionParams, 'actorId' | 'idempotencyKeyFingerprint'>): Promise<void>;
|
|
12
|
+
export declare function lockAdminOwnerGrants(tx: Tx): Promise<void>;
|
|
13
|
+
export declare function findAdminCommandResult(tx: Tx, params: Pick<AdminCommandTransactionParams, 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'> & {
|
|
14
|
+
command: string;
|
|
15
|
+
}): Promise<AdminCommandResultDb | undefined>;
|
|
16
|
+
export declare function storeAdminCommandResult(tx: Tx, params: AdminCommandTransactionParams, result: StoredAdminCommandResult): Promise<void>;
|
|
17
|
+
export declare function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=admin-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-command.d.ts","sourceRoot":"","sources":["../../src/db/admin-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,wBAAwB,EAC9B,MAAM,mCAAmC,CAAC;AAG3C,MAAM,MAAM,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEpF,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,EAAE,EACN,MAAM,EAAE,IAAI,CAAC,6BAA6B,EAAE,SAAS,GAAG,2BAA2B,CAAC,GACnF,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhE;AAED,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,EAAE,EACN,MAAM,EAAE,IAAI,CACV,6BAA6B,EAC7B,SAAS,GAAG,2BAA2B,GAAG,oBAAoB,CAC/D,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpB,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAoB3C;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,MAAM,EAAE,6BAA6B,EACrC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9F"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { writeOutboxEvent } from '@shipfox/node-outbox';
|
|
2
|
+
import { and, eq, sql } from 'drizzle-orm';
|
|
3
|
+
import { AdminIdempotencyKeyReuseError } from '#core/errors.js';
|
|
4
|
+
import { adminCommandResults } from './schema/admin-command-results.js';
|
|
5
|
+
import { authOutbox } from './schema/outbox.js';
|
|
6
|
+
export async function lockAdminCommand(tx, params) {
|
|
7
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`);
|
|
8
|
+
}
|
|
9
|
+
export async function lockAdminOwnerGrants(tx) {
|
|
10
|
+
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);
|
|
11
|
+
}
|
|
12
|
+
export async function findAdminCommandResult(tx, params) {
|
|
13
|
+
const rows = await tx.select().from(adminCommandResults).where(and(eq(adminCommandResults.actorId, params.actorId), eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint))).limit(1);
|
|
14
|
+
const result = rows[0];
|
|
15
|
+
if (!result) return undefined;
|
|
16
|
+
if (result.command !== params.command || result.requestFingerprint !== params.requestFingerprint) {
|
|
17
|
+
throw new AdminIdempotencyKeyReuseError();
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
export async function storeAdminCommandResult(tx, params, result) {
|
|
22
|
+
await tx.insert(adminCommandResults).values({
|
|
23
|
+
actorId: params.actorId,
|
|
24
|
+
idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
|
|
25
|
+
command: params.event.command,
|
|
26
|
+
requestFingerprint: params.requestFingerprint,
|
|
27
|
+
result
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function writeAdminAction(tx, event) {
|
|
31
|
+
await writeOutboxEvent(tx, authOutbox, {
|
|
32
|
+
type: 'administration.action.performed',
|
|
33
|
+
payload: event
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=admin-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/db/admin-command.ts"],"sourcesContent":["import type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, eq, sql} from 'drizzle-orm';\nimport {AdminIdempotencyKeyReuseError} from '#core/errors.js';\nimport type {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminCommandResult,\n} from './schema/admin-command-results.js';\nimport {authOutbox} from './schema/outbox.js';\n\nexport type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport interface AdminCommandTransactionParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nexport async function lockAdminCommand(\n tx: Tx,\n params: Pick<AdminCommandTransactionParams, 'actorId' | 'idempotencyKeyFingerprint'>,\n): Promise<void> {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function lockAdminOwnerGrants(tx: Tx): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n}\n\nexport async function findAdminCommandResult(\n tx: Tx,\n params: Pick<\n AdminCommandTransactionParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {command: string},\n): Promise<AdminCommandResultDb | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return result;\n}\n\nexport async function storeAdminCommandResult(\n tx: Tx,\n params: AdminCommandTransactionParams,\n result: StoredAdminCommandResult,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.event.command,\n requestFingerprint: params.requestFingerprint,\n result,\n });\n}\n\nexport async function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n"],"names":["writeOutboxEvent","and","eq","sql","AdminIdempotencyKeyReuseError","adminCommandResults","authOutbox","lockAdminCommand","tx","params","execute","actorId","idempotencyKeyFingerprint","lockAdminOwnerGrants","findAdminCommandResult","rows","select","from","where","limit","result","undefined","command","requestFingerprint","storeAdminCommandResult","insert","values","event","writeAdminAction","type","payload"],"mappings":"AAIA,SAAQA,gBAAgB,QAAO,uBAAuB;AACtD,SAAQC,GAAG,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AACzC,SAAQC,6BAA6B,QAAO,kBAAkB;AAE9D,SAEEC,mBAAmB,QAEd,oCAAoC;AAC3C,SAAQC,UAAU,QAAO,qBAAqB;AAW9C,OAAO,eAAeC,iBACpBC,EAAM,EACNC,MAAoF;IAEpF,MAAMD,GAAGE,OAAO,CACdP,GAAG,CAAC,sCAAsC,EAAE,CAAC,mBAAmB,EAAEM,OAAOE,OAAO,CAAC,CAAC,EAAEF,OAAOG,yBAAyB,EAAE,CAAC,EAAE,CAAC;AAE9H;AAEA,OAAO,eAAeC,qBAAqBL,EAAM;IAC/C,MAAMA,GAAGE,OAAO,CAACP,GAAG,CAAC,iEAAiE,CAAC;AACzF;AAEA,OAAO,eAAeW,uBACpBN,EAAM,EACNC,MAGqB;IAErB,MAAMM,OAAO,MAAMP,GAChBQ,MAAM,GACNC,IAAI,CAACZ,qBACLa,KAAK,CACJjB,IACEC,GAAGG,oBAAoBM,OAAO,EAAEF,OAAOE,OAAO,GAC9CT,GAAGG,oBAAoBO,yBAAyB,EAAEH,OAAOG,yBAAyB,IAGrFO,KAAK,CAAC;IACT,MAAMC,SAASL,IAAI,CAAC,EAAE;IACtB,IAAI,CAACK,QAAQ,OAAOC;IACpB,IACED,OAAOE,OAAO,KAAKb,OAAOa,OAAO,IACjCF,OAAOG,kBAAkB,KAAKd,OAAOc,kBAAkB,EACvD;QACA,MAAM,IAAInB;IACZ;IACA,OAAOgB;AACT;AAEA,OAAO,eAAeI,wBACpBhB,EAAM,EACNC,MAAqC,EACrCW,MAAgC;IAEhC,MAAMZ,GAAGiB,MAAM,CAACpB,qBAAqBqB,MAAM,CAAC;QAC1Cf,SAASF,OAAOE,OAAO;QACvBC,2BAA2BH,OAAOG,yBAAyB;QAC3DU,SAASb,OAAOkB,KAAK,CAACL,OAAO;QAC7BC,oBAAoBd,OAAOc,kBAAkB;QAC7CH;IACF;AACF;AAEA,OAAO,eAAeQ,iBAAiBpB,EAAM,EAAEmB,KAAgC;IAC7E,MAAM3B,iBAA+CQ,IAAIF,YAAY;QACnEuB,MAAM;QACNC,SAASH;IACX;AACF"}
|
|
@@ -3,8 +3,8 @@ import type { AdministrationActionEvent } from '@shipfox/api-common-dto';
|
|
|
3
3
|
import { type TimestampIdCursor } from '@shipfox/node-drizzle';
|
|
4
4
|
import type { AdminGrant } from '#core/entities/admin-grant.js';
|
|
5
5
|
import type { AdministratorGrantSummary } from '#core/entities/administrator-read-model.js';
|
|
6
|
+
import { type Tx } from './admin-command.js';
|
|
6
7
|
import { db } from './db.js';
|
|
7
|
-
type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
8
8
|
type AdminOwnerQueryExecutor = ReturnType<typeof db> | Tx;
|
|
9
9
|
export interface CreateAdminGrantParams {
|
|
10
10
|
userId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4CAA4C,CAAC;AAQ1F,OAAO,EAKL,KAAK,EAAE,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAK3B,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC;AAmB1D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,MAAM,WAAW,+BAA+B;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,CAAC;CACzC;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC;IACV,IAAI,EAAE,+BAA+B,EAAE,CAAC;IACxC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC,CAAC,CA+BD;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc9F;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,GAAE,uBAA8B,GACvC,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA2BjG;AAED,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAiDD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACnD,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACpE,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,yBAAyB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpD,OAAO,CAAC,UAAU,CAAC,CAqCrB"}
|
package/dist/db/admin-grants.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { paginateTimestampIdRows, timestampIdCursorWhere } from '@shipfox/node-drizzle';
|
|
2
|
-
import {
|
|
3
|
-
import { and, desc, eq, isNull, sql } from 'drizzle-orm';
|
|
2
|
+
import { and, desc, eq, isNull } from 'drizzle-orm';
|
|
4
3
|
import { highestAdminRole } from '#core/admin-role-model.js';
|
|
5
|
-
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError,
|
|
4
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
|
|
5
|
+
import { findAdminCommandResult, lockAdminCommand, lockAdminOwnerGrants, storeAdminCommandResult, writeAdminAction } from './admin-command.js';
|
|
6
6
|
import { db } from './db.js';
|
|
7
|
-
import { adminCommandResults } from './schema/admin-command-results.js';
|
|
8
7
|
import { adminGrants, toAdminGrant } from './schema/admin-grants.js';
|
|
9
|
-
import { authOutbox } from './schema/outbox.js';
|
|
10
8
|
import { users } from './schema/users.js';
|
|
11
9
|
function activeAdminOwnerWhere() {
|
|
12
10
|
return and(eq(adminGrants.role, 'admin-owner'), isNull(adminGrants.revokedAt), eq(users.status, 'active'));
|
|
@@ -74,7 +72,7 @@ export async function revokeAdminGrant(params) {
|
|
|
74
72
|
return await db().transaction(async (tx)=>{
|
|
75
73
|
// All owner grant changes share one lock so concurrent revocations cannot
|
|
76
74
|
// both observe the same final owner and leave the instance ownerless.
|
|
77
|
-
await tx
|
|
75
|
+
await lockAdminOwnerGrants(tx);
|
|
78
76
|
const rows = await tx.select().from(adminGrants).where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt))).limit(1);
|
|
79
77
|
const grant = rows[0];
|
|
80
78
|
if (!grant) return undefined;
|
|
@@ -111,38 +109,22 @@ function fromStoredAdminGrant(grant) {
|
|
|
111
109
|
};
|
|
112
110
|
}
|
|
113
111
|
async function findCommandResult(tx, params) {
|
|
114
|
-
const
|
|
115
|
-
const result = rows[0];
|
|
112
|
+
const result = await findAdminCommandResult(tx, params);
|
|
116
113
|
if (!result) return undefined;
|
|
117
|
-
if (
|
|
118
|
-
throw new
|
|
114
|
+
if (!('grant' in result.result)) {
|
|
115
|
+
throw new Error('Administrator command result has an unexpected shape');
|
|
119
116
|
}
|
|
120
117
|
return fromStoredAdminGrant(result.result.grant);
|
|
121
118
|
}
|
|
122
119
|
async function storeCommandResult(tx, params, grant) {
|
|
123
|
-
await tx
|
|
124
|
-
|
|
125
|
-
idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
|
|
126
|
-
command: params.event.command,
|
|
127
|
-
requestFingerprint: params.requestFingerprint,
|
|
128
|
-
result: {
|
|
129
|
-
grant: toStoredAdminGrant(grant)
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
async function writeAdminAction(tx, event) {
|
|
134
|
-
await writeOutboxEvent(tx, authOutbox, {
|
|
135
|
-
type: 'administration.action.performed',
|
|
136
|
-
payload: event
|
|
120
|
+
await storeAdminCommandResult(tx, params, {
|
|
121
|
+
grant: toStoredAdminGrant(grant)
|
|
137
122
|
});
|
|
138
123
|
}
|
|
139
|
-
async function lockAdminCommand(tx, params) {
|
|
140
|
-
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`);
|
|
141
|
-
}
|
|
142
124
|
export async function bootstrapFirstAdminOwner(params) {
|
|
143
125
|
return await db().transaction(async (tx)=>{
|
|
144
126
|
await lockAdminCommand(tx, params);
|
|
145
|
-
await tx
|
|
127
|
+
await lockAdminOwnerGrants(tx);
|
|
146
128
|
const existing = await findCommandResult(tx, {
|
|
147
129
|
...params,
|
|
148
130
|
command: params.event.command
|
|
@@ -170,7 +152,7 @@ export async function bootstrapFirstAdminOwner(params) {
|
|
|
170
152
|
export async function grantAdminRoleWithAudit(params) {
|
|
171
153
|
return await db().transaction(async (tx)=>{
|
|
172
154
|
await lockAdminCommand(tx, params);
|
|
173
|
-
await tx
|
|
155
|
+
await lockAdminOwnerGrants(tx);
|
|
174
156
|
const existing = await findCommandResult(tx, {
|
|
175
157
|
...params,
|
|
176
158
|
command: params.event.command
|
|
@@ -201,7 +183,7 @@ export async function grantAdminRoleWithAudit(params) {
|
|
|
201
183
|
export async function revokeAdminGrantWithAudit(params) {
|
|
202
184
|
return await db().transaction(async (tx)=>{
|
|
203
185
|
await lockAdminCommand(tx, params);
|
|
204
|
-
await tx
|
|
186
|
+
await lockAdminOwnerGrants(tx);
|
|
205
187
|
const existing = await findCommandResult(tx, {
|
|
206
188
|
...params,
|
|
207
189
|
command: params.event.command
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {\n paginateTimestampIdRows,\n type TimestampIdCursor,\n timestampIdCursorWhere,\n} from '@shipfox/node-drizzle';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, desc, eq, isNull, sql} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminGrant,\n} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {authOutbox} from './schema/outbox.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\ntype AdminOwnerQueryExecutor = ReturnType<typeof db> | Tx;\n\nfunction activeAdminOwnerWhere() {\n return and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n );\n}\n\nasync function listActiveAdminOwners(executor: AdminOwnerQueryExecutor) {\n return await executor\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(activeAdminOwnerWhere())\n .limit(2);\n}\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport interface AdministratorGrantSummaryRecord {\n id: string;\n role: AdminRole;\n createdAt: Date;\n revokedAt: Date | null;\n user: AdministratorGrantSummary['user'];\n}\n\nexport async function listAdminGrantSummaries(params: {\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n rows: AdministratorGrantSummaryRecord[];\n nextCursor: TimestampIdCursor | null;\n}> {\n const cursorCondition = timestampIdCursorWhere({\n timestampColumn: adminGrants.createdAt,\n idColumn: adminGrants.id,\n cursor: params.cursor,\n });\n const conditions = cursorCondition ? [cursorCondition] : [];\n const rows = await db()\n .select({\n id: adminGrants.id,\n role: adminGrants.role,\n createdAt: adminGrants.createdAt,\n revokedAt: adminGrants.revokedAt,\n user: {\n id: users.id,\n email: users.email,\n name: users.name,\n status: users.status,\n },\n })\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(and(...conditions))\n .orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))\n .limit(params.limit + 1);\n\n const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});\n return {\n rows: page.pageRows,\n nextCursor: page.nextCursor,\n };\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\n/**\n * An owner is active only while both the grant and its user account remain\n * active. A suspended owner's grant is retained, but it intentionally does\n * not prevent deployment-bound bootstrap recovery.\n */\nexport async function hasActiveAdminOwner(\n executor: AdminOwnerQueryExecutor = db(),\n): Promise<boolean> {\n const rows = await listActiveAdminOwners(executor);\n return rows.length > 0;\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result: AdminCommandResultDb | undefined = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.event.command,\n requestFingerprint: params.requestFingerprint,\n result: {grant: toStoredAdminGrant(grant)},\n });\n}\n\nasync function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n\nasync function lockAdminCommand(\n tx: Tx,\n params: {actorId: string; idempotencyKeyFingerprint: string},\n) {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n if (await hasActiveAdminOwner(tx)) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["paginateTimestampIdRows","timestampIdCursorWhere","writeOutboxEvent","and","desc","eq","isNull","sql","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","LastAdminOwnerError","UserNotFoundError","db","adminCommandResults","adminGrants","toAdminGrant","authOutbox","users","activeAdminOwnerWhere","role","revokedAt","status","listActiveAdminOwners","executor","select","id","from","innerJoin","userId","where","limit","createAdminGrant","params","rows","insert","values","returning","row","Error","listAdminGrantSummaries","cursorCondition","timestampColumn","createdAt","idColumn","cursor","conditions","user","email","name","orderBy","page","timestampKey","pageRows","nextCursor","findCurrentAdminRole","map","hasActiveAdminOwner","length","revokeAdminGrant","transaction","tx","execute","grantId","grant","undefined","activeOwners","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","actorId","idempotencyKeyFingerprint","result","command","requestFingerprint","storeCommandResult","event","writeAdminAction","type","payload","lockAdminCommand","bootstrapFirstAdminOwner","existing","userRows","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {AdministrationActionEvent} from '@shipfox/api-common-dto';\nimport {\n paginateTimestampIdRows,\n type TimestampIdCursor,\n timestampIdCursorWhere,\n} from '@shipfox/node-drizzle';\nimport {and, desc, eq, isNull} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {\n findAdminCommandResult,\n lockAdminCommand,\n lockAdminOwnerGrants,\n storeAdminCommandResult,\n type Tx,\n writeAdminAction,\n} from './admin-command.js';\nimport {db} from './db.js';\nimport type {StoredAdminGrant} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {users} from './schema/users.js';\n\ntype AdminOwnerQueryExecutor = ReturnType<typeof db> | Tx;\n\nfunction activeAdminOwnerWhere() {\n return and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n );\n}\n\nasync function listActiveAdminOwners(executor: AdminOwnerQueryExecutor) {\n return await executor\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(activeAdminOwnerWhere())\n .limit(2);\n}\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport interface AdministratorGrantSummaryRecord {\n id: string;\n role: AdminRole;\n createdAt: Date;\n revokedAt: Date | null;\n user: AdministratorGrantSummary['user'];\n}\n\nexport async function listAdminGrantSummaries(params: {\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n rows: AdministratorGrantSummaryRecord[];\n nextCursor: TimestampIdCursor | null;\n}> {\n const cursorCondition = timestampIdCursorWhere({\n timestampColumn: adminGrants.createdAt,\n idColumn: adminGrants.id,\n cursor: params.cursor,\n });\n const conditions = cursorCondition ? [cursorCondition] : [];\n const rows = await db()\n .select({\n id: adminGrants.id,\n role: adminGrants.role,\n createdAt: adminGrants.createdAt,\n revokedAt: adminGrants.revokedAt,\n user: {\n id: users.id,\n email: users.email,\n name: users.name,\n status: users.status,\n },\n })\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(and(...conditions))\n .orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))\n .limit(params.limit + 1);\n\n const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});\n return {\n rows: page.pageRows,\n nextCursor: page.nextCursor,\n };\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\n/**\n * An owner is active only while both the grant and its user account remain\n * active. A suspended owner's grant is retained, but it intentionally does\n * not prevent deployment-bound bootstrap recovery.\n */\nexport async function hasActiveAdminOwner(\n executor: AdminOwnerQueryExecutor = db(),\n): Promise<boolean> {\n const rows = await listActiveAdminOwners(executor);\n return rows.length > 0;\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await lockAdminOwnerGrants(tx);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const result = await findAdminCommandResult(tx, params);\n if (!result) return undefined;\n if (!('grant' in result.result)) {\n throw new Error('Administrator command result has an unexpected shape');\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await storeAdminCommandResult(tx, params, {grant: toStoredAdminGrant(grant)});\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n if (await hasActiveAdminOwner(tx)) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["paginateTimestampIdRows","timestampIdCursorWhere","and","desc","eq","isNull","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","LastAdminOwnerError","UserNotFoundError","findAdminCommandResult","lockAdminCommand","lockAdminOwnerGrants","storeAdminCommandResult","writeAdminAction","db","adminGrants","toAdminGrant","users","activeAdminOwnerWhere","role","revokedAt","status","listActiveAdminOwners","executor","select","id","from","innerJoin","userId","where","limit","createAdminGrant","params","rows","insert","values","returning","row","Error","listAdminGrantSummaries","cursorCondition","timestampColumn","createdAt","idColumn","cursor","conditions","user","email","name","orderBy","page","timestampKey","pageRows","nextCursor","findCurrentAdminRole","map","hasActiveAdminOwner","length","revokeAdminGrant","transaction","tx","grantId","grant","undefined","activeOwners","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","result","storeCommandResult","bootstrapFirstAdminOwner","existing","command","event","userRows","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AdministrationActionEvent } from '@shipfox/api-common-dto';
|
|
2
|
+
interface UserModerationCommandParams {
|
|
3
|
+
actorId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
idempotencyKeyFingerprint: string;
|
|
6
|
+
requestFingerprint: string;
|
|
7
|
+
event: AdministrationActionEvent;
|
|
8
|
+
}
|
|
9
|
+
declare function executeUserModerationCommand(params: UserModerationCommandParams & {
|
|
10
|
+
operation: 'suspend' | 'reactivate' | 'revoke-sessions';
|
|
11
|
+
}): Promise<{
|
|
12
|
+
user: {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
name: string | null;
|
|
16
|
+
emailVerifiedAt: Date | null;
|
|
17
|
+
status: import("../index.js").UserStatus;
|
|
18
|
+
createdAt: Date;
|
|
19
|
+
adminRole: "admin-observer" | "admin-operator" | "admin-owner" | null;
|
|
20
|
+
};
|
|
21
|
+
correlationId: string;
|
|
22
|
+
sessionsRevoked: number;
|
|
23
|
+
}>;
|
|
24
|
+
export type UserModerationResult = Awaited<ReturnType<typeof executeUserModerationCommand>>;
|
|
25
|
+
export declare function suspendUserWithAudit(params: UserModerationCommandParams): Promise<UserModerationResult>;
|
|
26
|
+
export declare function reactivateUserWithAudit(params: UserModerationCommandParams): Promise<UserModerationResult>;
|
|
27
|
+
export declare function revokeUserSessionsWithAudit(params: UserModerationCommandParams): Promise<UserModerationResult>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=admin-user-moderation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-user-moderation.d.ts","sourceRoot":"","sources":["../../src/db/admin-user-moderation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AAwBvE,UAAU,2BAA2B;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAyGD,iBAAe,4BAA4B,CACzC,MAAM,EAAE,2BAA2B,GAAG;IACpC,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,iBAAiB,CAAC;CACzD;;;;;;;;;;;;GAuEF;AAED,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC,CAAC;AAE5F,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B"}
|