Allowed Opaque Origins
By default, Stellate handles CORS (Cross-Origin Resource Sharing) automatically for standard web origins. However, certain environments use opaque origins that cannot be parsed as standard tuple origins by URL parsers. This causes Stellate to fall back to wildcard (*) CORS headers, which breaks credentialed requests.
The allowedOpaqueOrigins configuration option allows you to explicitly permit specific opaque origins, enabling proper CORS handling for browser extensions, hybrid mobile apps, and other non-standard environments.
What are Opaque Origins?
Opaque origins are non-standard origin schemes that browsers and hybrid app frameworks use internally. Unlike tuple origins (e.g., https://example.com), opaque origins cannot be parsed into the standard scheme://host:port format.
Common opaque origins include:
| Environment | Origin Format | Example |
|---|---|---|
| Chrome Extensions | chrome-extension://<id> | chrome-extension://abc123def456 |
| Firefox Extensions | moz-extension://<id> | moz-extension://xyz789 |
| Capacitor/Ionic Apps | capacitor://<host> | capacitor://localhost |
| Electron Apps | Custom protocols | app://electron |
The Problem
When a request arrives with an opaque origin, Stellate’s URL parser cannot process it as a tuple origin. The current behavior falls back to returning Access-Control-Allow-Origin: * in the response.
While * allows the request to complete, it prevents credentialed requests (requests with cookies or authorization headers) from working. Browsers require the exact origin to be echoed back for credentialed requests to succeed.
Without allowedOpaqueOrigins configured, credentialed requests from browser extensions and hybrid apps will fail with CORS errors, even though non-credentialed requests work fine.
Configuration
Add the allowedOpaqueOrigins property to your Stellate configuration to specify which opaque origins should be allowed.
- Name
allowedOpaqueOrigins- Type
- Description
An optional array of origin patterns that should be allowed for CORS. When a request’s
Originheader matches one of these patterns, Stellate echoes back the exact origin in theAccess-Control-Allow-Originresponse header.Pattern syntax:
- Exact match:
capacitor://localhost - Wildcard suffix:
chrome-extension://*(matches any Chrome extension)
This is an opt-in feature. If not configured, the existing fallback behavior (
*) remains unchanged.- Exact match:
stellate.ts
import { Config } from 'stellate'
const config: Config = {
config: {
name: 'my-service',
originUrl: 'https://my-api.example.com/graphql',
allowedOpaqueOrigins: [
'chrome-extension://*', // All Chrome extensions
'moz-extension://*', // All Firefox extensions
'capacitor://localhost', // Capacitor apps (exact)
],
},
}
export default configBehavior
The following table shows how different origins are handled based on your configuration:
| Origin Header | Config | Access-Control-Allow-Origin |
|---|---|---|
chrome-extension://abc123 | ['chrome-extension://*'] | chrome-extension://abc123 |
moz-extension://xyz | ['chrome-extension://*'] | * (not in allowed list) |
https://example.com | ['chrome-extension://*'] | https://example.com (tuple origin, works normally) |
chrome-extension://abc123 | Not configured | * (default fallback) |
capacitor://localhost | ['capacitor://localhost'] | capacitor://localhost |
capacitor://other | ['capacitor://localhost'] | * (exact match required) |
Standard tuple origins (like https://example.com) continue to work normally regardless of this configuration. The allowedOpaqueOrigins setting only affects how opaque origins are handled.
Use Cases
Browser Extensions
Chrome and Firefox extensions that make GraphQL requests to your Stellate service need their origins explicitly allowed:
const config: Config = {
config: {
name: 'my-service',
originUrl: 'https://api.example.com/graphql',
allowedOpaqueOrigins: [
'chrome-extension://*', // Allow all Chrome extensions
'moz-extension://*', // Allow all Firefox extensions
],
},
}If you want to restrict access to specific extensions, use exact matches:
allowedOpaqueOrigins: [
'chrome-extension://abcdefghijklmnop', // Only your extension
]Capacitor/Ionic Apps
Hybrid mobile apps built with Capacitor or Ionic use the capacitor:// protocol:
const config: Config = {
config: {
name: 'my-service',
originUrl: 'https://api.example.com/graphql',
allowedOpaqueOrigins: [
'capacitor://localhost', // Standard Capacitor origin
'capacitor://*', // Or allow all Capacitor origins
],
},
}Electron Apps
Desktop applications built with Electron may use custom protocol schemes:
const config: Config = {
config: {
name: 'my-service',
originUrl: 'https://api.example.com/graphql',
allowedOpaqueOrigins: [
'app://*', // Custom Electron protocol
'electron://*', // Alternative Electron protocol
],
},
}Security Considerations
Using wildcard patterns like chrome-extension://* allows any Chrome extension to make credentialed requests to your API. Consider the security implications for your specific use case.
Recommendations:
- For public APIs: Wildcard patterns are generally acceptable since any client can already access your API.
- For private APIs: Consider using exact matches for specific extension IDs or app identifiers you control.
- For sensitive operations: Combine this feature with other security measures like rate limiting and authentication.