@@ -66,6 +66,62 @@ export interface HonoPluginOptions {
6666 * - `@objectstack/rest` → CRUD, metadata, discovery, UI, batch
6767 * - `createDispatcherPlugin()` → auth, graphql, analytics, packages, etc.
6868 */
69+ /**
70+ * Check if an origin matches a pattern with wildcards.
71+ * Supports patterns like:
72+ * - "https://*.example.com" - matches any subdomain
73+ * - "http://localhost:*" - matches any port
74+ * - "https://*.objectui.org,https://*.objectstack.ai" - comma-separated patterns
75+ *
76+ * @param origin The origin to check (e.g., "https://app.example.com")
77+ * @param pattern The pattern to match against (supports * wildcard)
78+ * @returns true if origin matches the pattern
79+ */
80+ function matchOriginPattern ( origin : string , pattern : string ) : boolean {
81+ if ( pattern === '*' ) return true ;
82+ if ( pattern === origin ) return true ;
83+
84+ // Convert wildcard pattern to regex
85+ // Escape special regex characters except *
86+ const regexPattern = pattern
87+ . replace ( / [ . + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) // Escape special chars
88+ . replace ( / \* / g, '.*' ) ; // Convert * to .*
89+
90+ const regex = new RegExp ( `^${ regexPattern } $` ) ;
91+ return regex . test ( origin ) ;
92+ }
93+
94+ /**
95+ * Create a CORS origin matcher function that supports wildcard patterns.
96+ *
97+ * @param patterns Single pattern, array of patterns, or comma-separated patterns
98+ * @returns Function that returns the origin if it matches, or null/undefined
99+ */
100+ function createOriginMatcher (
101+ patterns : string | string [ ]
102+ ) : ( origin : string ) => string | undefined | null {
103+ // Normalize to array
104+ let patternList : string [ ] ;
105+ if ( typeof patterns === 'string' ) {
106+ // Handle comma-separated patterns
107+ patternList = patterns . includes ( ',' )
108+ ? patterns . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean )
109+ : [ patterns ] ;
110+ } else {
111+ patternList = patterns ;
112+ }
113+
114+ // Return matcher function
115+ return ( requestOrigin : string ) => {
116+ for ( const pattern of patternList ) {
117+ if ( matchOriginPattern ( requestOrigin , pattern ) ) {
118+ return requestOrigin ;
119+ }
120+ }
121+ return null ;
122+ } ;
123+ }
124+
69125export class HonoServerPlugin implements Plugin {
70126 name = 'com.objectstack.server.hono' ;
71127 type = 'server' ;
@@ -128,12 +184,27 @@ export class HonoServerPlugin implements Plugin {
128184 const credentials = corsOpts . credentials ?? ( process . env . CORS_CREDENTIALS !== 'false' ) ;
129185 const maxAge = corsOpts . maxAge ?? ( process . env . CORS_MAX_AGE ? parseInt ( process . env . CORS_MAX_AGE , 10 ) : 86400 ) ;
130186
131- // When credentials is true, browsers reject wildcard '*' for Access-Control-Allow-Origin.
132- // Use a function to reflect the request's Origin header instead.
187+ // Determine origin handler based on configuration
133188 let origin : string | string [ ] | ( ( origin : string ) => string | undefined | null ) ;
134- if ( credentials && configuredOrigin === '*' ) {
189+
190+ // Check if patterns contain wildcards (*, subdomain patterns, port patterns)
191+ const hasWildcard = ( patterns : string | string [ ] ) : boolean => {
192+ const list = Array . isArray ( patterns ) ? patterns : [ patterns ] ;
193+ return list . some ( p => p . includes ( '*' ) ) ;
194+ } ;
195+
196+ // When credentials is true, browsers reject wildcard '*' for Access-Control-Allow-Origin.
197+ // For wildcard patterns (like "https://*.example.com"), always use a matcher function.
198+ // For exact origins, we can pass them directly as string/array.
199+ if ( configuredOrigin === '*' && credentials ) {
200+ // Credentials mode with '*' - reflect the request origin
135201 origin = ( requestOrigin : string ) => requestOrigin || '*' ;
202+ } else if ( hasWildcard ( configuredOrigin ) ) {
203+ // Wildcard patterns (including better-auth style patterns like "https://*.objectui.org")
204+ // Use pattern matcher to support subdomain and port wildcards
205+ origin = createOriginMatcher ( configuredOrigin ) ;
136206 } else {
207+ // Exact origin(s) - pass through as-is
137208 origin = configuredOrigin ;
138209 }
139210
0 commit comments