@qlover/oauth-wrapper 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,791 @@
1
+ // src/client/interface/OAuthConfig.ts
2
+ var OAuthWrapperEndpoints = {
3
+ authorize: "/oauth/authorize",
4
+ token: "/oauth/token",
5
+ revoke: "/oauth/revoke",
6
+ userinfo: "/userinfo"
7
+ };
8
+ var DEFAULT_OAUTH_AUTHORIZE_PATH = OAuthWrapperEndpoints.authorize;
9
+ var DEFAULT_PKCE_STORAGE_KEY = "oauth-wrapper-pkcesession";
10
+ var defaultOAuthClientConfig = {
11
+ locale: "en",
12
+ localeIn: "path",
13
+ localeQueryParam: "local",
14
+ localeHeader: "Accept-Language",
15
+ serverUrl: "",
16
+ clientId: "",
17
+ scope: "openid profile email",
18
+ redirectPath: "oauth/callback"
19
+ };
20
+ function resolveOAuthClientConfig(partial) {
21
+ return {
22
+ ...defaultOAuthClientConfig,
23
+ ...partial
24
+ };
25
+ }
26
+
27
+ // src/client/OAuthGateway.ts
28
+ import {
29
+ HttpMethods
30
+ } from "@qlover/fe-corekit";
31
+
32
+ // src/client/oauthUrlUtils.ts
33
+ function resolveLocaleIn(options) {
34
+ if (options.localeIn) {
35
+ return options.localeIn;
36
+ }
37
+ return options.locale ? "path" : "none";
38
+ }
39
+ function buildAuthorizeSearchParams(input) {
40
+ const params = new URLSearchParams({
41
+ response_type: "code",
42
+ client_id: input.clientId,
43
+ redirect_uri: input.redirectUri,
44
+ scope: input.scope,
45
+ state: input.state,
46
+ code_challenge: input.codeChallenge,
47
+ code_challenge_method: "S256"
48
+ });
49
+ return params;
50
+ }
51
+ function mergeOAuthAuthorizationConfig(params, baseConfig) {
52
+ const serverUrl = (params?.serverUrl ?? baseConfig?.serverUrl)?.trim();
53
+ const clientId = (params?.clientId ?? baseConfig?.clientId)?.trim();
54
+ if (!serverUrl || !clientId) {
55
+ return null;
56
+ }
57
+ return {
58
+ serverUrl: serverUrl.replace(/\/$/, ""),
59
+ clientId,
60
+ scope: params?.scope?.trim() || baseConfig?.scope || defaultOAuthClientConfig.scope,
61
+ redirectPath: params?.redirectPath?.trim() || baseConfig?.redirectPath || defaultOAuthClientConfig.redirectPath
62
+ };
63
+ }
64
+ function buildOAuthRedirectUri(redirectPath, options = {}) {
65
+ const origin = (options.origin ?? (typeof window !== "undefined" ? window.location.origin : "")).replace(/\/$/, "");
66
+ const prefix = (options.routerPrefix ?? "").replace(/\/$/, "");
67
+ const path = redirectPath.replace(/^\//, "");
68
+ const localeIn = resolveLocaleIn(options);
69
+ const segments = [
70
+ prefix,
71
+ localeIn === "path" && options.locale ? options.locale : "",
72
+ path
73
+ ].filter(Boolean);
74
+ let url = `${origin}/${segments.join("/")}`.replace(/([^:]\/)\/+/g, "$1");
75
+ if (options.locale && localeIn === "query") {
76
+ const param = options.localeQueryParam ?? "locale";
77
+ const joiner = url.includes("?") ? "&" : "?";
78
+ url = `${url}${joiner}${param}=${encodeURIComponent(options.locale)}`;
79
+ }
80
+ return url;
81
+ }
82
+ function formatOAuthAuthorizeUrl(serverUrl, authorizePath, searchParams, localeOptions = {}) {
83
+ const base = serverUrl.replace(/\/$/, "");
84
+ const path = authorizePath.startsWith("/") ? authorizePath : `/${authorizePath}`;
85
+ const localeIn = resolveLocaleIn(localeOptions);
86
+ const url = localeOptions.locale && localeIn === "path" ? `${base}/${localeOptions.locale}${path}` : `${base}${path}`;
87
+ const params = new URLSearchParams(searchParams);
88
+ if (localeOptions.locale && localeIn === "query") {
89
+ params.set(
90
+ localeOptions.localeQueryParam ?? "locale",
91
+ localeOptions.locale
92
+ );
93
+ }
94
+ const qs = params.toString();
95
+ return qs ? `${url}?${qs}` : url;
96
+ }
97
+ function buildOAuthAuthorizeUrl(input) {
98
+ const { config, authorizePath, state, codeChallenge, redirectPath, scope } = input;
99
+ const redirectUri = buildOAuthRedirectUri(
100
+ redirectPath ?? config.redirectPath,
101
+ input
102
+ );
103
+ const searchParams = buildAuthorizeSearchParams({
104
+ clientId: config.clientId,
105
+ redirectUri,
106
+ scope: scope ?? config.scope,
107
+ state,
108
+ codeChallenge
109
+ });
110
+ return formatOAuthAuthorizeUrl(
111
+ config.serverUrl,
112
+ authorizePath,
113
+ searchParams,
114
+ input
115
+ );
116
+ }
117
+ function buildOAuthLocaleHeaders(options = {}) {
118
+ if (!options.locale || options.localeIn !== "header") {
119
+ return {};
120
+ }
121
+ const header = options.localeHeader ?? "Accept-Language";
122
+ return { [header]: options.locale };
123
+ }
124
+
125
+ // src/client/parseEnvelope.ts
126
+ import { z as z2 } from "zod";
127
+
128
+ // src/client/types.ts
129
+ import { z } from "zod";
130
+ var OAuthUserInfoSchema = z.object({
131
+ sub: z.string(),
132
+ email: z.email(),
133
+ name: z.string(),
134
+ roles: z.array(z.string()).optional()
135
+ });
136
+ var OAuthTokenResponseSchema = z.object({
137
+ access_token: z.string(),
138
+ token_type: z.literal("Bearer").optional(),
139
+ expires_in: z.number().optional(),
140
+ refresh_token: z.string().optional(),
141
+ scope: z.string().optional()
142
+ });
143
+ var OAuthTokenErrorSchema = z.object({
144
+ error: z.string(),
145
+ error_description: z.string().optional(),
146
+ error_id: z.string().optional()
147
+ });
148
+
149
+ // src/client/parseEnvelope.ts
150
+ var OAuthAppApiSuccessSchema = (dataSchema) => z2.object({
151
+ success: z2.literal(true),
152
+ requestId: z2.string().optional(),
153
+ data: dataSchema
154
+ });
155
+ var OAuthAppApiErrorSchema = z2.object({
156
+ success: z2.literal(false),
157
+ id: z2.string().optional(),
158
+ message: z2.string().optional(),
159
+ requestId: z2.string().optional()
160
+ });
161
+ function formatAppApiError(json, fallback) {
162
+ const wrapped = OAuthAppApiErrorSchema.safeParse(json);
163
+ if (wrapped.success) {
164
+ return wrapped.data.message?.trim() || wrapped.data.id || fallback;
165
+ }
166
+ const oauth = OAuthTokenErrorSchema.safeParse(json);
167
+ if (oauth.success) {
168
+ return oauth.data.error_description?.trim() || oauth.data.error;
169
+ }
170
+ return fallback;
171
+ }
172
+ function parseOAuthTokenResponse(json) {
173
+ const direct = OAuthTokenResponseSchema.safeParse(json);
174
+ if (direct.success) {
175
+ return direct.data;
176
+ }
177
+ const wrapped = OAuthAppApiSuccessSchema(OAuthTokenResponseSchema).safeParse(
178
+ json
179
+ );
180
+ if (wrapped.success) {
181
+ return wrapped.data.data;
182
+ }
183
+ throw new Error(
184
+ formatAppApiError(json, "Invalid token response from authorization server")
185
+ );
186
+ }
187
+ function parseOAuthTokenError(json, fallback) {
188
+ return formatAppApiError(json, fallback);
189
+ }
190
+ function parseOAuthUserInfoResponse(json) {
191
+ const direct = OAuthUserInfoSchema.safeParse(json);
192
+ if (direct.success) {
193
+ return direct.data;
194
+ }
195
+ const wrapped = OAuthAppApiSuccessSchema(OAuthUserInfoSchema).safeParse(json);
196
+ if (wrapped.success) {
197
+ return wrapped.data.data;
198
+ }
199
+ throw new Error(
200
+ formatAppApiError(
201
+ json,
202
+ "Invalid userinfo response from authorization server"
203
+ )
204
+ );
205
+ }
206
+
207
+ // src/core/utils/pkce.ts
208
+ var PKCE_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
209
+ function generatePkceVerifier(length = 64) {
210
+ const size = Math.min(128, Math.max(43, length));
211
+ const values = new Uint8Array(size);
212
+ crypto.getRandomValues(values);
213
+ let result = "";
214
+ for (let i = 0; i < size; i++) {
215
+ result += PKCE_CHARSET[values[i] % PKCE_CHARSET.length];
216
+ }
217
+ return result;
218
+ }
219
+ function base64UrlEncode(buffer) {
220
+ const bytes = new Uint8Array(buffer);
221
+ let binary = "";
222
+ for (const byte of bytes) {
223
+ binary += String.fromCharCode(byte);
224
+ }
225
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
226
+ }
227
+ async function computePkceS256Challenge(verifier) {
228
+ const data = new TextEncoder().encode(verifier);
229
+ const digest = await crypto.subtle.digest("SHA-256", data);
230
+ return base64UrlEncode(digest);
231
+ }
232
+ function randomOAuthState() {
233
+ const bytes = new Uint8Array(16);
234
+ crypto.getRandomValues(bytes);
235
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
236
+ }
237
+
238
+ // src/client/OAuthGateway.ts
239
+ function parseCallbackParams(params) {
240
+ if (params instanceof URLSearchParams) {
241
+ return {
242
+ code: params.get("code") ?? void 0,
243
+ state: params.get("state") ?? void 0,
244
+ error: params.get("error") ?? void 0,
245
+ error_description: params.get("error_description") ?? void 0
246
+ };
247
+ }
248
+ return params ?? {};
249
+ }
250
+ function callbackDedupeKey(params) {
251
+ if (!params.code || !params.state) {
252
+ return null;
253
+ }
254
+ return `${params.code}:${params.state}`;
255
+ }
256
+ var OAuthGateway = class {
257
+ constructor(options) {
258
+ this.options = options;
259
+ }
260
+ callbackInflight = /* @__PURE__ */ new Map();
261
+ get localeOptions() {
262
+ return {
263
+ locale: this.options.locale,
264
+ localeIn: this.options.localeIn,
265
+ localeQueryParam: this.options.localeQueryParam,
266
+ localeHeader: this.options.localeHeader
267
+ };
268
+ }
269
+ get authorizePath() {
270
+ return this.options.authorizePath ?? DEFAULT_OAUTH_AUTHORIZE_PATH;
271
+ }
272
+ get urlOptions() {
273
+ return {
274
+ origin: this.options.origin,
275
+ routerPrefix: this.options.routerPrefix
276
+ };
277
+ }
278
+ getServerUrl() {
279
+ const serverUrl = this.options.config?.serverUrl;
280
+ if (!serverUrl) {
281
+ throw new Error("OAuth is not configured");
282
+ }
283
+ return serverUrl;
284
+ }
285
+ buildRequestHeaders(headers) {
286
+ return {
287
+ ...buildOAuthLocaleHeaders(this.localeOptions),
288
+ ...headers
289
+ };
290
+ }
291
+ async requestJson(path, init, errorFallback) {
292
+ const requester = this.options.requester;
293
+ if (requester) {
294
+ const response = await requester.request({
295
+ url: path,
296
+ method: init.method,
297
+ headers: init.headers,
298
+ data: init.body
299
+ });
300
+ if (response.status < 200 || response.status >= 300) {
301
+ throw new Error(parseOAuthTokenError(response.data, errorFallback));
302
+ }
303
+ return response.data;
304
+ }
305
+ const res = await fetch(`${this.getServerUrl()}${path}`, {
306
+ method: init.method,
307
+ headers: init.headers,
308
+ body: init.body
309
+ });
310
+ const json = await res.json();
311
+ if (!res.ok) {
312
+ throw new Error(parseOAuthTokenError(json, errorFallback));
313
+ }
314
+ return json;
315
+ }
316
+ /**
317
+ * @override
318
+ */
319
+ async authorize(paramsOrUrl) {
320
+ if (typeof paramsOrUrl === "string") {
321
+ window.location.assign(paramsOrUrl);
322
+ return;
323
+ }
324
+ const config = mergeOAuthAuthorizationConfig(
325
+ paramsOrUrl,
326
+ this.options.config
327
+ );
328
+ if (!config) {
329
+ throw new Error("OAuth is not configured");
330
+ }
331
+ const session = this.options.store.loadPkceSession();
332
+ if (!session) {
333
+ throw new Error("PKCE session is required before authorize");
334
+ }
335
+ const url = buildOAuthAuthorizeUrl({
336
+ config,
337
+ authorizePath: this.authorizePath,
338
+ redirectPath: paramsOrUrl.redirectPath,
339
+ scope: paramsOrUrl.scope,
340
+ state: session.state,
341
+ codeChallenge: await computePkceS256Challenge(session.codeVerifier),
342
+ ...this.urlOptions,
343
+ ...this.localeOptions
344
+ });
345
+ if (paramsOrUrl.target === "_blank") {
346
+ window.open(url, "_blank", "noopener,noreferrer");
347
+ return;
348
+ }
349
+ window.location.assign(url);
350
+ }
351
+ /**
352
+ * @override
353
+ */
354
+ async getToken(request) {
355
+ const body = new URLSearchParams({
356
+ grant_type: request.grant_type,
357
+ code: request.code,
358
+ redirect_uri: request.redirect_uri,
359
+ client_id: request.client_id,
360
+ code_verifier: request.code_verifier
361
+ });
362
+ const json = await this.requestJson(
363
+ OAuthWrapperEndpoints.token,
364
+ {
365
+ method: HttpMethods.POST,
366
+ headers: this.buildRequestHeaders({
367
+ "Content-Type": "application/x-www-form-urlencoded"
368
+ }),
369
+ body: body.toString()
370
+ },
371
+ "Token exchange failed"
372
+ );
373
+ return parseOAuthTokenResponse(json);
374
+ }
375
+ /**
376
+ * @override
377
+ */
378
+ async getUserInfo(accessToken) {
379
+ const json = await this.requestJson(
380
+ OAuthWrapperEndpoints.userinfo,
381
+ {
382
+ method: HttpMethods.GET,
383
+ headers: this.buildRequestHeaders({
384
+ Authorization: `Bearer ${accessToken}`
385
+ })
386
+ },
387
+ "Failed to fetch userinfo"
388
+ );
389
+ return parseOAuthUserInfoResponse(json);
390
+ }
391
+ /**
392
+ * @override
393
+ */
394
+ async oAuthWrapperCallback(params) {
395
+ const parsed = parseCallbackParams(params);
396
+ const dedupeKey = callbackDedupeKey(parsed);
397
+ if (dedupeKey) {
398
+ const inflight = this.callbackInflight.get(dedupeKey);
399
+ if (inflight) {
400
+ return inflight;
401
+ }
402
+ }
403
+ const promise = this.runOAuthWrapperCallback(parsed);
404
+ if (dedupeKey) {
405
+ this.callbackInflight.set(dedupeKey, promise);
406
+ promise.finally(() => {
407
+ this.callbackInflight.delete(dedupeKey);
408
+ });
409
+ }
410
+ return promise;
411
+ }
412
+ async runOAuthWrapperCallback(params) {
413
+ if (params.error) {
414
+ throw new Error(
415
+ params.error_description?.trim() || params.error || "Authorization denied"
416
+ );
417
+ }
418
+ if (!params.code || !params.state) {
419
+ throw new Error("Missing authorization code or state");
420
+ }
421
+ const store = this.options.store;
422
+ const session = store.loadPkceSession();
423
+ if (!session) {
424
+ throw new Error("OAuth session expired; please sign in again");
425
+ }
426
+ if (session.state !== params.state) {
427
+ store.clearPkceSession();
428
+ throw new Error("Invalid state (possible CSRF)");
429
+ }
430
+ const currentLocale = this.options.locale;
431
+ if (session.locale != null && currentLocale != null && session.locale !== currentLocale) {
432
+ store.clearPkceSession();
433
+ throw new Error("Locale mismatch during OAuth callback");
434
+ }
435
+ const config = this.options.config;
436
+ if (!config) {
437
+ throw new Error("OAuth is not configured");
438
+ }
439
+ const redirectUri = buildOAuthRedirectUri(
440
+ config.redirectPath ?? defaultOAuthClientConfig.redirectPath,
441
+ { ...this.urlOptions, ...this.localeOptions }
442
+ );
443
+ try {
444
+ const tokens = await this.getToken({
445
+ grant_type: "authorization_code",
446
+ code: params.code,
447
+ redirect_uri: redirectUri,
448
+ client_id: config.clientId,
449
+ code_verifier: session.codeVerifier
450
+ });
451
+ const userinfo = await this.getUserInfo(tokens.access_token);
452
+ return this.options.mapUser(
453
+ userinfo,
454
+ tokens.access_token,
455
+ tokens.refresh_token
456
+ );
457
+ } finally {
458
+ store.clearPkceSession();
459
+ }
460
+ }
461
+ /**
462
+ * @override
463
+ */
464
+ async revokeToken(options) {
465
+ const config = this.options.config;
466
+ if (!config || !options?.refreshToken) {
467
+ return;
468
+ }
469
+ const body = new URLSearchParams({
470
+ token: options.refreshToken,
471
+ token_type_hint: "refresh_token",
472
+ client_id: config.clientId
473
+ });
474
+ await this.postRevokeToken(body);
475
+ }
476
+ async postRevokeToken(body) {
477
+ const path = OAuthWrapperEndpoints.revoke;
478
+ const headers = this.buildRequestHeaders({
479
+ "Content-Type": "application/x-www-form-urlencoded"
480
+ });
481
+ const requester = this.options.requester;
482
+ if (requester) {
483
+ await requester.request({
484
+ url: path,
485
+ method: HttpMethods.POST,
486
+ headers,
487
+ data: body.toString()
488
+ });
489
+ return;
490
+ }
491
+ await fetch(`${this.getServerUrl()}${path}`, {
492
+ method: HttpMethods.POST,
493
+ headers,
494
+ body: body.toString()
495
+ });
496
+ }
497
+ };
498
+
499
+ // src/client/SessionStorage.ts
500
+ var sessionStorage = {
501
+ setItem(key, value) {
502
+ window.sessionStorage.setItem(key, value);
503
+ },
504
+ getItem(key, defaultValue) {
505
+ return window.sessionStorage.getItem(key) ?? defaultValue;
506
+ },
507
+ removeItem(key) {
508
+ window.sessionStorage.removeItem(key);
509
+ },
510
+ clear() {
511
+ window.sessionStorage.clear();
512
+ }
513
+ };
514
+
515
+ // src/client/PKCESessionStore.ts
516
+ function createDefaultPkceStorage() {
517
+ return {
518
+ setItem(key, value) {
519
+ sessionStorage.setItem(key, JSON.stringify(value));
520
+ },
521
+ getItem(key) {
522
+ const raw = sessionStorage.getItem(key);
523
+ if (!raw) {
524
+ return null;
525
+ }
526
+ try {
527
+ const parsed = JSON.parse(raw);
528
+ if (typeof parsed.state === "string" && typeof parsed.codeVerifier === "string" && (parsed.locale === void 0 || typeof parsed.locale === "string")) {
529
+ return parsed;
530
+ }
531
+ } catch {
532
+ }
533
+ return null;
534
+ },
535
+ removeItem(key) {
536
+ sessionStorage.removeItem(key);
537
+ },
538
+ clear() {
539
+ sessionStorage.clear();
540
+ }
541
+ };
542
+ }
543
+ var PKCESessionStore = class {
544
+ pkceStorage;
545
+ pkceStorageKey;
546
+ constructor(options) {
547
+ this.pkceStorage = options?.pkceStorage ?? createDefaultPkceStorage();
548
+ this.pkceStorageKey = options?.pkceStorageKey ?? DEFAULT_PKCE_STORAGE_KEY;
549
+ }
550
+ savePkceSession(session) {
551
+ this.pkceStorage.setItem(this.pkceStorageKey, session);
552
+ }
553
+ loadPkceSession() {
554
+ return this.pkceStorage.getItem(this.pkceStorageKey, null);
555
+ }
556
+ clearPkceSession() {
557
+ this.pkceStorage.removeItem(this.pkceStorageKey);
558
+ }
559
+ };
560
+
561
+ // src/client/OAuthClient.ts
562
+ var OAUTH_CLIENT_CONFIG_KEYS = [
563
+ "serverUrl",
564
+ "clientId",
565
+ "scope",
566
+ "redirectPath",
567
+ "origin",
568
+ "routerPrefix",
569
+ "locale",
570
+ "localeIn",
571
+ "localeQueryParam",
572
+ "localeHeader"
573
+ ];
574
+ function pickClientConfig(options) {
575
+ const fromOptions = Object.fromEntries(
576
+ OAUTH_CLIENT_CONFIG_KEYS.filter((key) => options[key] !== void 0).map(
577
+ (key) => [key, options[key]]
578
+ )
579
+ );
580
+ return {
581
+ ...fromOptions,
582
+ ...options.config
583
+ };
584
+ }
585
+ var OAuthClient = class {
586
+ serviceName;
587
+ config;
588
+ authorizePath;
589
+ mapUser;
590
+ requester;
591
+ logger;
592
+ pkceStore;
593
+ gateway;
594
+ constructor(options) {
595
+ const {
596
+ serviceName = "OAuthClient",
597
+ gateway,
598
+ mapUser,
599
+ requester,
600
+ logger,
601
+ authorizePath = DEFAULT_OAUTH_AUTHORIZE_PATH,
602
+ pkceStorage,
603
+ pkceStorageKey
604
+ } = options;
605
+ this.serviceName = serviceName;
606
+ this.config = resolveOAuthClientConfig(pickClientConfig(options));
607
+ this.pkceStore = new PKCESessionStore({ pkceStorage, pkceStorageKey });
608
+ this.mapUser = mapUser;
609
+ this.requester = requester;
610
+ this.logger = logger;
611
+ this.authorizePath = authorizePath;
612
+ this.gateway = gateway ?? new OAuthGateway({
613
+ store: this.pkceStore,
614
+ config: this.authorizationConfig,
615
+ mapUser: mapUser ?? ((userinfo) => {
616
+ return userinfo;
617
+ }),
618
+ requester,
619
+ authorizePath,
620
+ origin: this.config.origin,
621
+ routerPrefix: this.config.routerPrefix,
622
+ locale: this.config.locale,
623
+ localeIn: this.config.localeIn,
624
+ localeQueryParam: this.config.localeQueryParam,
625
+ localeHeader: this.config.localeHeader
626
+ });
627
+ }
628
+ patchConfig(config) {
629
+ Object.assign(this.config, config);
630
+ }
631
+ get authorizationConfig() {
632
+ const serverUrl = this.config.serverUrl?.trim();
633
+ const clientId = this.config.clientId?.trim();
634
+ if (!serverUrl || !clientId) {
635
+ return null;
636
+ }
637
+ return {
638
+ serverUrl: serverUrl.replace(/\/$/, ""),
639
+ clientId,
640
+ scope: this.config.scope || defaultOAuthClientConfig.scope,
641
+ redirectPath: this.config.redirectPath || defaultOAuthClientConfig.redirectPath
642
+ };
643
+ }
644
+ get urlOptions() {
645
+ const { origin, routerPrefix } = this.config;
646
+ return { origin, routerPrefix };
647
+ }
648
+ get localeOptions() {
649
+ const { locale, localeIn, localeQueryParam, localeHeader } = this.config;
650
+ return { locale, localeIn, localeQueryParam, localeHeader };
651
+ }
652
+ getGateway() {
653
+ return this.gateway;
654
+ }
655
+ getLogger() {
656
+ return this.logger;
657
+ }
658
+ /**
659
+ * @override
660
+ */
661
+ isConfigured() {
662
+ return this.authorizationConfig != null;
663
+ }
664
+ /**
665
+ * @override
666
+ */
667
+ async startOAuthLogin(params) {
668
+ const config = mergeOAuthAuthorizationConfig(
669
+ params,
670
+ this.authorizationConfig
671
+ );
672
+ if (!config) {
673
+ throw new Error("OAuth is not configured");
674
+ }
675
+ const verifier = generatePkceVerifier();
676
+ const challenge = await computePkceS256Challenge(verifier);
677
+ const state = randomOAuthState();
678
+ const redirectPath = params?.redirectPath ?? config.redirectPath;
679
+ this.pkceStore.savePkceSession({
680
+ state,
681
+ codeVerifier: verifier,
682
+ locale: this.config.locale
683
+ });
684
+ const redirectUri = buildOAuthRedirectUri(redirectPath, {
685
+ ...this.urlOptions,
686
+ ...this.localeOptions
687
+ });
688
+ const url = buildOAuthAuthorizeUrl({
689
+ config,
690
+ authorizePath: this.authorizePath,
691
+ redirectPath: params?.redirectPath,
692
+ scope: params?.scope,
693
+ state,
694
+ codeChallenge: challenge,
695
+ ...this.urlOptions,
696
+ ...this.localeOptions
697
+ });
698
+ this.logger?.debug?.(`[${String(this.serviceName)}] startOAuthLogin`, {
699
+ locale: this.config.locale,
700
+ redirectUri,
701
+ statePreview: `${state.slice(0, 8)}\u2026`,
702
+ pkceSaved: this.pkceStore.loadPkceSession() != null
703
+ });
704
+ if (params?.target === "_blank") {
705
+ window.open(url, "_blank", "noopener,noreferrer");
706
+ return;
707
+ }
708
+ window.location.assign(url);
709
+ }
710
+ async completeOAuthCallback(params) {
711
+ const session = this.pkceStore.loadPkceSession();
712
+ const callbackState = params instanceof URLSearchParams ? params.get("state") : params?.state;
713
+ this.logger?.debug?.(
714
+ `[${String(this.serviceName)}] completeOAuthCallback`,
715
+ {
716
+ hasPkceSession: session != null,
717
+ callbackStatePreview: callbackState ? `${callbackState.slice(0, 8)}\u2026` : null,
718
+ sessionStatePreview: session?.state ? `${session.state.slice(0, 8)}\u2026` : null,
719
+ locale: this.config.locale
720
+ }
721
+ );
722
+ try {
723
+ const result = await this.gateway.oAuthWrapperCallback(params);
724
+ this.logger?.debug?.(
725
+ `[${String(this.serviceName)}] completeOAuthCallback succeeded`
726
+ );
727
+ return result;
728
+ } catch (error) {
729
+ this.logger?.warn?.(
730
+ `[${String(this.serviceName)}] completeOAuthCallback failed`,
731
+ {
732
+ message: error instanceof Error ? error.message : String(error),
733
+ hasPkceSessionAfterFailure: this.pkceStore.loadPkceSession() != null
734
+ }
735
+ );
736
+ throw error;
737
+ }
738
+ }
739
+ parseOAuthCallbackSearchParams(searchParams) {
740
+ return {
741
+ code: searchParams.get("code") ?? void 0,
742
+ state: searchParams.get("state") ?? void 0,
743
+ error: searchParams.get("error") ?? void 0,
744
+ error_description: searchParams.get("error_description") ?? void 0
745
+ };
746
+ }
747
+ /**
748
+ * @override
749
+ */
750
+ async revokeToken(refreshToken) {
751
+ if (!this.isConfigured()) {
752
+ return;
753
+ }
754
+ await this.gateway.revokeToken({
755
+ refreshToken: refreshToken ?? void 0
756
+ });
757
+ }
758
+ /**
759
+ * Fetch user profile from the OAuth userinfo endpoint and map it via {@link mapUser}.
760
+ */
761
+ async fetchUserInfo(accessToken, refreshToken) {
762
+ if (!this.isConfigured()) {
763
+ throw new Error("OAuth is not configured");
764
+ }
765
+ const userinfo = await this.gateway.getUserInfo(accessToken);
766
+ const mapper = this.mapUser ?? ((info) => info);
767
+ return await mapper(userinfo, accessToken, refreshToken);
768
+ }
769
+ };
770
+ export {
771
+ DEFAULT_OAUTH_AUTHORIZE_PATH,
772
+ DEFAULT_PKCE_STORAGE_KEY,
773
+ OAuthClient,
774
+ OAuthGateway,
775
+ OAuthTokenErrorSchema,
776
+ OAuthTokenResponseSchema,
777
+ OAuthUserInfoSchema,
778
+ OAuthWrapperEndpoints,
779
+ PKCESessionStore,
780
+ buildAuthorizeSearchParams,
781
+ buildOAuthAuthorizeUrl,
782
+ buildOAuthLocaleHeaders,
783
+ buildOAuthRedirectUri,
784
+ defaultOAuthClientConfig,
785
+ formatOAuthAuthorizeUrl,
786
+ mergeOAuthAuthorizationConfig,
787
+ parseOAuthTokenError,
788
+ parseOAuthTokenResponse,
789
+ parseOAuthUserInfoResponse,
790
+ resolveOAuthClientConfig
791
+ };