chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/external-2fa-codes. Resolve formatting
conflicts caused by biome rollout; preserve both feature streams:
PR's external 2FA token + signing-session 2FA proof additions plus
main's RateLimit/RecipientExpired/signingReminders/date-auto-insert.

In complete-document-with-token.ts, drop the duplicate early
field-fetching block introduced when main moved that logic later
with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH
check using derivedRecipientActionAuth.
This commit is contained in:
ephraimduncan
2026-05-12 11:46:11 +00:00
parent 9194884fbe
commit 138d663c25
1959 changed files with 93488 additions and 47038 deletions
@@ -17,6 +17,7 @@ export const AuthenticationErrorCode = {
// TwoFactorMissingSecret: 'TWO_FACTOR_MISSING_SECRET',
// TwoFactorMissingCredentials: 'TWO_FACTOR_MISSING_CREDENTIALS',
InvalidTwoFactorCode: 'INVALID_TWO_FACTOR_CODE',
SignupDisabled: 'SIGNUP_DISABLED',
// IncorrectTwoFactorBackupCode: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
// IncorrectIdentityProvider: 'INCORRECT_IDENTITY_PROVIDER',
// IncorrectPassword: 'INCORRECT_PASSWORD',
@@ -1,13 +1,8 @@
import type { Context } from 'hono';
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie';
import {
formatSecureCookieName,
getCookieDomain,
useSecureCookies,
} from '@documenso/lib/constants/auth';
import { formatSecureCookieName, getCookieDomain, useSecureCookies } from '@documenso/lib/constants/auth';
import { appLog } from '@documenso/lib/utils/debugger';
import { env } from '@documenso/lib/utils/env';
import type { Context } from 'hono';
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie';
import { AUTH_SESSION_LIFETIME } from '../../config';
import { extractCookieFromHeaders } from '../utils/cookies';
@@ -61,13 +56,7 @@ export const getSessionCookie = async (c: Context): Promise<string | null> => {
* @param sessionToken - The session token to set.
*/
export const setSessionCookie = async (c: Context, sessionToken: string) => {
await setSignedCookie(
c,
sessionCookieName,
sessionToken,
getAuthSecret(),
sessionCookieOptions,
).catch((err) => {
await setSignedCookie(c, sessionCookieName, sessionToken, getAuthSecret(), sessionCookieOptions).catch((err) => {
appLog('SetSessionCookie', `Error setting signed cookie: ${err}`);
throw err;
+6 -20
View File
@@ -1,10 +1,9 @@
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
import { AUTH_SESSION_LIFETIME } from '../../config';
@@ -15,14 +14,7 @@ import { AUTH_SESSION_LIFETIME } from '../../config';
*/
export type SessionUser = Pick<
User,
| 'id'
| 'name'
| 'email'
| 'emailVerified'
| 'avatarImageId'
| 'twoFactorEnabled'
| 'roles'
| 'signature'
'id' | 'name' | 'email' | 'emailVerified' | 'avatarImageId' | 'twoFactorEnabled' | 'roles' | 'signature'
>;
export type SessionValidationResult =
@@ -43,11 +35,7 @@ export const generateSessionToken = (): string => {
return token;
};
export const createSession = async (
token: string,
userId: number,
metadata: RequestMetadata,
): Promise<Session> => {
export const createSession = async (token: string, userId: number, metadata: RequestMetadata): Promise<Session> => {
const hashedSessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Session = {
@@ -166,9 +154,7 @@ export const invalidateSessions = async ({
userId,
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
type: isRevoke
? UserSecurityAuditLogType.SESSION_REVOKED
: UserSecurityAuditLogType.SIGN_OUT,
type: isRevoke ? UserSecurityAuditLogType.SESSION_REVOKED : UserSecurityAuditLogType.SIGN_OUT,
})),
});
});
@@ -1,8 +1,7 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import type { Context } from 'hono';
import { ORGANISATION_USER_ACCOUNT_TYPE } from '@documenso/lib/constants/organisations';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@prisma/client';
import type { Context } from 'hono';
import { getSession } from './get-session';
@@ -1,6 +1,5 @@
import type { Context } from 'hono';
import { prisma } from '@documenso/prisma';
import type { Context } from 'hono';
import { getSession } from './get-session';
@@ -1,8 +1,7 @@
import type { Session } from '@prisma/client';
import type { Context } from 'hono';
import { AppError } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import type { Session } from '@prisma/client';
import type { Context } from 'hono';
import { AuthenticationErrorCode } from '../errors/error-codes';
import type { SessionValidationResult } from '../session/session';
@@ -23,9 +22,7 @@ export const getSession = async (c: Context | Request) => {
throw new AppError(AuthenticationErrorCode.Unauthorized);
};
export const getOptionalSession = async (
c: Context | Request,
): Promise<SessionValidationResult> => {
export const getOptionalSession = async (c: Context | Request): Promise<SessionValidationResult> => {
const sessionId = await getSessionCookie(mapRequestToContextForCookie(c));
if (!sessionId) {
@@ -1,9 +1,8 @@
import { CodeChallengeMethod, OAuth2Client, generateCodeVerifier, generateState } from 'arctic';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { CodeChallengeMethod, generateCodeVerifier, generateState, OAuth2Client } from 'arctic';
import type { Context } from 'hono';
import { setCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { OAuthClientOptions } from '../../config';
import { sessionCookieOptions } from '../session/session-cookies';
import { getOpenIdConfiguration } from './open-id';
@@ -49,11 +48,7 @@ export const handleOAuthAuthorizeUrl = async (options: HandleOAuthAuthorizeUrlOp
requiredScopes: clientOptions.scope,
});
const oAuthClient = new OAuth2Client(
clientOptions.clientId,
clientOptions.clientSecret,
clientOptions.redirectUrl,
);
const oAuthClient = new OAuth2Client(clientOptions.clientId, clientOptions.clientSecret, clientOptions.redirectUrl);
const scopes = clientOptions.scope;
const state = generateState();
@@ -1,12 +1,15 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { OAuth2Client, decodeIdToken } from 'arctic';
import type { Context } from 'hono';
import { deleteCookie } from 'hono/cookie';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { isEmailDomainAllowedForSignup, isSignupEnabledForProvider } from '@documenso/lib/constants/auth';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@prisma/client';
import { decodeIdToken, OAuth2Client } from 'arctic';
import type { Context } from 'hono';
import { deleteCookie } from 'hono/cookie';
import type { OAuthClientOptions } from '../../config';
import { AuthenticationErrorCode } from '../errors/error-codes';
@@ -23,8 +26,14 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
const requestMeta = c.get('requestMetadata');
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } =
await validateOauth({ c, clientOptions });
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } = await validateOauth({
c,
clientOptions,
});
if (email.toLowerCase() === legacyServiceAccountEmail() || email.toLowerCase() === deletedServiceAccountEmail()) {
return c.text('FORBIDDEN', 403);
}
// Find the account if possible.
const existingAccount = await prisma.account.findFirst({
@@ -105,6 +114,24 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
return c.redirect(redirectPath, 302);
}
// Check if signups are disabled for this provider.
if (!isSignupEnabledForProvider(clientOptions.id as 'google' | 'microsoft' | 'oidc')) {
const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL());
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
return c.redirect(errorUrl.toString(), 302);
}
// Check domain restriction for new SSO users.
if (!isEmailDomainAllowedForSignup(email)) {
const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL());
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
return c.redirect(errorUrl.toString(), 302);
}
// Handle new user.
const createdUser = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
@@ -152,11 +179,7 @@ export const validateOauth = async (options: HandleOAuthCallbackUrlOptions) => {
requiredScopes: clientOptions.scope,
});
const oAuthClient = new OAuth2Client(
clientOptions.clientId,
clientOptions.clientSecret,
clientOptions.redirectUrl,
);
const oAuthClient = new OAuth2Client(clientOptions.clientId, clientOptions.clientSecret, clientOptions.redirectUrl);
const code = c.req.query('code');
const state = c.req.query('state');
@@ -184,11 +207,7 @@ export const validateOauth = async (options: HandleOAuthCallbackUrlOptions) => {
redirectPath = normalizeReturnTo(redirectPath) || '/';
const tokens = await oAuthClient.validateAuthorizationCode(
token_endpoint,
code,
storedCodeVerifier,
);
const tokens = await oAuthClient.validateAuthorizationCode(token_endpoint, code, storedCodeVerifier);
const accessToken = tokens.accessToken();
const accessTokenExpiresAt = tokens.accessTokenExpiresAt();
@@ -1,10 +1,10 @@
import type { Context } from 'hono';
import { sendOrganisationAccountLinkConfirmationEmail } from '@documenso/ee/server-only/lib/send-organisation-account-link-confirmation-email';
import { isSignupEnabledForProvider } from '@documenso/lib/constants/auth';
import { AppError } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { formatOrganisationLoginUrl } from '@documenso/lib/utils/organisation-authentication-portal';
import { prisma } from '@documenso/prisma';
import type { Context } from 'hono';
import { AuthenticationErrorCode } from '../errors/error-codes';
import { onAuthorize } from './authorizer';
@@ -16,9 +16,7 @@ type HandleOAuthOrganisationCallbackUrlOptions = {
orgUrl: string;
};
export const handleOAuthOrganisationCallbackUrl = async (
options: HandleOAuthOrganisationCallbackUrlOptions,
) => {
export const handleOAuthOrganisationCallbackUrl = async (options: HandleOAuthOrganisationCallbackUrlOptions) => {
const { c, orgUrl } = options;
const { organisation, clientOptions } = await getOrganisationAuthenticationPortalOptions({
@@ -68,6 +66,14 @@ export const handleOAuthOrganisationCallbackUrl = async (
// Handle new user.
if (!userToLink) {
if (!isSignupEnabledForProvider('oidc')) {
const errorUrl = new URL(formatOrganisationLoginUrl(orgUrl));
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
return c.redirect(errorUrl.toString(), 302);
}
userToLink = await prisma.user.create({
data: {
email: email,
@@ -76,7 +82,9 @@ export const handleOAuthOrganisationCallbackUrl = async (
},
});
await onCreateUserHook(userToLink).catch((err) => {
await onCreateUserHook(userToLink, {
skipPersonalOrganisation: !organisation.organisationAuthenticationPortal.allowPersonalOrganisations,
}).catch((err) => {
// Todo: (RR7) Add logging.
console.error(err);
});
@@ -55,11 +55,7 @@ export const getOrganisationAuthenticationPortalOptions = async (
});
}
const {
clientId,
clientSecret: encryptedClientSecret,
wellKnownUrl,
} = organisation.organisationAuthenticationPortal;
const { clientId, clientSecret: encryptedClientSecret, wellKnownUrl } = organisation.organisationAuthenticationPortal;
if (!clientId || !encryptedClientSecret || !wellKnownUrl) {
throw new AppError(AppErrorCode.NOT_SETUP, {