Skip to content

Commit 2f3564a

Browse files
committed
refactor(oidc-client): move Micro helpers into micros.ts; utils.ts holds only pure utilities
Establish the convention: any function returning a Micro lives in authorize.request.micros.ts and ends in µ. authorize.request.utils.ts now contains only pure stateless utilities (type guards, error converters, URL builders) with no dependency on the Effect library.
1 parent eaaadfe commit 2f3564a

5 files changed

Lines changed: 156 additions & 154 deletions

File tree

packages/oidc-client/src/lib/authorize.request.micros.test.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,16 @@ import * as sdkOidc from '@forgerock/sdk-oidc';
1111
import * as sdkUtilities from '@forgerock/sdk-utilities';
1212

1313
import {
14-
buildParBody,
15-
validateParResponse,
16-
handleDispatchError,
17-
createAuthorizeUrlMicro,
18-
} from './authorize.request.utils.js';
19-
import {
14+
buildParBodyµ,
2015
buildParSlimUrlµ,
16+
createAuthorizeUrlµ,
2117
dispatchAuthorizeFetchµ,
2218
dispatchAuthorizeIframeµ,
2319
generateAuthValuesµ,
2420
generatePkceChallengeµ,
21+
handleDispatchErrorµ,
2522
storeAuthOptionsµ,
23+
validateParResponseµ,
2624
} from './authorize.request.micros.js';
2725

2826
import type { OidcConfig } from './config.types.js';
@@ -114,11 +112,11 @@ it.effect('generatePkceChallengeµ fails with auth_error when createChallenge th
114112
}),
115113
);
116114

117-
// ─── buildParBody ─────────────────────────────────────────────────────────────
115+
// ─── buildParBodyµ ─────────────────────────────────────────────────────────────
118116

119-
it.effect('buildParBody returns URLSearchParams with expected fields', () =>
117+
it.effect('buildParBodyµ returns URLSearchParams with expected fields', () =>
120118
Micro.gen(function* () {
121-
const params = yield* buildParBody(config, {}, 'challenge-abc', 'state-xyz');
119+
const params = yield* buildParBodyµ(config, {}, 'challenge-abc', 'state-xyz');
122120
expect(params.get('client_id')).toBe(clientId);
123121
expect(params.get('code_challenge')).toBe('challenge-abc');
124122
expect(params.get('state')).toBe('state-xyz');
@@ -127,19 +125,19 @@ it.effect('buildParBody returns URLSearchParams with expected fields', () =>
127125
}),
128126
);
129127

130-
it.effect('buildParBody includes prompt when provided', () =>
128+
it.effect('buildParBodyµ includes prompt when provided', () =>
131129
Micro.gen(function* () {
132-
const params = yield* buildParBody(config, {}, 'challenge-abc', 'state-xyz', 'login');
130+
const params = yield* buildParBodyµ(config, {}, 'challenge-abc', 'state-xyz', 'login');
133131
expect(params.get('prompt')).toBe('login');
134132
}),
135133
);
136134

137-
it.effect('buildParBody fails with auth_error when buildAuthorizeParams throws', () =>
135+
it.effect('buildParBodyµ fails with auth_error when buildAuthorizeParams throws', () =>
138136
Micro.gen(function* () {
139137
vi.spyOn(sdkOidc, 'buildAuthorizeParams').mockImplementation(() => {
140138
throw new Error('build failed');
141139
});
142-
const exit = yield* Micro.exit(buildParBody(config, {}, 'ch', 'st'));
140+
const exit = yield* Micro.exit(buildParBodyµ(config, {}, 'ch', 'st'));
143141
expect(Micro.exitIsFailure(exit)).toBe(true);
144142
if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return;
145143
expect(exit.cause.error.type).toBe('auth_error');
@@ -199,21 +197,21 @@ it.effect('storeAuthOptionsµ fails with unknown_error when store function throw
199197
}),
200198
);
201199

202-
// ─── validateParResponse ──────────────────────────────────────────────────────
200+
// ─── validateParResponseµ ──────────────────────────────────────────────────────
203201

204-
it.effect('validateParResponse succeeds when request_uri is present', () =>
202+
it.effect('validateParResponseµ succeeds when request_uri is present', () =>
205203
Micro.gen(function* () {
206-
const result = yield* validateParResponse({
204+
const result = yield* validateParResponseµ({
207205
data: { request_uri: 'urn:ietf:params:oauth:request_uri:xyz', expires_in: 60 },
208206
});
209207
expect(result.request_uri).toBe('urn:ietf:params:oauth:request_uri:xyz');
210208
}),
211209
);
212210

213-
it.effect('validateParResponse fails with network_error on RTK error', () =>
211+
it.effect('validateParResponseµ fails with network_error on RTK error', () =>
214212
Micro.gen(function* () {
215213
const exit = yield* Micro.exit(
216-
validateParResponse({
214+
validateParResponseµ({
217215
error: {
218216
status: 400,
219217
data: { error: 'invalid_client', error_description: 'bad creds', type: 'auth_error' },
@@ -226,19 +224,19 @@ it.effect('validateParResponse fails with network_error on RTK error', () =>
226224
}),
227225
);
228226

229-
it.effect('validateParResponse fails with network_error when request_uri is absent', () =>
227+
it.effect('validateParResponseµ fails with network_error when request_uri is absent', () =>
230228
Micro.gen(function* () {
231-
const exit = yield* Micro.exit(validateParResponse({ data: { expires_in: 60 } }));
229+
const exit = yield* Micro.exit(validateParResponseµ({ data: { expires_in: 60 } }));
232230
expect(Micro.exitIsFailure(exit)).toBe(true);
233231
if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return;
234232
expect(exit.cause.error.type).toBe('network_error');
235233
expect(exit.cause.error.error_description).toContain('request_uri');
236234
}),
237235
);
238236

239-
// ─── createAuthorizeUrlMicro ──────────────────────────────────────────────────
237+
// ─── createAuthorizeUrlµ ──────────────────────────────────────────────────
240238

241-
it.effect('createAuthorizeUrlMicro returns [url, options] tuple', () =>
239+
it.effect('createAuthorizeUrlµ returns [url, options] tuple', () =>
242240
Micro.gen(function* () {
243241
vi.stubGlobal('sessionStorage', sessionStorageStub);
244242
const opts = {
@@ -252,20 +250,17 @@ it.effect('createAuthorizeUrlMicro returns [url, options] tuple', () =>
252250
vi.spyOn(sdkOidc, 'createAuthorizeUrl').mockResolvedValue(
253251
'https://example.com/authorize?foo=bar',
254252
);
255-
const [url, returnedOpts] = yield* createAuthorizeUrlMicro(
256-
wellknown.authorization_endpoint,
257-
opts,
258-
);
253+
const [url, returnedOpts] = yield* createAuthorizeUrlµ(wellknown.authorization_endpoint, opts);
259254
expect(url).toBe('https://example.com/authorize?foo=bar');
260255
expect(returnedOpts).toBe(opts);
261256
}),
262257
);
263258

264-
it.effect('createAuthorizeUrlMicro fails with auth_error when createAuthorizeUrl rejects', () =>
259+
it.effect('createAuthorizeUrlµ fails with auth_error when createAuthorizeUrl rejects', () =>
265260
Micro.gen(function* () {
266261
vi.spyOn(sdkOidc, 'createAuthorizeUrl').mockRejectedValue(new Error('url build failed'));
267262
const exit = yield* Micro.exit(
268-
createAuthorizeUrlMicro(wellknown.authorization_endpoint, {
263+
createAuthorizeUrlµ(wellknown.authorization_endpoint, {
269264
clientId,
270265
redirectUri,
271266
scope,
@@ -279,12 +274,12 @@ it.effect('createAuthorizeUrlMicro fails with auth_error when createAuthorizeUrl
279274
}),
280275
);
281276

282-
// ─── handleDispatchError ──────────────────────────────────────────────────────
277+
// ─── handleDispatchErrorµ ──────────────────────────────────────────────────────
283278

284-
it.effect('handleDispatchError fails immediately for CONFIGURATION_ERROR', () =>
279+
it.effect('handleDispatchErrorµ fails immediately for CONFIGURATION_ERROR', () =>
285280
Micro.gen(function* () {
286281
const exit = yield* Micro.exit(
287-
handleDispatchError(
282+
handleDispatchErrorµ(
288283
{
289284
status: 'CUSTOM_ERROR',
290285
statusText: 'CONFIGURATION_ERROR',
@@ -301,13 +296,13 @@ it.effect('handleDispatchError fails immediately for CONFIGURATION_ERROR', () =>
301296
}),
302297
);
303298

304-
it.effect('handleDispatchError builds redirect URL for non-config errors', () =>
299+
it.effect('handleDispatchErrorµ builds redirect URL for non-config errors', () =>
305300
Micro.gen(function* () {
306301
vi.spyOn(sdkOidc, 'createAuthorizeUrl').mockResolvedValue(
307302
'https://example.com/authorize?error=login_required',
308303
);
309304
const exit = yield* Micro.exit(
310-
handleDispatchError(
305+
handleDispatchErrorµ(
311306
{
312307
status: 400,
313308
data: {

packages/oidc-client/src/lib/authorize.request.micros.ts

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
import { generateAndStoreAuthUrlValues } from '@forgerock/sdk-oidc';
7+
import {
8+
buildAuthorizeParams,
9+
createAuthorizeUrl,
10+
generateAndStoreAuthUrlValues,
11+
} from '@forgerock/sdk-oidc';
812
import { createChallenge } from '@forgerock/sdk-utilities';
913
import { Micro } from 'effect';
1014

1115
import {
1216
buildParAuthorizeUrl,
13-
handleDispatchError,
17+
hasPushRequestUri,
18+
isFetchBaseQueryError,
19+
toDispatchError,
1420
type PromptValue,
1521
} from './authorize.request.utils.js';
1622

@@ -78,6 +84,113 @@ export const storeAuthOptionsµ = (
7884
});
7985
};
8086

87+
// ─── PAR body / URL builders ─────────────────────────────────────────────────
88+
89+
export const buildParBodyµ = (
90+
config: OidcConfig,
91+
parBodyOptions: OptionalAuthorizeOptions,
92+
challenge: string,
93+
state: string,
94+
prompt?: PromptValue,
95+
): Micro.Micro<URLSearchParams, AuthorizationError, never> => {
96+
return Micro.try({
97+
try: () =>
98+
buildAuthorizeParams({
99+
clientId: config.clientId,
100+
redirectUri: config.redirectUri,
101+
scope: config.scope || 'openid',
102+
responseType: config.responseType || 'code',
103+
...parBodyOptions,
104+
challenge,
105+
state,
106+
...(prompt && { prompt }),
107+
}),
108+
catch: (err): AuthorizationError => ({
109+
error: 'PAR_PARAM_BUILD_ERROR',
110+
error_description: err instanceof Error ? err.message : 'Failed to build PAR parameters',
111+
type: 'auth_error',
112+
}),
113+
});
114+
};
115+
116+
export const createAuthorizeUrlµ = (
117+
path: string,
118+
options: GetAuthorizationUrlOptions,
119+
): Micro.Micro<[string, GetAuthorizationUrlOptions], AuthorizationError, never> => {
120+
return Micro.tryPromise({
121+
try: async () =>
122+
[await createAuthorizeUrl(path, { ...options, prompt: 'none' }), options] as [
123+
string,
124+
GetAuthorizationUrlOptions,
125+
],
126+
catch: (error): AuthorizationError => ({
127+
error: 'AuthorizationUrlError',
128+
error_description:
129+
error instanceof Error ? error.message : 'Error creating authorization URL',
130+
type: 'auth_error',
131+
}),
132+
});
133+
};
134+
135+
export const buildAuthorizeRedirectUrlµ = (
136+
res: { error: string; error_description: string },
137+
wellknown: WellknownResponse,
138+
options: GetAuthorizationUrlOptions,
139+
): Micro.Micro<never, AuthorizationError, never> => {
140+
return Micro.tryPromise({
141+
try: () => createAuthorizeUrl(wellknown.authorization_endpoint, { ...options }),
142+
catch: (error): AuthorizationError => ({
143+
error: 'AuthorizationUrlError',
144+
error_description:
145+
error instanceof Error ? error.message : 'Error creating authorization URL',
146+
type: 'auth_error',
147+
}),
148+
}).pipe(
149+
Micro.flatMap((url) =>
150+
Micro.fail({
151+
error: res.error,
152+
error_description: res.error_description,
153+
type: 'auth_error',
154+
redirectUrl: url,
155+
} as const),
156+
),
157+
);
158+
};
159+
160+
export const validateParResponseµ = (result: {
161+
error?: FetchBaseQueryError | SerializedError;
162+
data?: unknown;
163+
}): Micro.Micro<{ request_uri: string; expires_in: number }, AuthorizationError, never> => {
164+
if (result.error) {
165+
return Micro.fail(toDispatchError(result.error));
166+
}
167+
if (!hasPushRequestUri(result.data)) {
168+
return Micro.fail({
169+
error: 'PAR_ERROR',
170+
error_description: "PAR response missing required 'request_uri' field",
171+
type: 'network_error',
172+
} as const);
173+
}
174+
const d = result.data as { request_uri: string; expires_in?: number };
175+
return Micro.succeed({ request_uri: d.request_uri, expires_in: d.expires_in ?? 60 });
176+
};
177+
178+
export const handleDispatchErrorµ = (
179+
error: FetchBaseQueryError | SerializedError,
180+
wellknown: WellknownResponse,
181+
options: GetAuthorizationUrlOptions,
182+
): Micro.Micro<never, AuthorizationError, never> => {
183+
const errorDetails = toDispatchError(error);
184+
const isConfigError =
185+
isFetchBaseQueryError(error) &&
186+
'statusText' in error &&
187+
error.statusText === 'CONFIGURATION_ERROR';
188+
189+
return isConfigError
190+
? Micro.fail(errorDetails)
191+
: buildAuthorizeRedirectUrlµ(errorDetails, wellknown, options);
192+
};
193+
81194
// ─── PAR POST ────────────────────────────────────────────────────────────────
82195

83196
export const dispatchParRequestµ = (
@@ -134,7 +247,7 @@ export const dispatchAuthorizeFetchµ = (
134247
}).pipe(
135248
Micro.flatMap(({ error, data }) => {
136249
if (error) {
137-
return handleDispatchError(error, wellknown, options);
250+
return handleDispatchErrorµ(error, wellknown, options);
138251
}
139252
if (data?.authorizeResponse) {
140253
return Micro.succeed(data.authorizeResponse);
@@ -165,7 +278,7 @@ export const dispatchAuthorizeIframeµ = (
165278
}).pipe(
166279
Micro.flatMap(({ error, data }) => {
167280
if (error) {
168-
return handleDispatchError(error, wellknown, options);
281+
return handleDispatchErrorµ(error, wellknown, options);
169282
}
170283
const d = data as { code?: unknown; state?: unknown } | undefined;
171284
if (d !== undefined && typeof d.code === 'string' && typeof d.state === 'string') {

packages/oidc-client/src/lib/authorize.request.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,18 @@
77
import { CustomLogger } from '@forgerock/sdk-logger';
88
import { Micro } from 'effect';
99

10+
import { buildAuthorizeOptions } from './authorize.request.utils.js';
1011
import {
11-
buildAuthorizeOptions,
12-
buildParBody,
13-
createAuthorizeUrlMicro,
14-
validateParResponse,
15-
} from './authorize.request.utils.js';
16-
import {
12+
buildParBodyµ,
1713
buildParSlimUrlµ,
14+
createAuthorizeUrlµ,
1815
dispatchAuthorizeFetchµ,
1916
dispatchAuthorizeIframeµ,
2017
dispatchParRequestµ,
2118
generateAuthValuesµ,
2219
generatePkceChallengeµ,
2320
storeAuthOptionsµ,
21+
validateParResponseµ,
2422
} from './authorize.request.micros.js';
2523

2624
import type { GetAuthorizationUrlOptions, WellknownResponse } from '@forgerock/sdk-types';
@@ -99,15 +97,15 @@ export function parAuthorizeµ(
9997
return Micro.gen(function* () {
10098
const [authUrlOptions, storeOptions] = yield* generateAuthValuesµ(config, wellknown, options);
10199
const challenge = yield* generatePkceChallengeµ(authUrlOptions.verifier);
102-
const body = yield* buildParBody(
100+
const body = yield* buildParBodyµ(
103101
config,
104102
parBodyOptions,
105103
challenge,
106104
authUrlOptions.state,
107105
prompt,
108106
);
109107
const parResult = yield* dispatchParRequestµ(store, parEndpoint, body);
110-
const { request_uri, expires_in } = yield* validateParResponse(parResult);
108+
const { request_uri, expires_in } = yield* validateParResponseµ(parResult);
111109
if (expires_in < 30) {
112110
yield* Micro.sync(() =>
113111
log.warn(
@@ -177,7 +175,7 @@ export function authorizeµ(
177175
);
178176

179177
const [path, opts] = buildAuthorizeOptions(wellknown, config, options);
180-
const standardFlow = createAuthorizeUrlMicro(path, opts).pipe(
178+
const standardFlow = createAuthorizeUrlµ(path, opts).pipe(
181179
Micro.tap(([url]) => log.debug('Authorize URL created', url)),
182180
Micro.tapError((err) => Micro.sync(() => log.error('Error creating authorize URL', err))),
183181
Micro.flatMap(([url, dispatchOpts]) =>

packages/oidc-client/src/lib/authorize.request.utils.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,14 +273,14 @@ it('hasPushRequestUri returns false when request_uri is missing', () => {
273273
expect(hasPushRequestUri('string')).toBe(false);
274274
});
275275

276-
// ─── validateParResponse ─────────────────────────────────────────────────────
276+
// ─── validateParResponseµ ────────────────────────────────────────────────────
277277

278-
import { validateParResponse } from './authorize.request.utils.js';
278+
import { validateParResponseµ } from './authorize.request.micros.js';
279279

280-
it.effect('validateParResponse with SerializedError preserves error message', () =>
280+
it.effect('validateParResponseµ with SerializedError preserves error message', () =>
281281
Micro.gen(function* () {
282282
const serializedError = { name: 'Error', message: 'network timeout', code: 'FETCH_ERROR' };
283-
const exit = yield* Micro.exit(validateParResponse({ error: serializedError }));
283+
const exit = yield* Micro.exit(validateParResponseµ({ error: serializedError }));
284284
expect(Micro.exitIsFailure(exit)).toBe(true);
285285
if (!Micro.exitIsFailure(exit) || !Micro.causeIsFail(exit.cause)) return;
286286
// Should surface the actual message, not generic "Unknown_Error"

0 commit comments

Comments
 (0)