security: widget workflow & MCP data hardening (5 fixes) (#43) #36
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.
|