11import { isIpLiteral , unwrapIpv6Brackets } from '@sim/security/ssrf'
2+ import type { OAuthService } from '@/lib/oauth/types'
23
34export type OciDestinationProvenance = 'static' | 'authenticated-discovery'
45
@@ -12,26 +13,43 @@ export interface OciRegion {
1213 readonly realm : OciRealm
1314}
1415
15- declare const validatedOciDestinationBrand : unique symbol
16+ declare const preparedOciEndpointBrand : unique symbol
1617
17- /** An OCI origin that passed both structural and service-owned hostname validation . */
18- export interface ValidatedOciDestination {
18+ /** An OCI endpoint prepared from a declarative product policy . */
19+ export interface OciPreparedEndpoint {
1920 readonly origin : string
2021 readonly hostname : string
21- readonly service : string
22+ readonly serviceId : OAuthService
23+ readonly serviceName : string
2224 readonly region : OciRegion
2325 readonly provenance : OciDestinationProvenance
24- readonly [ validatedOciDestinationBrand ] : true
26+ readonly [ preparedOciEndpointBrand ] : true
2527}
2628
27- declare const ociServiceHostnamePredicateBrand : unique symbol
29+ declare const ociEndpointPolicyBrand : unique symbol
2830
29- export type OciServiceHostnamePredicate = ( ( params : {
30- hostname : string
31- service : string
32- region : OciRegion
33- provenance : OciDestinationProvenance
34- } ) => boolean ) & { readonly [ ociServiceHostnamePredicateBrand ] : true }
31+ export interface OciStaticEndpointPolicy {
32+ readonly kind : 'static'
33+ readonly serviceId : OAuthService
34+ readonly serviceName : string
35+ readonly [ ociEndpointPolicyBrand ] : true
36+ }
37+
38+ export type OciDiscoverySource =
39+ | { readonly kind : 'header' ; readonly name : string }
40+ | { readonly kind : 'json' ; readonly path : readonly string [ ] }
41+
42+ export interface OciDiscoveredEndpointPolicy {
43+ readonly kind : 'authenticated-discovery'
44+ readonly serviceId : OAuthService
45+ readonly serviceName : string
46+ readonly responsePolicy : OciEndpointPolicy
47+ readonly source : OciDiscoverySource
48+ readonly allowRegionalHost : boolean
49+ readonly [ ociEndpointPolicyBrand ] : true
50+ }
51+
52+ export type OciEndpointPolicy = OciStaticEndpointPolicy | OciDiscoveredEndpointPolicy
3553
3654/**
3755 * Realm and region snapshot copied from `oci-common@2.140.0` files
@@ -179,17 +197,83 @@ export function resolveEffectiveOciRegion(defaultRegion: string, override?: stri
179197 return effective
180198}
181199
182- export function objectStorageOciHostname ( region : OciRegion ) : string {
183- return `objectstorage.${ region . id } .${ region . realm . domain } `
200+ function assertServiceName ( value : string ) : void {
201+ if ( ! / ^ [ a - z ] [ a - z 0 - 9 - ] { 0 , 62 } $ / . test ( value ) ) {
202+ throw new Error ( 'OCI endpoint policy service name is invalid' )
203+ }
204+ }
205+
206+ function assertDiscoverySource ( source : OciDiscoverySource ) : void {
207+ if ( source . kind === 'header' ) {
208+ if ( ! / ^ [ ! # $ % & ' * + . ^ _ ` | ~ 0 - 9 A - Z a - z - ] + $ / . test ( source . name ) ) {
209+ throw new Error ( 'OCI discovery header name is invalid' )
210+ }
211+ return
212+ }
213+ if (
214+ source . kind !== 'json' ||
215+ source . path . length === 0 ||
216+ source . path . length > 8 ||
217+ source . path . some (
218+ ( segment ) =>
219+ segment . length === 0 || segment . length > 128 || / [ \u0000 - \u001f \u007f ] / . test ( segment )
220+ )
221+ ) {
222+ throw new Error ( 'OCI discovery JSON path is invalid' )
223+ }
224+ }
225+
226+ /** Creates a frozen exact regional-host policy owned by one registered service. */
227+ export function createOciStaticEndpointPolicy ( params : {
228+ serviceId : OAuthService
229+ serviceName : string
230+ } ) : OciStaticEndpointPolicy {
231+ assertServiceName ( params . serviceName )
232+ return Object . freeze ( {
233+ kind : 'static' ,
234+ serviceId : params . serviceId ,
235+ serviceName : params . serviceName ,
236+ } ) as OciStaticEndpointPolicy
237+ }
238+
239+ /** Creates a frozen authenticated-discovery policy without executable hostname callbacks. */
240+ export function createOciDiscoveredEndpointPolicy ( params : {
241+ serviceId : OAuthService
242+ serviceName : string
243+ responsePolicy : OciEndpointPolicy
244+ source : OciDiscoverySource
245+ allowRegionalHost ?: boolean
246+ } ) : OciDiscoveredEndpointPolicy {
247+ assertServiceName ( params . serviceName )
248+ assertDiscoverySource ( params . source )
249+ if ( params . responsePolicy . serviceId !== params . serviceId ) {
250+ throw new Error ( 'OCI discovery source policy must have the same owning service' )
251+ }
252+ const source =
253+ params . source . kind === 'json'
254+ ? Object . freeze ( { ...params . source , path : Object . freeze ( [ ...params . source . path ] ) } )
255+ : Object . freeze ( { ...params . source , name : params . source . name . toLowerCase ( ) } )
256+ return Object . freeze ( {
257+ kind : 'authenticated-discovery' ,
258+ serviceId : params . serviceId ,
259+ serviceName : params . serviceName ,
260+ responsePolicy : params . responsePolicy ,
261+ source,
262+ allowRegionalHost : params . allowRegionalHost ?? false ,
263+ } ) as OciDiscoveredEndpointPolicy
264+ }
265+
266+ export function regionalOciHostname ( serviceName : string , region : OciRegion ) : string {
267+ assertServiceName ( serviceName )
268+ return `${ serviceName } .${ region . id } .${ region . realm . domain } `
184269}
185270
186- export function validateOciDestination ( params : {
271+ function validateOciOrigin ( params : {
187272 origin : string
188- service : string
273+ policy : OciEndpointPolicy
189274 region : OciRegion
190275 provenance : OciDestinationProvenance
191- isServiceHostname : OciServiceHostnamePredicate
192- } ) : ValidatedOciDestination {
276+ } ) : OciPreparedEndpoint {
193277 const knownRegion = getOciRegion ( params . region . id )
194278 if (
195279 knownRegion . realm . id !== params . region . realm . id ||
@@ -204,8 +288,7 @@ export function validateOciDestination(params: {
204288 throw new Error ( 'OCI destination must be a valid HTTPS origin' )
205289 }
206290 if (
207- ( params . provenance !== 'static' && params . provenance !== 'authenticated-discovery' ) ||
208- ! / ^ [ a - z ] [ a - z 0 - 9 - ] { 0 , 62 } $ / . test ( params . service ) ||
291+ params . policy . kind !== params . provenance ||
209292 url . protocol !== 'https:' ||
210293 url . port !== '' ||
211294 url . username !== '' ||
@@ -218,39 +301,51 @@ export function validateOciDestination(params: {
218301 ) {
219302 throw new Error ( 'OCI destination must be an exact HTTPS origin with the default port' )
220303 }
221- if (
222- ! params . isServiceHostname ( {
223- hostname : url . hostname ,
224- service : params . service ,
225- region : knownRegion ,
226- provenance : params . provenance ,
227- } )
228- ) {
304+ const regionalHostname = regionalOciHostname ( params . policy . serviceName , knownRegion )
305+ const hostnameMatches =
306+ params . provenance === 'static'
307+ ? url . hostname === regionalHostname
308+ : url . hostname . endsWith ( `.${ regionalHostname } ` ) ||
309+ ( params . policy . kind === 'authenticated-discovery' &&
310+ params . policy . allowRegionalHost &&
311+ url . hostname === regionalHostname )
312+ if ( ! hostnameMatches ) {
229313 throw new Error ( 'OCI destination hostname is not owned by the requested service' )
230314 }
231315 return {
232316 origin : url . origin ,
233317 hostname : url . hostname ,
234- service : params . service ,
318+ serviceId : params . policy . serviceId ,
319+ serviceName : params . policy . serviceName ,
235320 region : knownRegion ,
236321 provenance : params . provenance ,
237- } as ValidatedOciDestination
322+ } as OciPreparedEndpoint
238323}
239324
240- export const isObjectStorageOciHostname = ( ( { hostname, service, region } ) =>
241- service === 'objectstorage' &&
242- hostname === objectStorageOciHostname ( region ) ) as OciServiceHostnamePredicate
325+ /** Resolves a static policy exclusively from its service and validated region. */
326+ export function resolveStaticOciEndpoint (
327+ policy : OciStaticEndpointPolicy ,
328+ region : OciRegion
329+ ) : OciPreparedEndpoint {
330+ const hostname = regionalOciHostname ( policy . serviceName , region )
331+ return validateOciOrigin ( {
332+ origin : `https://${ hostname } ` ,
333+ policy,
334+ region,
335+ provenance : 'static' ,
336+ } )
337+ }
243338
244- export function objectStorageOciDestination (
339+ /** Structurally validates an origin extracted from an authenticated response. */
340+ export function resolveDiscoveredOciEndpoint (
341+ policy : OciDiscoveredEndpointPolicy ,
245342 region : OciRegion ,
246- provenance : OciDestinationProvenance = 'static'
247- ) : ValidatedOciDestination {
248- const hostname = objectStorageOciHostname ( region )
249- return validateOciDestination ( {
250- origin : `https://${ hostname } ` ,
251- service : 'objectstorage' ,
343+ origin : string
344+ ) : OciPreparedEndpoint {
345+ return validateOciOrigin ( {
346+ origin,
347+ policy,
252348 region,
253- provenance,
254- isServiceHostname : isObjectStorageOciHostname ,
349+ provenance : 'authenticated-discovery' ,
255350 } )
256351}
0 commit comments