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