@@ -139,16 +139,8 @@ export function validateEnterpriseAuthorizationProfile(profile: Profile): Enterp
139139 }
140140
141141 export type ApiKeyStoreConfig =
142- | { type: ' inline' ; keys: InlineApiKeyEntry [] }
143- | {
144- type: ' sasanka' ;
145- /** Base URL for the Sasanka API. Defaults to https://sasanka.seznam.net */
146- base_url? : string ;
147- /** Resolve base_url from env var. */
148- base_url_from_env? : string ;
149- /** Request timeout in ms for /users/me. Default: 5000. */
150- timeout_ms? : number ;
151- };
142+ | { type: ' inline' ; keys: InlineApiKeyEntry [] };
143+ // Phase 4 adds: | { type: 'sasanka'; base_url?: string; base_url_from_env?: string; timeout_ms?: number }
152144
153145 export interface ClientAuthGateConfig {
154146 /** 'required' (default): reject session if no valid identity resolved. 'optional': allow anonymous sessions. */
@@ -205,20 +197,23 @@ export function validateEnterpriseAuthorizationProfile(profile: Profile): Enterp
205197 <verify >
206198 <automated >npm run generate - schemas && npm run check - schema - sync && npm run typecheck < / automated >
207199 < / verify >
208- <done >All three commands exit 0. ClientAuthGateConfig , ApiKeyStoreConfig , ClientAuthJwtConfig appear in src / generated - schemas .ts . SessionData .clientPrincipal typed as AuthorizedPrincipal . ClientAuthGateError exported from src /core /errors .ts . HttpProfileContext .client_auth_gate and HttpTransportConfig .client_auth_gate present .</done >
200+ <done >All three commands exit 0. ClientAuthGateConfig and ApiKeyStoreConfig appear in src / generated - schemas .ts ( NO ClientAuthJwtConfig — Phase 4 ) . SessionData .clientPrincipal typed as AuthorizedPrincipal . ClientAuthGateError exported from src /core /errors .ts . HttpProfileContext .client_auth_gate and HttpTransportConfig .client_auth_gate present .</done >
209201< / task >
210202
211203< task type = " auto" tdd = " true" >
212204 <name >Task 2 : Implement validateClientAuthGateProfile () validator and wire into profile - loader < / name >
213205 <files >src / profile / client - auth - gate - validator .ts , src / profile / client - auth - gate - validator .test .ts , src / profile / profile - loader .ts < / files >
214206 <behavior >
215- - Valid profile with api_keys inline (non - empty keys array , each entry has key_from_env + subject ) returns config
216- - Valid profile with api_keys sasanka returns config
207+ - Valid profile with api_keys inline (non - empty keys array , each entry has key_from_env + subject , env vars set ) returns config
208+ - Profile with api_keys . type = ' sasanka' throws ClientAuthGateError ( not supported until Phase 4 )
217209 - Profile with api_keys .type = ' vault' throws ClientAuthGateError (unknown backend )
218210 - Profile with api_keys inline and empty keys [] throws ClientAuthGateError
211+ - Profile with api_keys inline where key_from_env is " " throws ClientAuthGateError (trim check )
212+ - Profile with api_keys inline where key_from_env env var is not set throws ClientAuthGateError (fail-fast)
219213 - Profile with no client_auth_gate field returns undefined (no - op )
214+ - Profile with OAuth interceptor AND client_auth_gate throws ClientAuthGateError (mutual exclusion )
220215 - env var resolution — mode_from_env: set to ' optional' → mode resolved to ' optional' ; unset → throws ClientAuthGateError
221- - env var resolution — base_url_from_env (sasanka) : set → base_url resolved ; unset → throws ClientAuthGateError
216+ - env var resolution — mode_from_env set to invalid value ' admin ' → throws ClientAuthGateError
222217 < / behavior >
223218 <action >
224219 Create src / profile / client - auth - gate - validator .ts following the pattern of enterprise - profile - validator .ts :
@@ -229,9 +224,8 @@ export function validateEnterpriseAuthorizationProfile(profile: Profile): Enterp
229224 import { ClientAuthGateError } from '../core/errors.js';
230225 import type { ClientAuthGateConfig, Profile } from '../types/profile.js';
231226
232- const DEFAULT_SASANKA_BASE_URL = 'https://sasanka.seznam.net';
233- const DEFAULT_TIMEOUT_MS = 5000;
234- const ALLOWED_API_KEY_TYPES = ['inline', 'sasanka'] as const;
227+ const ALLOWED_API_KEY_TYPES = ['inline'] as const;
228+ // Phase 4 adds 'sasanka' to ALLOWED_API_KEY_TYPES
235229
236230 function resolveEnv(value: string | undefined, fromEnv: string | undefined, fieldPath: string): string | undefined {
237231 if (value) return value;
@@ -247,6 +241,14 @@ export function validateEnterpriseAuthorizationProfile(profile: Profile): Enterp
247241 const config = profile .client_auth_gate ;
248242 if (! config ) return undefined ;
249243
244+ // Mutual exclusion: client_auth_gate cannot coexist with OAuth interceptors
245+ const auths = profile .interceptors ?.auth
246+ ? (Array .isArray (profile .interceptors .auth ) ? profile .interceptors .auth : [profile .interceptors .auth ])
247+ : [];
248+ if (auths .some (a => a .type === ' oauth' )) {
249+ throw new ClientAuthGateError (' client_auth_gate cannot be combined with OAuth interceptors; configure one inbound auth method per profile' );
250+ }
251+
250252 // Resolve mode
251253 const mode = resolveEnv (config .mode , config .mode_from_env , ' client_auth_gate.mode' ) ?? ' required' ;
252254 if (mode !== ' required' && mode !== ' optional' ) {
@@ -266,20 +268,16 @@ export function validateEnterpriseAuthorizationProfile(profile: Profile): Enterp
266268 throw new ClientAuthGateError (' client_auth_gate.api_keys.keys must be a non-empty array for type=inline' );
267269 }
268270 for (const entry of apiKeys .keys ) {
269- if (! entry .key_from_env ) throw new ClientAuthGateError (' client_auth_gate.api_keys.keys[].key_from_env is required' );
270- if (! entry .subject ) throw new ClientAuthGateError (' client_auth_gate.api_keys.keys[].subject is required' );
271- }
272- }
273- if (apiKeys .type === ' sasanka' ) {
274- const baseUrl = resolveEnv (apiKeys .base_url , apiKeys .base_url_from_env , ' client_auth_gate.api_keys.base_url' ) ?? DEFAULT_SASANKA_BASE_URL ;
275- if (process .env .NODE_ENV !== ' test' ) {
276- const parsed = new URL (baseUrl );
277- if (parsed .protocol !== ' https:' ) {
278- throw new ClientAuthGateError (' client_auth_gate.api_keys.base_url must use https' );
271+ if (! entry .key_from_env ?.trim ()) throw new ClientAuthGateError (' client_auth_gate.api_keys.keys[].key_from_env is required' );
272+ if (! entry .subject ?.trim ()) throw new ClientAuthGateError (' client_auth_gate.api_keys.keys[].subject is required' );
273+ // Fail-fast: catch misconfigured env vars at load time so operators get
274+ // an actionable error instead of silent all-key rejection at runtime.
275+ if (! process .env [entry .key_from_env ]) {
276+ throw new ClientAuthGateError (` client_auth_gate.api_keys.keys[].key_from_env: env var '${entry .key_from_env }' is not set ` );
279277 }
280278 }
281- apiKeys = { ... apiKeys , base_url: baseUrl , timeout_ms: apiKeys .timeout_ms ?? DEFAULT_TIMEOUT_MS };
282279 }
280+ // Phase 4 adds sasanka branch here
283281 }
284282
285283 if (! apiKeys && mode === ' required' ) {
0 commit comments