Skip to content

security: widget workflow & MCP data hardening (5 fixes) (#43) #36

security: widget workflow & MCP data hardening (5 fixes) (#43)

security: widget workflow & MCP data hardening (5 fixes) (#43) #36

Triggered via push June 15, 2026 13:20
Status Failure
Total duration 11m 31s
Artifacts

ci.yml

on: push
Fit to window
Zoom out
Zoom in

Annotations

5 errors
test
Process completed with exit code 1.
apps/web/src/lib/server/auth/__tests__/magic-link-security.test.ts > magic-link security configuration > does not log magic-link redirect locations: apps/web/src/lib/server/auth/__tests__/magic-link-security.test.ts#L20
AssertionError: expected 'import { betterAuth } from \'better-a…' not to contain 'response.headers.get(\'location\')' - Expected + Received - response.headers.get('location') + import { betterAuth } from 'better-auth' + import { drizzleAdapter } from 'better-auth/adapters/drizzle' + import { + anonymous, + emailOTP, + oneTimeToken, + magicLink, + jwt, + genericOAuth, + bearer, + twoFactor, + } from 'better-auth/plugins' + import { oauthProvider } from '@better-auth/oauth-provider' + import { tanstackStartCookies } from 'better-auth/tanstack-start' + import { generateId } from '@opencoven-feedback/ids' + import { config } from '@/lib/server/config' + + // Plugin callbacks (magicLink, emailOTP) stash tokens here instead of + // emailing — callers that own the email template (invitations, + // combined sign-in email) drain the stash and email themselves. + const STASH_TTL_MS = 30_000 + + function makeStash<T>() { + const m = new Map<string, { value: T; ts: number }>() + return { + set(key: string, value: T) { + const k = key.toLowerCase() + m.set(k, { value, ts: Date.now() }) + setTimeout(() => { + const s = m.get(k) + if (s && Date.now() - s.ts >= STASH_TTL_MS) m.delete(k) + }, STASH_TTL_MS) + }, + take(key: string): T | undefined { + const k = key.toLowerCase() + const s = m.get(k) + if (!s) return undefined + m.delete(k) + return s.value + }, + } + } + + const magicLinkStash = makeStash<string>() + const otpStash = makeStash<string>() + + export const storeMagicLinkToken = (email: string, token: string) => + magicLinkStash.set(email, token) + export const getMagicLinkToken = (email: string) => magicLinkStash.take(email) + export const storeOTP = (email: string, otp: string) => otpStash.set(email, otp) + export const getOTP = (email: string) => otpStash.take(email) + + // Lazy-initialized auth instance + // This prevents client bundling of database code + type AuthInstance = Awaited<ReturnType<typeof createAuth>>['instance'] + let _auth: AuthInstance | null = null + // Cross-pod invalidation: the version of `settings.auth_config_version` + // at the time the cached _auth was built. Compared per-request against + // the current value (via the existing settings cache, no extra DB + // round-trip). Mismatch → resetAuth(), other pods' writes propagate. + let _authConfigVersion: number | null = null + + async function createAuth() { + // Dynamic imports to prevent client bundling + const { + db, + user: userTable, + session: sessionTable, + account: accountTable, + verification: verificationTable, + oneTimeToken: oneTimeTokenTable, + settings: settingsTable, + principal: principalTable, + invitation: invitationTable, + jwks: jwksTable, + oauthClient: oauthClientTable, + oauthAccessToken: oauthAccessTokenTable, + oauthRefreshToken: oauthRefreshTokenTable, + oauthConsent: oauthConsentTable, + twoFactor: twoFactorTable, + eq, + } = await import('@/lib/server/db') + const { sendPasswordResetEmail, isEmailConfigured } = await import('@opencoven-feedback/email') + const { getPlatformCredentials } = + await import('@/lib/server/domains/platform-credentials/platform-credential.service') + const { getAllAuthProviders } = await import('./auth-providers') + const { getTierLimits } = await import('@/lib/server/domains/settings/tier-limits.service') + const { getTenantSettings } = await import('@/lib/server/domains/settings/settings.service') + + // Build socialProviders config from DB-stored credentials + const socialProviders: Record<string, Record<string, string>> = {} + const trustedProviders: string[] = [] + const genericOAuthConfigs: Array<{ + providerId: string + clientId: string + clientSecret: string + disableSignUp?: boolean + discoveryUrl?: string + authorizationUrl?: string + tokenUrl?: string + scopes?: string[] + // SSO-only: force the IdP account picker so admins notice when + //
apps/web/src/lib/server/auth/__tests__/magic-link-security.test.ts > magic-link security configuration > does not log bearer magic-link tokens from request URLs: apps/web/src/lib/server/auth/__tests__/magic-link-security.test.ts#L16
AssertionError: expected 'import { betterAuth } from \'better-a…' not to contain '${url.pathname}${url.search}' - Expected + Received - ${url.pathname}${url.search} + import { betterAuth } from 'better-auth' + import { drizzleAdapter } from 'better-auth/adapters/drizzle' + import { + anonymous, + emailOTP, + oneTimeToken, + magicLink, + jwt, + genericOAuth, + bearer, + twoFactor, + } from 'better-auth/plugins' + import { oauthProvider } from '@better-auth/oauth-provider' + import { tanstackStartCookies } from 'better-auth/tanstack-start' + import { generateId } from '@opencoven-feedback/ids' + import { config } from '@/lib/server/config' + + // Plugin callbacks (magicLink, emailOTP) stash tokens here instead of + // emailing — callers that own the email template (invitations, + // combined sign-in email) drain the stash and email themselves. + const STASH_TTL_MS = 30_000 + + function makeStash<T>() { + const m = new Map<string, { value: T; ts: number }>() + return { + set(key: string, value: T) { + const k = key.toLowerCase() + m.set(k, { value, ts: Date.now() }) + setTimeout(() => { + const s = m.get(k) + if (s && Date.now() - s.ts >= STASH_TTL_MS) m.delete(k) + }, STASH_TTL_MS) + }, + take(key: string): T | undefined { + const k = key.toLowerCase() + const s = m.get(k) + if (!s) return undefined + m.delete(k) + return s.value + }, + } + } + + const magicLinkStash = makeStash<string>() + const otpStash = makeStash<string>() + + export const storeMagicLinkToken = (email: string, token: string) => + magicLinkStash.set(email, token) + export const getMagicLinkToken = (email: string) => magicLinkStash.take(email) + export const storeOTP = (email: string, otp: string) => otpStash.set(email, otp) + export const getOTP = (email: string) => otpStash.take(email) + + // Lazy-initialized auth instance + // This prevents client bundling of database code + type AuthInstance = Awaited<ReturnType<typeof createAuth>>['instance'] + let _auth: AuthInstance | null = null + // Cross-pod invalidation: the version of `settings.auth_config_version` + // at the time the cached _auth was built. Compared per-request against + // the current value (via the existing settings cache, no extra DB + // round-trip). Mismatch → resetAuth(), other pods' writes propagate. + let _authConfigVersion: number | null = null + + async function createAuth() { + // Dynamic imports to prevent client bundling + const { + db, + user: userTable, + session: sessionTable, + account: accountTable, + verification: verificationTable, + oneTimeToken: oneTimeTokenTable, + settings: settingsTable, + principal: principalTable, + invitation: invitationTable, + jwks: jwksTable, + oauthClient: oauthClientTable, + oauthAccessToken: oauthAccessTokenTable, + oauthRefreshToken: oauthRefreshTokenTable, + oauthConsent: oauthConsentTable, + twoFactor: twoFactorTable, + eq, + } = await import('@/lib/server/db') + const { sendPasswordResetEmail, isEmailConfigured } = await import('@opencoven-feedback/email') + const { getPlatformCredentials } = + await import('@/lib/server/domains/platform-credentials/platform-credential.service') + const { getAllAuthProviders } = await import('./auth-providers') + const { getTierLimits } = await import('@/lib/server/domains/settings/tier-limits.service') + const { getTenantSettings } = await import('@/lib/server/domains/settings/settings.service') + + // Build socialProviders config from DB-stored credentials + const socialProviders: Record<string, Record<string, string>> = {} + const trustedProviders: string[] = [] + const genericOAuthConfigs: Array<{ + providerId: string + clientId: string + clientSecret: string + disableSignUp?: boolean + discoveryUrl?: string + authorizationUrl?: string + tokenUrl?: string + scopes?: string[] + // SSO-only: force the IdP account picker so admins notice when + // they're a
apps/web/src/lib/server/auth/__tests__/magic-link-security.test.ts > magic-link security configuration > keeps Better Auth magic-link verification single-use: apps/web/src/lib/server/auth/__tests__/magic-link-security.test.ts#L11
AssertionError: expected 'import { betterAuth } from \'better-a…' not to contain 'allowedAttempts' - Expected + Received - allowedAttempts + import { betterAuth } from 'better-auth' + import { drizzleAdapter } from 'better-auth/adapters/drizzle' + import { + anonymous, + emailOTP, + oneTimeToken, + magicLink, + jwt, + genericOAuth, + bearer, + twoFactor, + } from 'better-auth/plugins' + import { oauthProvider } from '@better-auth/oauth-provider' + import { tanstackStartCookies } from 'better-auth/tanstack-start' + import { generateId } from '@opencoven-feedback/ids' + import { config } from '@/lib/server/config' + + // Plugin callbacks (magicLink, emailOTP) stash tokens here instead of + // emailing — callers that own the email template (invitations, + // combined sign-in email) drain the stash and email themselves. + const STASH_TTL_MS = 30_000 + + function makeStash<T>() { + const m = new Map<string, { value: T; ts: number }>() + return { + set(key: string, value: T) { + const k = key.toLowerCase() + m.set(k, { value, ts: Date.now() }) + setTimeout(() => { + const s = m.get(k) + if (s && Date.now() - s.ts >= STASH_TTL_MS) m.delete(k) + }, STASH_TTL_MS) + }, + take(key: string): T | undefined { + const k = key.toLowerCase() + const s = m.get(k) + if (!s) return undefined + m.delete(k) + return s.value + }, + } + } + + const magicLinkStash = makeStash<string>() + const otpStash = makeStash<string>() + + export const storeMagicLinkToken = (email: string, token: string) => + magicLinkStash.set(email, token) + export const getMagicLinkToken = (email: string) => magicLinkStash.take(email) + export const storeOTP = (email: string, otp: string) => otpStash.set(email, otp) + export const getOTP = (email: string) => otpStash.take(email) + + // Lazy-initialized auth instance + // This prevents client bundling of database code + type AuthInstance = Awaited<ReturnType<typeof createAuth>>['instance'] + let _auth: AuthInstance | null = null + // Cross-pod invalidation: the version of `settings.auth_config_version` + // at the time the cached _auth was built. Compared per-request against + // the current value (via the existing settings cache, no extra DB + // round-trip). Mismatch → resetAuth(), other pods' writes propagate. + let _authConfigVersion: number | null = null + + async function createAuth() { + // Dynamic imports to prevent client bundling + const { + db, + user: userTable, + session: sessionTable, + account: accountTable, + verification: verificationTable, + oneTimeToken: oneTimeTokenTable, + settings: settingsTable, + principal: principalTable, + invitation: invitationTable, + jwks: jwksTable, + oauthClient: oauthClientTable, + oauthAccessToken: oauthAccessTokenTable, + oauthRefreshToken: oauthRefreshTokenTable, + oauthConsent: oauthConsentTable, + twoFactor: twoFactorTable, + eq, + } = await import('@/lib/server/db') + const { sendPasswordResetEmail, isEmailConfigured } = await import('@opencoven-feedback/email') + const { getPlatformCredentials } = + await import('@/lib/server/domains/platform-credentials/platform-credential.service') + const { getAllAuthProviders } = await import('./auth-providers') + const { getTierLimits } = await import('@/lib/server/domains/settings/tier-limits.service') + const { getTenantSettings } = await import('@/lib/server/domains/settings/settings.service') + + // Build socialProviders config from DB-stored credentials + const socialProviders: Record<string, Record<string, string>> = {} + const trustedProviders: string[] = [] + const genericOAuthConfigs: Array<{ + providerId: string + clientId: string + clientSecret: string + disableSignUp?: boolean + discoveryUrl?: string + authorizationUrl?: string + tokenUrl?: string + scopes?: string[] + // SSO-only: force the IdP account picker so admins notice when + // they're already signed in as a diff
check
Process completed with exit code 2.