feat: add wildcard CORS origin pattern matching for better-auth compatibility#1152
feat: add wildcard CORS origin pattern matching for better-auth compatibility#1152
Conversation
- Add matchOriginPattern() and createOriginMatcher() functions to support wildcard patterns - Support subdomain wildcards (e.g., https://*.objectui.org) - Support port wildcards (e.g., http://localhost:*) - Support comma-separated patterns for better-auth compatibility - Add comprehensive unit tests for pattern matching - Update CORS middleware to use pattern matching for wildcard origins - Maintain backward compatibility with exact origin matching Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/a7ac1ca8-a54d-49a5-8469-ec453eb41b8f Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/a7ac1ca8-a54d-49a5-8469-ec453eb41b8f Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
There was a problem hiding this comment.
Pull request overview
Adds wildcard-capable CORS origin matching to @objectstack/plugin-hono-server so configurations like better-auth trustedOrigins (e.g., https://*.objectui.org, http://localhost:*) can be used safely and conveniently.
Changes:
- Introduces wildcard origin matching helpers and wires them into the Hono CORS middleware setup.
- Expands the plugin test suite with CORS-related coverage and updates the adapter mock to include
rawApp.use. - Updates the plugin README with wildcard CORS configuration examples (options + env vars).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/plugins/plugin-hono-server/src/pattern-matcher.test.ts | Adds unit tests for wildcard origin matching behavior (currently using local re-implementations). |
| packages/plugins/plugin-hono-server/src/hono-plugin.ts | Adds wildcard matching helpers and applies them when configuring hono/cors origin handling. |
| packages/plugins/plugin-hono-server/src/hono-plugin.test.ts | Updates mocks and adds CORS-related plugin init tests. |
| packages/plugins/plugin-hono-server/README.md | Documents wildcard CORS usage patterns and env var examples. |
| function matchOriginPattern(origin: string, pattern: string): boolean { | ||
| if (pattern === '*') return true; | ||
| if (pattern === origin) return true; | ||
|
|
||
| // Convert wildcard pattern to regex | ||
| // Escape special regex characters except * | ||
| const regexPattern = pattern | ||
| .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars | ||
| .replace(/\*/g, '.*'); // Convert * to .* | ||
|
|
||
| const regex = new RegExp(`^${regexPattern}$`); | ||
| return regex.test(origin); | ||
| } | ||
|
|
||
| /** | ||
| * Create a CORS origin matcher function that supports wildcard patterns. | ||
| * | ||
| * @param patterns Single pattern, array of patterns, or comma-separated patterns | ||
| * @returns Function that returns the origin if it matches, or null/undefined | ||
| */ | ||
| function createOriginMatcher( | ||
| patterns: string | string[] | ||
| ): (origin: string) => string | undefined | null { | ||
| // Normalize to array | ||
| let patternList: string[]; | ||
| if (typeof patterns === 'string') { | ||
| // Handle comma-separated patterns | ||
| patternList = patterns.includes(',') | ||
| ? patterns.split(',').map(s => s.trim()).filter(Boolean) | ||
| : [patterns]; | ||
| } else { | ||
| patternList = patterns; | ||
| } | ||
|
|
||
| // Return matcher function | ||
| return (requestOrigin: string) => { | ||
| for (const pattern of patternList) { | ||
| if (matchOriginPattern(requestOrigin, pattern)) { | ||
| return requestOrigin; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| } |
There was a problem hiding this comment.
This test file re-implements matchOriginPattern/createOriginMatcher locally instead of importing the production implementation, so the tests can pass even if the real CORS matcher in hono-plugin.ts is broken or diverges over time. Extract these helpers into a shared module (e.g., src/pattern-matcher.ts) and import them from both hono-plugin.ts and this test to ensure the tests exercise the shipped behavior.
| * Check if an origin matches a pattern with wildcards. | ||
| * Supports patterns like: | ||
| * - "https://*.example.com" - matches any subdomain | ||
| * - "http://localhost:*" - matches any port | ||
| * - "https://*.objectui.org,https://*.objectstack.ai" - comma-separated patterns | ||
| * | ||
| * @param origin The origin to check (e.g., "https://app.example.com") | ||
| * @param pattern The pattern to match against (supports * wildcard) |
There was a problem hiding this comment.
The docstring for matchOriginPattern() says it supports comma-separated patterns, but the function matches a single pattern and does not split on commas (comma handling is done in createOriginMatcher). Please update this comment to avoid misleading future readers about what this function accepts.
| * Check if an origin matches a pattern with wildcards. | |
| * Supports patterns like: | |
| * - "https://*.example.com" - matches any subdomain | |
| * - "http://localhost:*" - matches any port | |
| * - "https://*.objectui.org,https://*.objectstack.ai" - comma-separated patterns | |
| * | |
| * @param origin The origin to check (e.g., "https://app.example.com") | |
| * @param pattern The pattern to match against (supports * wildcard) | |
| * Check if an origin matches a single pattern with wildcards. | |
| * Supports patterns like: | |
| * - "https://*.example.com" - matches any subdomain | |
| * - "http://localhost:*" - matches any port | |
| * | |
| * @param origin The origin to check (e.g., "https://app.example.com") | |
| * @param pattern The single pattern to match against (supports * wildcard) |
| /** | ||
| * Check if an origin matches a pattern with wildcards. | ||
| * Supports patterns like: | ||
| * - "https://*.example.com" - matches any subdomain | ||
| * - "http://localhost:*" - matches any port | ||
| * - "https://*.objectui.org,https://*.objectstack.ai" - comma-separated patterns | ||
| * | ||
| * @param origin The origin to check (e.g., "https://app.example.com") | ||
| * @param pattern The pattern to match against (supports * wildcard) | ||
| * @returns true if origin matches the pattern | ||
| */ | ||
| function matchOriginPattern(origin: string, pattern: string): boolean { | ||
| if (pattern === '*') return true; | ||
| if (pattern === origin) return true; | ||
|
|
||
| // Convert wildcard pattern to regex | ||
| // Escape special regex characters except * | ||
| const regexPattern = pattern | ||
| .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars | ||
| .replace(/\*/g, '.*'); // Convert * to .* | ||
|
|
||
| const regex = new RegExp(`^${regexPattern}$`); | ||
| return regex.test(origin); | ||
| } | ||
|
|
||
| /** | ||
| * Create a CORS origin matcher function that supports wildcard patterns. | ||
| * | ||
| * @param patterns Single pattern, array of patterns, or comma-separated patterns | ||
| * @returns Function that returns the origin if it matches, or null/undefined | ||
| */ | ||
| function createOriginMatcher( | ||
| patterns: string | string[] | ||
| ): (origin: string) => string | undefined | null { | ||
| // Normalize to array | ||
| let patternList: string[]; | ||
| if (typeof patterns === 'string') { | ||
| // Handle comma-separated patterns | ||
| patternList = patterns.includes(',') | ||
| ? patterns.split(',').map(s => s.trim()).filter(Boolean) | ||
| : [patterns]; | ||
| } else { | ||
| patternList = patterns; | ||
| } | ||
|
|
||
| // Return matcher function | ||
| return (requestOrigin: string) => { | ||
| for (const pattern of patternList) { | ||
| if (matchOriginPattern(requestOrigin, pattern)) { | ||
| return requestOrigin; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| } |
There was a problem hiding this comment.
matchOriginPattern()/createOriginMatcher() are embedded in hono-plugin.ts, duplicating logic and making it hard to reuse or unit test the real implementation (tests currently copy/paste their own version). Move these helpers into a dedicated module and import them here; this reduces drift risk and keeps hono-plugin.ts focused on plugin wiring.
| function createOriginMatcher( | ||
| patterns: string | string[] | ||
| ): (origin: string) => string | undefined | null { | ||
| // Normalize to array | ||
| let patternList: string[]; | ||
| if (typeof patterns === 'string') { | ||
| // Handle comma-separated patterns | ||
| patternList = patterns.includes(',') | ||
| ? patterns.split(',').map(s => s.trim()).filter(Boolean) | ||
| : [patterns]; | ||
| } else { | ||
| patternList = patterns; | ||
| } | ||
|
|
||
| // Return matcher function | ||
| return (requestOrigin: string) => { | ||
| for (const pattern of patternList) { | ||
| if (matchOriginPattern(requestOrigin, pattern)) { | ||
| return requestOrigin; | ||
| } | ||
| } | ||
| return null; | ||
| }; |
There was a problem hiding this comment.
createOriginMatcher() compiles a new RegExp on every request for every pattern (via matchOriginPattern()), which is unnecessary overhead under load. Consider pre-compiling patterns once when building the matcher (e.g., convert patterns to RegExp instances up front) and then only running regex.test() per request.
The Hono server plugin's CORS middleware did not support wildcard origin patterns like
https://*.objectui.org,https://*.objectstack.aiused by better-auth'strustedOriginsconfiguration.Changes
Pattern Matching Implementation
matchOriginPattern()andcreateOriginMatcher()functions to support wildcard patterns in CORS originshttps://*.objectui.org)http://localhost:*)https://*.objectui.org,https://*.objectstack.ai)CORS Middleware Enhancement
cors.originsoption andCORS_ORIGINenvironment variableTesting & Documentation
Usage