From b8c7c09597cc310cb38d1a34c86517a9ff9169f8 Mon Sep 17 00:00:00 2001 From: Ian Rees Date: Thu, 21 Aug 2025 02:49:28 -0700 Subject: [PATCH 1/4] More flexible Polly config options --- src/runtime/config/pollySetup.ts | 99 +++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/src/runtime/config/pollySetup.ts b/src/runtime/config/pollySetup.ts index a7d060b3..098d8f6a 100644 --- a/src/runtime/config/pollySetup.ts +++ b/src/runtime/config/pollySetup.ts @@ -6,7 +6,20 @@ import FsPersister from '@pollyjs/persister-fs' Polly.register(FetchAdapter) Polly.register(FsPersister) -export function setupPolly (recordingName: string) { +interface PollyConfig { + // Patterns for requests that should pass through without recording + passthroughPatterns: string[] + // Patterns for requests that should be recorded/replayed + recordPatterns: string[] + // Whether to allow unmatched requests (default: false - will fail on unmatched) + allowUnmatched?: boolean + // Custom logic for complex passthrough decisions + shouldPassthrough?: (req: any) => boolean + // Custom logic for complex record decisions + shouldRecord?: (req: any) => boolean +} + +export function setupPolly (recordingName: string, config: PollyConfig) { const isCI = process.env.CI === 'true' const polly = new Polly(recordingName, { adapters: ['fetch'], @@ -26,23 +39,93 @@ export function setupPolly (recordingName: string) { }, }) - // Passthrough requests to localhost and 127.0.0.1 + // Helper to check if URL matches any pattern + const matchesAnyPattern = (url: string, patterns: string[]): boolean => { + return patterns.some(pattern => { + try { + return new RegExp(pattern).test(url) + } catch (e) { + // If pattern is not valid regex, treat as literal string match + return url.includes(pattern) + } + }) + } + + // Passthrough filter polly.server.any().filter((req) => { - console.warn(`Polly checking filter: ${req.url}`) - return (req.url.startsWith('http://localhost') || req.url.startsWith('http://127.0.0.1')) - }) - .passthrough() + const url = req.url + + // Check custom passthrough function first + if (config.shouldPassthrough?.(req)) { + console.log(`Polly: Custom passthrough logic matched: ${url}`) + return true + } + + // Check passthrough patterns + if (matchesAnyPattern(url, config.passthroughPatterns)) { + console.log(`Polly: Passthrough pattern matched: ${url}`) + return true + } + + return false + }).passthrough() + + // Record filter + polly.server.any().filter((req) => { + const url = req.url + + // Check custom record function first + if (config.shouldRecord?.(req)) { + console.log(`Polly: Custom record logic matched: ${url}`) + return true + } + + // Check record patterns + if (matchesAnyPattern(url, config.recordPatterns)) { + console.log(`Polly: Record pattern matched: ${url}`) + return true + } + + return false + }).recordAndReplay() + + // Fail on unmatched requests unless explicitly allowed + if (!config.allowUnmatched) { + polly.server.any().filter((req) => { + const url = req.url + const shouldPassthrough = config.shouldPassthrough?.(req) || + matchesAnyPattern(url, config.passthroughPatterns) + const shouldRecord = config.shouldRecord?.(req) || + matchesAnyPattern(url, config.recordPatterns) + + const isUnmatched = !shouldPassthrough && !shouldRecord + + if (isUnmatched) { + console.error(`Polly: Unmatched request detected: ${url}`) + return true + } + + return false + }).intercept((req, res) => { + res.status(400).json({ + error: 'Request blocked by Polly', + message: `URL ${req.url} is not configured for passthrough or recording. Add to passthroughPatterns, recordPatterns, or set allowUnmatched: true`, + url: req.url + }) + }) + } // Add request sanitization to remove sensitive headers from recordings polly.server.any().on('beforePersist', (req, recording) => { - console.log(`Sanitizing request for recording`) + console.log(`Polly: Sanitizing request for recording`) const sensitiveHeaders = ['authorization', 'apikey'] recording.request.headers = recording.request.headers.map((header: any) => { if (sensitiveHeaders.includes(header.name?.toLowerCase())) { - header.value = '' + header.value = '[REDACTED]' } return header }) }) + return polly } From c88b9629f3c7a371d8222f729c5e39a53ba62f87 Mon Sep 17 00:00:00 2001 From: Ian Rees Date: Thu, 21 Aug 2025 03:04:52 -0700 Subject: [PATCH 2/4] Tidy --- src/runtime/config/pollySetup.ts | 104 ++++++++++--------------------- 1 file changed, 32 insertions(+), 72 deletions(-) diff --git a/src/runtime/config/pollySetup.ts b/src/runtime/config/pollySetup.ts index 098d8f6a..2b2d6149 100644 --- a/src/runtime/config/pollySetup.ts +++ b/src/runtime/config/pollySetup.ts @@ -8,14 +8,14 @@ Polly.register(FsPersister) interface PollyConfig { // Patterns for requests that should pass through without recording - passthroughPatterns: string[] + passthroughPatterns?: string[] // Patterns for requests that should be recorded/replayed - recordPatterns: string[] + recordPatterns?: string[] // Whether to allow unmatched requests (default: false - will fail on unmatched) allowUnmatched?: boolean // Custom logic for complex passthrough decisions shouldPassthrough?: (req: any) => boolean - // Custom logic for complex record decisions + // Custom logic for complex record decisions shouldRecord?: (req: any) => boolean } @@ -39,73 +39,34 @@ export function setupPolly (recordingName: string, config: PollyConfig) { }, }) - // Helper to check if URL matches any pattern - const matchesAnyPattern = (url: string, patterns: string[]): boolean => { - return patterns.some(pattern => { - try { - return new RegExp(pattern).test(url) - } catch (e) { - // If pattern is not valid regex, treat as literal string match - return url.includes(pattern) + // Add request sanitization to remove sensitive headers from recordings + polly.server.any().on('beforePersist', (req, recording) => { + console.log(`Polly: Sanitizing request for recording`) + const sensitiveHeaders = ['authorization', 'apikey'] + recording.request.headers = recording.request.headers.map((header: any) => { + if (sensitiveHeaders.includes(header.name?.toLowerCase())) { + header.value = '[REDACTED]' } + return header }) - } + }) - // Passthrough filter + // Record filter polly.server.any().filter((req) => { - const url = req.url - - // Check custom passthrough function first - if (config.shouldPassthrough?.(req)) { - console.log(`Polly: Custom passthrough logic matched: ${url}`) - return true - } - - // Check passthrough patterns - if (matchesAnyPattern(url, config.passthroughPatterns)) { - console.log(`Polly: Passthrough pattern matched: ${url}`) - return true - } - - return false - }).passthrough() + return config.shouldRecord?.(req) || matchesAnyPattern(req.url, config.recordPatterns) + }) - // Record filter + // Passthrough filter polly.server.any().filter((req) => { - const url = req.url - - // Check custom record function first - if (config.shouldRecord?.(req)) { - console.log(`Polly: Custom record logic matched: ${url}`) - return true - } - - // Check record patterns - if (matchesAnyPattern(url, config.recordPatterns)) { - console.log(`Polly: Record pattern matched: ${url}`) - return true - } - - return false - }).recordAndReplay() + return config.shouldPassthrough?.(req) || matchesAnyPattern(req.url, config.passthroughPatterns) + }).passthrough() // Fail on unmatched requests unless explicitly allowed if (!config.allowUnmatched) { polly.server.any().filter((req) => { - const url = req.url - const shouldPassthrough = config.shouldPassthrough?.(req) || - matchesAnyPattern(url, config.passthroughPatterns) - const shouldRecord = config.shouldRecord?.(req) || - matchesAnyPattern(url, config.recordPatterns) - - const isUnmatched = !shouldPassthrough && !shouldRecord - - if (isUnmatched) { - console.error(`Polly: Unmatched request detected: ${url}`) - return true - } - - return false + const passthrough = config.shouldPassthrough?.(req) || matchesAnyPattern(req.url, config.passthroughPatterns) + const recorded = config.shouldRecord?.(req) || matchesAnyPattern(req.url, config.recordPatterns) + return !passthrough && !recorded }).intercept((req, res) => { res.status(400).json({ error: 'Request blocked by Polly', @@ -114,18 +75,17 @@ export function setupPolly (recordingName: string, config: PollyConfig) { }) }) } + return polly +} - // Add request sanitization to remove sensitive headers from recordings - polly.server.any().on('beforePersist', (req, recording) => { - console.log(`Polly: Sanitizing request for recording`) - const sensitiveHeaders = ['authorization', 'apikey'] - recording.request.headers = recording.request.headers.map((header: any) => { - if (sensitiveHeaders.includes(header.name?.toLowerCase())) { - header.value = '[REDACTED]' - } - return header - }) +// Helper to check if URL matches any pattern +function matchesAnyPattern (url: string, patterns: string[]): boolean { + return (patterns || []).some((pattern) => { + try { + return new RegExp(pattern).test(url) + } catch (e) { + // If pattern is not valid regex, treat as literal string match + return url.includes(pattern) + } }) - - return polly } From 745798b75bd57c3451b818a6c7be80fc47493a9a Mon Sep 17 00:00:00 2001 From: Ian Rees Date: Wed, 1 Oct 2025 19:08:43 -0700 Subject: [PATCH 3/4] Exports --- src/runtime/config/pollySetup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runtime/config/pollySetup.ts b/src/runtime/config/pollySetup.ts index 2b2d6149..0225018d 100644 --- a/src/runtime/config/pollySetup.ts +++ b/src/runtime/config/pollySetup.ts @@ -6,7 +6,9 @@ import FsPersister from '@pollyjs/persister-fs' Polly.register(FetchAdapter) Polly.register(FsPersister) -interface PollyConfig { +export { Polly } + +export interface PollyConfig { // Patterns for requests that should pass through without recording passthroughPatterns?: string[] // Patterns for requests that should be recorded/replayed From c550d8dfb4f1350e77deeed8ea80bc48e0aa33e2 Mon Sep 17 00:00:00 2001 From: Ian Rees Date: Wed, 1 Oct 2025 19:10:50 -0700 Subject: [PATCH 4/4] More exports --- src/runtime/auth/index.ts | 4 ++-- src/runtime/config/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runtime/auth/index.ts b/src/runtime/auth/index.ts index 331426ec..68123643 100644 --- a/src/runtime/auth/index.ts +++ b/src/runtime/auth/index.ts @@ -1,2 +1,2 @@ -export { useUser, clearUser } from './user' -export { useAuthHeaders, useApiEndpoint, useLogin, useLogout, useLoginGate } from './auth' +export * from './user' +export * from './auth' diff --git a/src/runtime/config/index.ts b/src/runtime/config/index.ts index 2eced981..f0b16d73 100644 --- a/src/runtime/config/index.ts +++ b/src/runtime/config/index.ts @@ -1,2 +1,2 @@ -export { setupPolly } from './pollySetup' -export { eslintRules, stylisticConfig, eslintConfig } from './eslint' +export * from './pollySetup' +export * from './eslint'