@qlover/oauth-wrapper 0.2.1 → 0.3.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/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/server/index.ts
21
21
  var server_exports = {};
22
22
  __export(server_exports, {
23
+ OAuthAbstractProvider: () => OAuthAbstractProvider,
23
24
  OAuthAuthorizationCodeRowSchema: () => OAuthAuthorizationCodeRowSchema,
24
25
  OAuthAuthorizeQuerySchema: () => OAuthAuthorizeQuerySchema,
25
26
  OAuthClientCreateResponseSchema: () => OAuthClientCreateResponseSchema,
@@ -57,125 +58,14 @@ __export(server_exports, {
57
58
  normalizeQuery: () => normalizeQuery,
58
59
  parseScopeList: () => parseScopeList,
59
60
  randomOAuthState: () => randomOAuthState,
61
+ signWithEmailOtpSchema: () => signWithEmailOtpSchema,
62
+ signWithPhoneOtpSchema: () => signWithPhoneOtpSchema,
60
63
  validatePkceParams: () => validatePkceParams,
61
64
  verifyClientSecret: () => verifyClientSecret,
62
65
  verifyPkceS256: () => verifyPkceS256
63
66
  });
64
67
  module.exports = __toCommonJS(server_exports);
65
68
 
66
- // src/server/services/OAuthClientsService.ts
67
- var OAuthClientsService = class {
68
- constructor(clientsRepo) {
69
- this.clientsRepo = clientsRepo;
70
- }
71
- /**
72
- * List all OAuth clients owned by a user
73
- * @override
74
- */
75
- async listForOwner(ownerUserId) {
76
- return this.clientsRepo.listClientByOwner(ownerUserId);
77
- }
78
- /**
79
- * Get detailed information about a specific OAuth client
80
- * @override
81
- */
82
- async getByClientId(ownerUserId, clientId) {
83
- const client = await this.clientsRepo.findClientById(clientId);
84
- if (!client) {
85
- throw new Error("Client not found");
86
- }
87
- if (client.owner_user_id !== ownerUserId) {
88
- throw new Error("Access denied");
89
- }
90
- return this.mapToDetail(client);
91
- }
92
- /**
93
- * Create a new OAuth client
94
- * @override
95
- */
96
- async create(ownerUserId, input) {
97
- const result = await this.clientsRepo.createClient(ownerUserId, input);
98
- return {
99
- client_id: result.client.client_id,
100
- client_secret: result.clientSecret,
101
- confidential: result.client.confidential,
102
- client_name: result.client.client_name,
103
- client_uri: result.client.client_uri,
104
- redirect_uris: result.client.redirect_uris,
105
- created_at: result.client.created_at
106
- };
107
- }
108
- /**
109
- * Update an existing OAuth client
110
- * @override
111
- */
112
- async update(ownerUserId, clientId, input) {
113
- const existing = await this.clientsRepo.findClientById(clientId);
114
- if (!existing) {
115
- throw new Error("Client not found");
116
- }
117
- if (existing.owner_user_id !== ownerUserId) {
118
- throw new Error("Access denied");
119
- }
120
- return this.clientsRepo.updateClient(ownerUserId, clientId, input);
121
- }
122
- /**
123
- * Rotate the client secret
124
- * @override
125
- */
126
- async rotateSecret(ownerUserId, clientId) {
127
- const existing = await this.clientsRepo.findClientById(clientId);
128
- if (!existing) {
129
- throw new Error("Client not found");
130
- }
131
- if (existing.owner_user_id !== ownerUserId) {
132
- throw new Error("Access denied");
133
- }
134
- if (!existing.confidential) {
135
- throw new Error("Public clients do not have a client_secret");
136
- }
137
- const result = await this.clientsRepo.rotateClientSecret(
138
- ownerUserId,
139
- clientId
140
- );
141
- return {
142
- client_id: clientId,
143
- client_secret: result.clientSecret
144
- };
145
- }
146
- /**
147
- * Delete an OAuth client
148
- * @override
149
- */
150
- async delete(ownerUserId, clientId) {
151
- const existing = await this.clientsRepo.findClientById(clientId);
152
- if (!existing) {
153
- throw new Error("Client not found");
154
- }
155
- if (existing.owner_user_id !== ownerUserId) {
156
- throw new Error("Access denied");
157
- }
158
- await this.clientsRepo.deleteClient(ownerUserId, clientId);
159
- }
160
- mapToDetail(row) {
161
- return {
162
- client_id: row.client_id,
163
- client_name: row.client_name,
164
- client_uri: row.client_uri,
165
- logo_uri: row.logo_uri,
166
- redirect_uris: row.redirect_uris,
167
- grant_types: row.grant_types,
168
- scopes: row.scopes,
169
- confidential: row.confidential,
170
- created_at: row.created_at,
171
- updated_at: row.updated_at
172
- };
173
- }
174
- };
175
-
176
- // src/server/services/OAuthTokenService.ts
177
- var import_crypto2 = require("crypto");
178
-
179
69
  // src/core/schema/OAuthAuthorizeSchema.ts
180
70
  var import_zod = require("zod");
181
71
  function isOAuthRedirectUri(value) {
@@ -284,6 +174,18 @@ var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
284
174
  used: import_zod.z.boolean(),
285
175
  created_at: import_zod.z.string()
286
176
  });
177
+ var signWithPhoneOtpSchema = import_zod.z.object({
178
+ /** The user's phone number. */
179
+ phone: import_zod.z.string(),
180
+ /** The otp sent to the user's phone number. */
181
+ token: import_zod.z.string().optional()
182
+ });
183
+ var signWithEmailOtpSchema = import_zod.z.object({
184
+ /** The user's email address. */
185
+ email: import_zod.z.email(),
186
+ /** The otp sent to the user's email address. */
187
+ token: import_zod.z.string().optional()
188
+ });
287
189
 
288
190
  // src/core/schema/OAuthClientSchema.ts
289
191
  var import_zod2 = require("zod");
@@ -314,12 +216,12 @@ var OAuthTokenResponseSchema = import_zod2.z.object({
314
216
  var import_zod3 = require("zod");
315
217
  var OAuthUserInfoResponseSchema = import_zod3.z.object({
316
218
  sub: import_zod3.z.string(),
317
- email: import_zod3.z.string().email(),
219
+ email: import_zod3.z.email(),
318
220
  name: import_zod3.z.string(),
319
221
  roles: import_zod3.z.array(import_zod3.z.string()).optional()
320
222
  });
321
223
  var OAuthUserInfoErrorResponseSchema = import_zod3.z.object({
322
- error: import_zod3.z.literal("invalid_token"),
224
+ error: import_zod3.z.string(),
323
225
  error_id: import_zod3.z.string().optional()
324
226
  });
325
227
 
@@ -400,16 +302,6 @@ function randomOAuthState() {
400
302
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
401
303
  }
402
304
 
403
- // src/server/utils/OAuthWrapperError.ts
404
- var import_fe_corekit = require("@qlover/fe-corekit");
405
- var OAuthWrapperError = class extends import_fe_corekit.ExecutorError {
406
- constructor(id, status, cause) {
407
- super(id, cause);
408
- this.status = status;
409
- }
410
- name = "OAuthWrapperError";
411
- };
412
-
413
305
  // src/server/utils/pkce.ts
414
306
  var import_crypto = require("crypto");
415
307
  var PKCE_VERIFIER_MIN = 43;
@@ -443,90 +335,467 @@ function verifyPkceS256(verifier, challenge) {
443
335
  }
444
336
  }
445
337
 
446
- // src/server/services/OAuthTokenService.ts
447
- function hashOpaqueToken(token) {
448
- return (0, import_crypto2.createHash)("sha256").update(token).digest("hex");
449
- }
450
- var OAuthTokenService = class _OAuthTokenService {
451
- constructor(tokenEncryption, userAdapter, oauthRepo) {
452
- this.tokenEncryption = tokenEncryption;
453
- this.userAdapter = userAdapter;
454
- this.oauthRepo = oauthRepo;
455
- }
456
- static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
457
- /**
458
- * @override
459
- */
460
- async exchangeToken(rawFields) {
461
- let request;
462
- try {
463
- request = OAuthTokenRequestSchema.parse(rawFields);
464
- } catch {
465
- throw new OAuthWrapperError(
466
- OAuthRfcCodes.INVALID_REQUEST,
467
- 400,
468
- "Malformed token request"
469
- );
338
+ // src/server/utils/authorizeUtil.ts
339
+ function validatePkceParams(parsed, confidential) {
340
+ const hasChallenge = Boolean(parsed.code_challenge?.trim());
341
+ const hasMethod = Boolean(parsed.code_challenge_method);
342
+ if (!confidential) {
343
+ if (!hasChallenge || parsed.code_challenge_method !== "S256") {
344
+ return {
345
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
346
+ message: "Public clients must send code_challenge and code_challenge_method=S256."
347
+ };
470
348
  }
471
- let client;
472
- try {
473
- client = await this.oauthRepo.verifyClientCredentials(
474
- request.client_id,
475
- request.client_secret
476
- );
477
- } catch {
478
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
349
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
350
+ return {
351
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
352
+ message: "Invalid code_challenge."
353
+ };
479
354
  }
480
- if (!client.grant_types.includes(request.grant_type)) {
481
- throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
355
+ return null;
356
+ }
357
+ if (hasChallenge !== hasMethod) {
358
+ return {
359
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
360
+ message: "code_challenge and code_challenge_method must be sent together."
361
+ };
362
+ }
363
+ if (hasChallenge) {
364
+ if (parsed.code_challenge_method !== "S256") {
365
+ return {
366
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
367
+ message: "Only code_challenge_method=S256 is supported."
368
+ };
482
369
  }
483
- if (request.grant_type === "authorization_code") {
484
- return await this.exchangeAuthorizationCode(request, client.client_id);
370
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
371
+ return {
372
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
373
+ message: "Invalid code_challenge."
374
+ };
485
375
  }
486
- return await this.exchangeRefreshToken(request, client.client_id);
487
376
  }
488
- async exchangeAuthorizationCode(request, verifiedClientId) {
489
- const authCode = await this.oauthRepo.consumeCode(request.code);
490
- if (!authCode) {
491
- throw new OAuthWrapperError(
492
- OAuthRfcCodes.INVALID_GRANT,
493
- 400,
494
- "Authorization code is invalid, expired, or already used"
495
- );
496
- }
497
- if (authCode.client_id !== verifiedClientId) {
498
- throw new OAuthWrapperError(
499
- OAuthRfcCodes.INVALID_GRANT,
500
- 400,
501
- "client_id mismatch"
502
- );
377
+ return null;
378
+ }
379
+ function normalizeQuery(raw) {
380
+ const result = {};
381
+ for (const [key, value] of Object.entries(raw)) {
382
+ if (Array.isArray(value)) {
383
+ result[key] = value[0];
384
+ } else {
385
+ result[key] = value;
503
386
  }
504
- if (authCode.redirect_uri !== request.redirect_uri) {
505
- throw new OAuthWrapperError(
506
- OAuthRfcCodes.INVALID_GRANT,
507
- 400,
508
- "redirect_uri mismatch"
509
- );
387
+ }
388
+ if (!result.response_type) {
389
+ result.response_type = "code";
390
+ }
391
+ return result;
392
+ }
393
+ function isRedirectUriAllowed(redirectUri, client) {
394
+ return client.redirect_uris.includes(redirectUri);
395
+ }
396
+
397
+ // src/server/utils/oauthRedirectUtils.ts
398
+ function buildOAuthRedirectUrl(redirectUri, params) {
399
+ const url = new URL(redirectUri);
400
+ for (const [key, value] of Object.entries(params)) {
401
+ if (value != null && value !== "") {
402
+ url.searchParams.set(key, value);
510
403
  }
511
- this.assertPkceForAuthorizationCode(request, authCode);
512
- const providerTokens = await this.fetchProviderAccessToken(
513
- authCode.user_id
514
- );
515
- const middlewareRefresh = await this.issueMiddlewareRefreshToken(
516
- authCode.client_id,
517
- authCode.user_id
518
- );
519
- return {
520
- access_token: providerTokens.access_token,
521
- token_type: "Bearer",
522
- expires_in: providerTokens.expires_in,
523
- refresh_token: middlewareRefresh,
524
- scope: authCode.scope ?? void 0
525
- };
526
404
  }
527
- assertPkceForAuthorizationCode(request, authCode) {
528
- const storedChallenge = authCode.code_challenge?.trim();
529
- if (storedChallenge) {
405
+ return url.toString();
406
+ }
407
+ function parseScopeList(scope) {
408
+ if (!scope?.trim()) {
409
+ return ["openid", "profile", "email"];
410
+ }
411
+ return scope.trim().split(/\s+/).filter(Boolean);
412
+ }
413
+
414
+ // src/server/services/OAuthAbstractProvider.ts
415
+ var import_fe_corekit = require("@qlover/fe-corekit");
416
+ var import_crypto2 = require("crypto");
417
+ var OAuthAbstractProvider = class {
418
+ authCodeTTLMs = 5 * 60 * 1e3;
419
+ isQuery(query) {
420
+ return OAuthAuthorizeQuerySchema.safeParse(query).success;
421
+ }
422
+ isValidateConsent(value) {
423
+ return OAuthConsentBodySchema.safeParse(value).success;
424
+ }
425
+ generageSessionPayload(userInfo) {
426
+ return {
427
+ userId: String(userInfo.id),
428
+ email: userInfo.email,
429
+ name: userInfo.name ?? userInfo.email,
430
+ providerSessionToken: userInfo.sessionToken
431
+ };
432
+ }
433
+ /**
434
+ * @override
435
+ */
436
+ async logout(userId) {
437
+ const oauthRepo = this.getOAuthRepo();
438
+ await oauthRepo.revokeRefreshTokensByUserId(userId);
439
+ await oauthRepo.upsertUserCredentials(userId, {
440
+ provider_refresh_token: null,
441
+ provider_session_token: null
442
+ });
443
+ await this.clearSession();
444
+ }
445
+ /**
446
+ * @override
447
+ */
448
+ async resolveAuthorizePage(rawQuery) {
449
+ const query = normalizeQuery(rawQuery);
450
+ if (!this.isQuery(query)) {
451
+ return {
452
+ ok: false,
453
+ error: {
454
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
455
+ message: "Missing or invalid authorization request parameters."
456
+ }
457
+ };
458
+ }
459
+ if (query.response_type !== "code") {
460
+ return {
461
+ ok: false,
462
+ error: {
463
+ errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
464
+ message: "Only response_type=code is supported."
465
+ }
466
+ };
467
+ }
468
+ const oauthRepo = this.getOAuthRepo();
469
+ const client = await oauthRepo.findClientById(query.client_id);
470
+ if (!client) {
471
+ return {
472
+ ok: false,
473
+ error: {
474
+ errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
475
+ message: "Unknown client_id."
476
+ }
477
+ };
478
+ }
479
+ if (!isRedirectUriAllowed(query.redirect_uri, client)) {
480
+ return {
481
+ ok: false,
482
+ error: {
483
+ errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
484
+ message: "redirect_uri is not registered for this client."
485
+ }
486
+ };
487
+ }
488
+ const requestedScopes = parseScopeList(query.scope);
489
+ const invalidScope = requestedScopes.find(
490
+ (scope) => !client.scopes.includes(scope)
491
+ );
492
+ if (invalidScope) {
493
+ return {
494
+ ok: false,
495
+ error: {
496
+ errorKey: OAuthRfcCodes.INVALID_SCOPE,
497
+ message: `Scope "${invalidScope}" is not allowed for this client.`
498
+ }
499
+ };
500
+ }
501
+ const pkceError = validatePkceParams(query, client.confidential);
502
+ if (pkceError) {
503
+ return { ok: false, error: pkceError };
504
+ }
505
+ return {
506
+ ok: true,
507
+ data: {
508
+ clientId: client.client_id,
509
+ clientName: client.client_name,
510
+ clientUri: client.client_uri ?? null,
511
+ logoUri: client.logo_uri ?? null,
512
+ redirectUri: query.redirect_uri,
513
+ scopes: requestedScopes,
514
+ state: query.state,
515
+ responseType: "code",
516
+ codeChallenge: query.code_challenge,
517
+ codeChallengeMethod: query.code_challenge_method,
518
+ confidential: client.confidential
519
+ }
520
+ };
521
+ }
522
+ /**
523
+ * @override
524
+ */
525
+ async processConsent(requestBody) {
526
+ if (!this.isValidateConsent(requestBody)) {
527
+ throw new import_fe_corekit.ExecutorError(
528
+ OAuthRfcCodes.INVALID_REQUEST,
529
+ "Invalid consent request body"
530
+ );
531
+ }
532
+ const session = await this.getSession();
533
+ if (!session) {
534
+ throw new import_fe_corekit.ExecutorError(
535
+ OAuthRfcCodes.ACCESS_DENIED,
536
+ "User session expired. Please sign in again."
537
+ );
538
+ }
539
+ const pageResult = await this.resolveAuthorizePage({
540
+ response_type: "code",
541
+ client_id: requestBody.client_id,
542
+ redirect_uri: requestBody.redirect_uri,
543
+ scope: requestBody.scope,
544
+ state: requestBody.state,
545
+ code_challenge: requestBody.code_challenge,
546
+ code_challenge_method: requestBody.code_challenge_method
547
+ });
548
+ if (!pageResult.ok) {
549
+ throw new import_fe_corekit.ExecutorError(
550
+ pageResult.error.errorKey,
551
+ pageResult.error.message
552
+ );
553
+ }
554
+ const { data } = pageResult;
555
+ if (requestBody.action === "deny") {
556
+ return {
557
+ redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
558
+ error: OAuthRfcCodes.ACCESS_DENIED,
559
+ error_description: "The resource owner denied the request",
560
+ state: data.state
561
+ })
562
+ };
563
+ }
564
+ const code = (0, import_crypto2.randomBytes)(32).toString("base64url");
565
+ const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
566
+ const oauthRepo = this.getOAuthRepo();
567
+ await oauthRepo.create({
568
+ code,
569
+ client_id: data.clientId,
570
+ user_id: session.userId,
571
+ redirect_uri: data.redirectUri,
572
+ scope: data.scopes.join(" ") || null,
573
+ code_challenge: data.codeChallenge ?? null,
574
+ code_challenge_method: data.codeChallengeMethod ?? null,
575
+ expires_at: expiresAt
576
+ });
577
+ void requestBody.trust;
578
+ return {
579
+ redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
580
+ code,
581
+ state: data.state
582
+ })
583
+ };
584
+ }
585
+ };
586
+
587
+ // src/server/services/OAuthClientsService.ts
588
+ var OAuthClientsService = class {
589
+ constructor(clientsRepo) {
590
+ this.clientsRepo = clientsRepo;
591
+ }
592
+ clientsRepo;
593
+ /**
594
+ * List all OAuth clients owned by a user
595
+ * @override
596
+ */
597
+ async listForOwner(ownerUserId) {
598
+ return this.clientsRepo.listClientByOwner(ownerUserId);
599
+ }
600
+ /**
601
+ * Get detailed information about a specific OAuth client
602
+ * @override
603
+ */
604
+ async getByClientId(ownerUserId, clientId) {
605
+ const client = await this.clientsRepo.findClientById(clientId);
606
+ if (!client) {
607
+ throw new Error("Client not found");
608
+ }
609
+ if (client.owner_user_id !== ownerUserId) {
610
+ throw new Error("Access denied");
611
+ }
612
+ return this.mapToDetail(client);
613
+ }
614
+ /**
615
+ * Create a new OAuth client
616
+ * @override
617
+ */
618
+ async create(ownerUserId, input) {
619
+ const result = await this.clientsRepo.createClient(ownerUserId, input);
620
+ return {
621
+ client_id: result.client.client_id,
622
+ client_secret: result.clientSecret,
623
+ confidential: result.client.confidential,
624
+ client_name: result.client.client_name,
625
+ client_uri: result.client.client_uri,
626
+ redirect_uris: result.client.redirect_uris,
627
+ created_at: result.client.created_at
628
+ };
629
+ }
630
+ /**
631
+ * Update an existing OAuth client
632
+ * @override
633
+ */
634
+ async update(ownerUserId, clientId, input) {
635
+ const existing = await this.clientsRepo.findClientById(clientId);
636
+ if (!existing) {
637
+ throw new Error("Client not found");
638
+ }
639
+ if (existing.owner_user_id !== ownerUserId) {
640
+ throw new Error("Access denied");
641
+ }
642
+ return this.clientsRepo.updateClient(ownerUserId, clientId, input);
643
+ }
644
+ /**
645
+ * Rotate the client secret
646
+ * @override
647
+ */
648
+ async rotateSecret(ownerUserId, clientId) {
649
+ const existing = await this.clientsRepo.findClientById(clientId);
650
+ if (!existing) {
651
+ throw new Error("Client not found");
652
+ }
653
+ if (existing.owner_user_id !== ownerUserId) {
654
+ throw new Error("Access denied");
655
+ }
656
+ if (!existing.confidential) {
657
+ throw new Error("Public clients do not have a client_secret");
658
+ }
659
+ const result = await this.clientsRepo.rotateClientSecret(
660
+ ownerUserId,
661
+ clientId
662
+ );
663
+ return {
664
+ client_id: clientId,
665
+ client_secret: result.clientSecret
666
+ };
667
+ }
668
+ /**
669
+ * Delete an OAuth client
670
+ * @override
671
+ */
672
+ async delete(ownerUserId, clientId) {
673
+ const existing = await this.clientsRepo.findClientById(clientId);
674
+ if (!existing) {
675
+ throw new Error("Client not found");
676
+ }
677
+ if (existing.owner_user_id !== ownerUserId) {
678
+ throw new Error("Access denied");
679
+ }
680
+ await this.clientsRepo.deleteClient(ownerUserId, clientId);
681
+ }
682
+ mapToDetail(row) {
683
+ return {
684
+ client_id: row.client_id,
685
+ client_name: row.client_name,
686
+ client_uri: row.client_uri,
687
+ logo_uri: row.logo_uri,
688
+ redirect_uris: row.redirect_uris,
689
+ grant_types: row.grant_types,
690
+ scopes: row.scopes,
691
+ confidential: row.confidential,
692
+ created_at: row.created_at,
693
+ updated_at: row.updated_at
694
+ };
695
+ }
696
+ };
697
+
698
+ // src/server/services/OAuthTokenService.ts
699
+ var import_crypto3 = require("crypto");
700
+
701
+ // src/server/utils/OAuthWrapperError.ts
702
+ var import_fe_corekit2 = require("@qlover/fe-corekit");
703
+ var OAuthWrapperError = class extends import_fe_corekit2.ExecutorError {
704
+ constructor(id, status, cause) {
705
+ super(id, cause);
706
+ this.status = status;
707
+ }
708
+ status;
709
+ name = "OAuthWrapperError";
710
+ };
711
+
712
+ // src/server/services/OAuthTokenService.ts
713
+ function hashOpaqueToken(token) {
714
+ return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
715
+ }
716
+ var OAuthTokenService = class _OAuthTokenService {
717
+ constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
718
+ this.tokenEncryption = tokenEncryption;
719
+ this.exchangeProviderAccessToken = exchangeProviderAccessToken;
720
+ this.oauthRepo = oauthRepo;
721
+ }
722
+ tokenEncryption;
723
+ exchangeProviderAccessToken;
724
+ oauthRepo;
725
+ static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
726
+ /**
727
+ * @override
728
+ */
729
+ async exchangeToken(rawFields) {
730
+ let request;
731
+ try {
732
+ request = OAuthTokenRequestSchema.parse(rawFields);
733
+ } catch {
734
+ throw new OAuthWrapperError(
735
+ OAuthRfcCodes.INVALID_REQUEST,
736
+ 400,
737
+ "Malformed token request"
738
+ );
739
+ }
740
+ let client;
741
+ try {
742
+ client = await this.oauthRepo.verifyClientCredentials(
743
+ request.client_id,
744
+ request.client_secret
745
+ );
746
+ } catch {
747
+ throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
748
+ }
749
+ if (!client.grant_types.includes(request.grant_type)) {
750
+ throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
751
+ }
752
+ if (request.grant_type === "authorization_code") {
753
+ return await this.exchangeAuthorizationCode(request, client.client_id);
754
+ }
755
+ return await this.exchangeRefreshToken(request, client.client_id);
756
+ }
757
+ async exchangeAuthorizationCode(request, verifiedClientId) {
758
+ const authCode = await this.oauthRepo.consumeCode(request.code);
759
+ if (!authCode) {
760
+ throw new OAuthWrapperError(
761
+ OAuthRfcCodes.INVALID_GRANT,
762
+ 400,
763
+ "Authorization code is invalid, expired, or already used"
764
+ );
765
+ }
766
+ if (authCode.client_id !== verifiedClientId) {
767
+ throw new OAuthWrapperError(
768
+ OAuthRfcCodes.INVALID_GRANT,
769
+ 400,
770
+ "client_id mismatch"
771
+ );
772
+ }
773
+ if (authCode.redirect_uri !== request.redirect_uri) {
774
+ throw new OAuthWrapperError(
775
+ OAuthRfcCodes.INVALID_GRANT,
776
+ 400,
777
+ "redirect_uri mismatch"
778
+ );
779
+ }
780
+ this.assertPkceForAuthorizationCode(request, authCode);
781
+ const providerTokens = await this.fetchProviderAccessToken(
782
+ authCode.user_id
783
+ );
784
+ const middlewareRefresh = await this.issueMiddlewareRefreshToken(
785
+ authCode.client_id,
786
+ authCode.user_id
787
+ );
788
+ return {
789
+ access_token: providerTokens.access_token,
790
+ token_type: "Bearer",
791
+ expires_in: providerTokens.expires_in,
792
+ refresh_token: middlewareRefresh,
793
+ scope: authCode.scope ?? void 0
794
+ };
795
+ }
796
+ assertPkceForAuthorizationCode(request, authCode) {
797
+ const storedChallenge = authCode.code_challenge?.trim();
798
+ if (storedChallenge) {
530
799
  const verifier = request.code_verifier?.trim();
531
800
  if (!verifier) {
532
801
  throw new OAuthWrapperError(
@@ -593,7 +862,7 @@ var OAuthTokenService = class _OAuthTokenService {
593
862
  );
594
863
  }
595
864
  try {
596
- const access = await this.userAdapter.exchangeAccessToken({
865
+ const access = await this.exchangeProviderAccessToken({
597
866
  token: sessionToken
598
867
  });
599
868
  if (access.refresh_token) {
@@ -616,7 +885,7 @@ var OAuthTokenService = class _OAuthTokenService {
616
885
  }
617
886
  }
618
887
  async issueMiddlewareRefreshToken(clientId, userId) {
619
- const plain = (0, import_crypto2.randomBytes)(32).toString("base64url");
888
+ const plain = (0, import_crypto3.randomBytes)(32).toString("base64url");
620
889
  const expiresAt = new Date(
621
890
  Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
622
891
  ).toISOString();
@@ -664,111 +933,41 @@ var OAuthTokenService = class _OAuthTokenService {
664
933
  };
665
934
 
666
935
  // src/server/services/OAuthWrapperService.ts
667
- var import_crypto3 = require("crypto");
668
- var import_fe_corekit2 = require("@qlover/fe-corekit");
669
-
670
- // src/server/utils/authorizeUtil.ts
671
- function validatePkceParams(parsed, confidential) {
672
- const hasChallenge = Boolean(parsed.code_challenge?.trim());
673
- const hasMethod = Boolean(parsed.code_challenge_method);
674
- if (!confidential) {
675
- if (!hasChallenge || parsed.code_challenge_method !== "S256") {
676
- return {
677
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
678
- message: "Public clients must send code_challenge and code_challenge_method=S256."
679
- };
680
- }
681
- if (!isValidCodeChallenge(parsed.code_challenge)) {
682
- return {
683
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
684
- message: "Invalid code_challenge."
685
- };
686
- }
687
- return null;
688
- }
689
- if (hasChallenge !== hasMethod) {
690
- return {
691
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
692
- message: "code_challenge and code_challenge_method must be sent together."
693
- };
694
- }
695
- if (hasChallenge) {
696
- if (parsed.code_challenge_method !== "S256") {
697
- return {
698
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
699
- message: "Only code_challenge_method=S256 is supported."
700
- };
701
- }
702
- if (!isValidCodeChallenge(parsed.code_challenge)) {
703
- return {
704
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
705
- message: "Invalid code_challenge."
706
- };
707
- }
708
- }
709
- return null;
710
- }
711
- function normalizeQuery(raw) {
712
- const result = {};
713
- for (const [key, value] of Object.entries(raw)) {
714
- if (Array.isArray(value)) {
715
- result[key] = value[0];
716
- } else {
717
- result[key] = value;
718
- }
719
- }
720
- if (!result.response_type) {
721
- result.response_type = "code";
722
- }
723
- return result;
724
- }
725
- function isRedirectUriAllowed(redirectUri, client) {
726
- return client.redirect_uris.includes(redirectUri);
727
- }
728
-
729
- // src/server/utils/oauthRedirectUtils.ts
730
- function buildOAuthRedirectUrl(redirectUri, params) {
731
- const url = new URL(redirectUri);
732
- for (const [key, value] of Object.entries(params)) {
733
- if (value != null && value !== "") {
734
- url.searchParams.set(key, value);
735
- }
736
- }
737
- return url.toString();
738
- }
739
- function parseScopeList(scope) {
740
- if (!scope?.trim()) {
741
- return ["openid", "profile", "email"];
742
- }
743
- return scope.trim().split(/\s+/).filter(Boolean);
744
- }
745
-
746
- // src/server/services/OAuthWrapperService.ts
747
- var AUTH_CODE_TTL_MS = 5 * 60 * 1e3;
748
- var OAuthWrapperService = class {
749
- constructor(oauthSession, userAdapter, tokenService, oauthRepo) {
936
+ var OAuthWrapperService = class extends OAuthAbstractProvider {
937
+ constructor(oauthSession, tokenEncryption, oauthRepo) {
938
+ super();
750
939
  this.oauthSession = oauthSession;
751
- this.userAdapter = userAdapter;
752
- this.tokenService = tokenService;
753
940
  this.oauthRepo = oauthRepo;
941
+ this.tokenService = new OAuthTokenService(
942
+ tokenEncryption,
943
+ (params) => this.providerExchangeAccessToken(params),
944
+ oauthRepo
945
+ );
754
946
  }
947
+ oauthSession;
948
+ oauthRepo;
949
+ tokenService;
755
950
  /**
756
951
  * @override
757
952
  */
758
- getOAuthSession() {
759
- return this.oauthSession;
760
- }
761
- /**
762
- * @override
763
- */
764
- getOAuthAdapter() {
765
- return this.userAdapter;
766
- }
767
- /**
768
- * @override
769
- */
770
- getOAuthTokenService() {
771
- return this.tokenService;
953
+ async login(params) {
954
+ const credentials = await this.providerLogin(params);
955
+ const sessionToken = credentials.token;
956
+ if (!sessionToken) {
957
+ throw new Error("User provider login did not return a session token");
958
+ }
959
+ const userInfo = await this.providerGetUserInfo(sessionToken);
960
+ const sessionPayload = this.generageSessionPayload({
961
+ email: params.email,
962
+ ...userInfo,
963
+ sessionToken
964
+ });
965
+ this.oauthSession.setSession(sessionPayload);
966
+ const oauthrepo = this.getOAuthRepo();
967
+ await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
968
+ provider_session_token: sessionPayload.providerSessionToken
969
+ });
970
+ return sessionPayload;
772
971
  }
773
972
  /**
774
973
  * @override
@@ -776,155 +975,23 @@ var OAuthWrapperService = class {
776
975
  getOAuthRepo() {
777
976
  return this.oauthRepo;
778
977
  }
779
- isQuery(query) {
780
- return OAuthAuthorizeQuerySchema.safeParse(query).success;
781
- }
782
- isValidateConsent(value) {
783
- return OAuthConsentBodySchema.safeParse(value).success;
784
- }
785
978
  /**
786
979
  * @override
787
980
  */
788
- async resolveAuthorizePage(rawQuery) {
789
- const query = normalizeQuery(rawQuery);
790
- if (!this.isQuery(query)) {
791
- return {
792
- ok: false,
793
- error: {
794
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
795
- message: "Missing or invalid authorization request parameters."
796
- }
797
- };
798
- }
799
- if (query.response_type !== "code") {
800
- return {
801
- ok: false,
802
- error: {
803
- errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
804
- message: "Only response_type=code is supported."
805
- }
806
- };
807
- }
808
- const client = await this.oauthRepo.findClientById(query.client_id);
809
- if (!client) {
810
- return {
811
- ok: false,
812
- error: {
813
- errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
814
- message: "Unknown client_id."
815
- }
816
- };
817
- }
818
- if (!isRedirectUriAllowed(query.redirect_uri, client)) {
819
- return {
820
- ok: false,
821
- error: {
822
- errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
823
- message: "redirect_uri is not registered for this client."
824
- }
825
- };
826
- }
827
- const requestedScopes = parseScopeList(query.scope);
828
- const invalidScope = requestedScopes.find(
829
- (scope) => !client.scopes.includes(scope)
830
- );
831
- if (invalidScope) {
832
- return {
833
- ok: false,
834
- error: {
835
- errorKey: OAuthRfcCodes.INVALID_SCOPE,
836
- message: `Scope "${invalidScope}" is not allowed for this client.`
837
- }
838
- };
839
- }
840
- const pkceError = validatePkceParams(query, client.confidential);
841
- if (pkceError) {
842
- return { ok: false, error: pkceError };
843
- }
844
- return {
845
- ok: true,
846
- data: {
847
- clientId: client.client_id,
848
- clientName: client.client_name,
849
- clientUri: client.client_uri ?? null,
850
- logoUri: client.logo_uri ?? null,
851
- redirectUri: query.redirect_uri,
852
- scopes: requestedScopes,
853
- state: query.state,
854
- responseType: "code",
855
- codeChallenge: query.code_challenge,
856
- codeChallengeMethod: query.code_challenge_method,
857
- confidential: client.confidential
858
- }
859
- };
981
+ async exchangeToken(rawFields) {
982
+ return await this.tokenService.exchangeToken(rawFields);
860
983
  }
861
984
  /**
862
985
  * @override
863
986
  */
864
- async processConsent(requestBody) {
865
- if (!this.isValidateConsent(requestBody)) {
866
- throw new import_fe_corekit2.ExecutorError(
867
- OAuthRfcCodes.INVALID_REQUEST,
868
- "Invalid consent request body"
869
- );
870
- }
871
- const session = await this.oauthSession.getSession();
872
- if (!session) {
873
- throw new import_fe_corekit2.ExecutorError(
874
- OAuthRfcCodes.ACCESS_DENIED,
875
- "User session expired. Please sign in again."
876
- );
877
- }
878
- const pageResult = await this.resolveAuthorizePage({
879
- response_type: "code",
880
- client_id: requestBody.client_id,
881
- redirect_uri: requestBody.redirect_uri,
882
- scope: requestBody.scope,
883
- state: requestBody.state,
884
- code_challenge: requestBody.code_challenge,
885
- code_challenge_method: requestBody.code_challenge_method
886
- });
887
- if (!pageResult.ok) {
888
- throw new import_fe_corekit2.ExecutorError(
889
- pageResult.error.errorKey,
890
- pageResult.error.message
891
- );
892
- }
893
- const { data } = pageResult;
894
- if (requestBody.action === "deny") {
895
- return {
896
- redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
897
- error: OAuthRfcCodes.ACCESS_DENIED,
898
- error_description: "The resource owner denied the request",
899
- state: data.state
900
- })
901
- };
902
- }
903
- const code = (0, import_crypto3.randomBytes)(32).toString("base64url");
904
- const expiresAt = new Date(Date.now() + AUTH_CODE_TTL_MS).toISOString();
905
- await this.oauthRepo.create({
906
- code,
907
- client_id: data.clientId,
908
- user_id: session.userId,
909
- redirect_uri: data.redirectUri,
910
- scope: data.scopes.join(" ") || null,
911
- code_challenge: data.codeChallenge ?? null,
912
- code_challenge_method: data.codeChallengeMethod ?? null,
913
- expires_at: expiresAt
914
- });
915
- void requestBody.trust;
916
- return {
917
- redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
918
- code,
919
- state: data.state
920
- })
921
- };
987
+ getSession() {
988
+ return this.oauthSession.getSession();
922
989
  }
923
990
  /**
924
991
  * @override
925
992
  */
926
- async exchangeToken(rawFields) {
927
- return await this.tokenService.exchangeToken(rawFields);
993
+ clearSession() {
994
+ return this.oauthSession.clearSession();
928
995
  }
929
996
  /**
930
997
  * @override
@@ -932,9 +999,6 @@ var OAuthWrapperService = class {
932
999
  async revokeToken(rawFields) {
933
1000
  return await this.tokenService.revokeToken(rawFields);
934
1001
  }
935
- /**
936
- * @override
937
- */
938
1002
  async logoutUser(userId) {
939
1003
  await this.oauthRepo.revokeRefreshTokensByUserId(userId);
940
1004
  await this.oauthRepo.upsertUserCredentials(userId, {
@@ -946,9 +1010,9 @@ var OAuthWrapperService = class {
946
1010
  /**
947
1011
  * @override
948
1012
  */
949
- async getUserInfo(accessToken) {
1013
+ async getUserInfoWithAccessToken(accessToken) {
950
1014
  try {
951
- const profile = await this.userAdapter.getUserInfoByAccessToken(accessToken);
1015
+ const profile = await this.providerGetUserInfoByAccessToken(accessToken);
952
1016
  return this.toUserInfoResponse(profile);
953
1017
  } catch {
954
1018
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
@@ -963,8 +1027,7 @@ var OAuthWrapperService = class {
963
1027
  if (!email) {
964
1028
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
965
1029
  }
966
- const nameFromParts = [profile.first_name, profile.last_name].filter(Boolean).join(" ");
967
- const name = profile.name?.trim() || nameFromParts || email;
1030
+ const name = profile.name?.trim() || email;
968
1031
  return {
969
1032
  sub,
970
1033
  email,
@@ -1000,6 +1063,7 @@ async function verifyClientSecret(secret, storedHash) {
1000
1063
  }
1001
1064
  // Annotate the CommonJS export names for ESM import in node:
1002
1065
  0 && (module.exports = {
1066
+ OAuthAbstractProvider,
1003
1067
  OAuthAuthorizationCodeRowSchema,
1004
1068
  OAuthAuthorizeQuerySchema,
1005
1069
  OAuthClientCreateResponseSchema,
@@ -1037,6 +1101,8 @@ async function verifyClientSecret(secret, storedHash) {
1037
1101
  normalizeQuery,
1038
1102
  parseScopeList,
1039
1103
  randomOAuthState,
1104
+ signWithEmailOtpSchema,
1105
+ signWithPhoneOtpSchema,
1040
1106
  validatePkceParams,
1041
1107
  verifyClientSecret,
1042
1108
  verifyPkceS256