@qlover/oauth-wrapper 0.5.0 → 0.6.3

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,7 +20,6 @@ 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,
24
23
  OAuthAuthorizationCodeRowSchema: () => OAuthAuthorizationCodeRowSchema,
25
24
  OAuthAuthorizeQuerySchema: () => OAuthAuthorizeQuerySchema,
26
25
  OAuthClientCreateResponseSchema: () => OAuthClientCreateResponseSchema,
@@ -66,6 +65,120 @@ __export(server_exports, {
66
65
  });
67
66
  module.exports = __toCommonJS(server_exports);
68
67
 
68
+ // src/server/services/OAuthClientsService.ts
69
+ var OAuthClientsService = class {
70
+ constructor(clientsRepo) {
71
+ this.clientsRepo = clientsRepo;
72
+ }
73
+ clientsRepo;
74
+ /**
75
+ * List all OAuth clients owned by a user
76
+ * @override
77
+ */
78
+ async listForOwner(ownerUserId) {
79
+ return this.clientsRepo.listClientByOwner(ownerUserId);
80
+ }
81
+ /**
82
+ * Get detailed information about a specific OAuth client
83
+ * @override
84
+ */
85
+ async getByClientId(ownerUserId, clientId) {
86
+ const client = await this.clientsRepo.findClientById(clientId);
87
+ if (!client) {
88
+ throw new Error("Client not found");
89
+ }
90
+ if (client.owner_user_id !== ownerUserId) {
91
+ throw new Error("Access denied");
92
+ }
93
+ return this.mapToDetail(client);
94
+ }
95
+ /**
96
+ * Create a new OAuth client
97
+ * @override
98
+ */
99
+ async create(ownerUserId, input) {
100
+ const result = await this.clientsRepo.createClient(ownerUserId, input);
101
+ return {
102
+ client_id: result.client.client_id,
103
+ client_secret: result.clientSecret,
104
+ confidential: result.client.confidential,
105
+ client_name: result.client.client_name,
106
+ client_uri: result.client.client_uri,
107
+ redirect_uris: result.client.redirect_uris,
108
+ created_at: result.client.created_at
109
+ };
110
+ }
111
+ /**
112
+ * Update an existing OAuth client
113
+ * @override
114
+ */
115
+ async update(ownerUserId, clientId, input) {
116
+ const existing = await this.clientsRepo.findClientById(clientId);
117
+ if (!existing) {
118
+ throw new Error("Client not found");
119
+ }
120
+ if (existing.owner_user_id !== ownerUserId) {
121
+ throw new Error("Access denied");
122
+ }
123
+ return this.clientsRepo.updateClient(ownerUserId, clientId, input);
124
+ }
125
+ /**
126
+ * Rotate the client secret
127
+ * @override
128
+ */
129
+ async rotateSecret(ownerUserId, clientId) {
130
+ const existing = await this.clientsRepo.findClientById(clientId);
131
+ if (!existing) {
132
+ throw new Error("Client not found");
133
+ }
134
+ if (existing.owner_user_id !== ownerUserId) {
135
+ throw new Error("Access denied");
136
+ }
137
+ if (!existing.confidential) {
138
+ throw new Error("Public clients do not have a client_secret");
139
+ }
140
+ const result = await this.clientsRepo.rotateClientSecret(
141
+ ownerUserId,
142
+ clientId
143
+ );
144
+ return {
145
+ client_id: clientId,
146
+ client_secret: result.clientSecret
147
+ };
148
+ }
149
+ /**
150
+ * Delete an OAuth client
151
+ * @override
152
+ */
153
+ async delete(ownerUserId, clientId) {
154
+ const existing = await this.clientsRepo.findClientById(clientId);
155
+ if (!existing) {
156
+ throw new Error("Client not found");
157
+ }
158
+ if (existing.owner_user_id !== ownerUserId) {
159
+ throw new Error("Access denied");
160
+ }
161
+ await this.clientsRepo.deleteClient(ownerUserId, clientId);
162
+ }
163
+ mapToDetail(row) {
164
+ return {
165
+ client_id: row.client_id,
166
+ client_name: row.client_name,
167
+ client_uri: row.client_uri,
168
+ logo_uri: row.logo_uri,
169
+ redirect_uris: row.redirect_uris,
170
+ grant_types: row.grant_types,
171
+ scopes: row.scopes,
172
+ confidential: row.confidential,
173
+ created_at: row.created_at,
174
+ updated_at: row.updated_at
175
+ };
176
+ }
177
+ };
178
+
179
+ // src/server/services/OAuthTokenService.ts
180
+ var import_crypto2 = require("crypto");
181
+
69
182
  // src/core/schema/OAuthAuthorizeSchema.ts
70
183
  var import_zod = require("zod");
71
184
  function isOAuthRedirectUri(value) {
@@ -206,7 +319,7 @@ var OAuthRefreshTokenSchema = import_zod2.z.object({
206
319
  });
207
320
  var OAuthTokenResponseSchema = import_zod2.z.object({
208
321
  access_token: import_zod2.z.string(),
209
- token_type: import_zod2.z.literal("Bearer"),
322
+ token_type: import_zod2.z.string(),
210
323
  expires_in: import_zod2.z.number(),
211
324
  refresh_token: import_zod2.z.string().optional(),
212
325
  scope: import_zod2.z.string().optional()
@@ -302,6 +415,17 @@ function randomOAuthState() {
302
415
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
303
416
  }
304
417
 
418
+ // src/server/utils/OAuthWrapperError.ts
419
+ var import_fe_corekit = require("@qlover/fe-corekit");
420
+ var OAuthWrapperError = class extends import_fe_corekit.ExecutorError {
421
+ constructor(id, status, cause) {
422
+ super(id, cause);
423
+ this.status = status;
424
+ }
425
+ status;
426
+ name = "OAuthWrapperError";
427
+ };
428
+
305
429
  // src/server/utils/pkce.ts
306
430
  var import_crypto = require("crypto");
307
431
  var PKCE_VERIFIER_MIN = 43;
@@ -325,393 +449,19 @@ function computeS256CodeChallenge(verifier) {
325
449
  }
326
450
  function verifyPkceS256(verifier, challenge) {
327
451
  if (!isValidCodeVerifier(verifier) || !isValidCodeChallenge(challenge)) {
328
- return false;
329
- }
330
- const computed = computeS256CodeChallenge(verifier);
331
- try {
332
- return (0, import_crypto.timingSafeEqual)(Buffer.from(computed), Buffer.from(challenge));
333
- } catch {
334
- return false;
335
- }
336
- }
337
-
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
- };
348
- }
349
- if (!isValidCodeChallenge(parsed.code_challenge)) {
350
- return {
351
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
352
- message: "Invalid code_challenge."
353
- };
354
- }
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
- };
369
- }
370
- if (!isValidCodeChallenge(parsed.code_challenge)) {
371
- return {
372
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
373
- message: "Invalid code_challenge."
374
- };
375
- }
376
- }
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;
386
- }
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);
403
- }
404
- }
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
- };
452
+ return false;
695
453
  }
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;
454
+ const computed = computeS256CodeChallenge(verifier);
455
+ try {
456
+ return (0, import_crypto.timingSafeEqual)(Buffer.from(computed), Buffer.from(challenge));
457
+ } catch {
458
+ return false;
707
459
  }
708
- status;
709
- name = "OAuthWrapperError";
710
- };
460
+ }
711
461
 
712
462
  // src/server/services/OAuthTokenService.ts
713
463
  function hashOpaqueToken(token) {
714
- return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
464
+ return (0, import_crypto2.createHash)("sha256").update(token).digest("hex");
715
465
  }
716
466
  var OAuthTokenService = class _OAuthTokenService {
717
467
  constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
@@ -863,7 +613,9 @@ var OAuthTokenService = class _OAuthTokenService {
863
613
  }
864
614
  try {
865
615
  const access = await this.exchangeProviderAccessToken({
866
- token: sessionToken
616
+ providerRefreshToken: sessionToken,
617
+ // TODO:
618
+ userId: ""
867
619
  });
868
620
  if (access.refresh_token) {
869
621
  await this.oauthRepo.upsertUserCredentials(userId, {
@@ -873,6 +625,7 @@ var OAuthTokenService = class _OAuthTokenService {
873
625
  });
874
626
  }
875
627
  return {
628
+ ...access,
876
629
  access_token: access.access_token,
877
630
  expires_in: access.expires_in
878
631
  };
@@ -885,7 +638,7 @@ var OAuthTokenService = class _OAuthTokenService {
885
638
  }
886
639
  }
887
640
  async issueMiddlewareRefreshToken(clientId, userId) {
888
- const plain = (0, import_crypto3.randomBytes)(32).toString("base64url");
641
+ const plain = (0, import_crypto2.randomBytes)(32).toString("base64url");
889
642
  const expiresAt = new Date(
890
643
  Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
891
644
  ).toISOString();
@@ -933,9 +686,88 @@ var OAuthTokenService = class _OAuthTokenService {
933
686
  };
934
687
 
935
688
  // src/server/services/OAuthWrapperService.ts
936
- var OAuthWrapperService = class extends OAuthAbstractProvider {
689
+ var import_fe_corekit2 = require("@qlover/fe-corekit");
690
+
691
+ // src/server/utils/authorizeUtil.ts
692
+ function validatePkceParams(parsed, confidential) {
693
+ const hasChallenge = Boolean(parsed.code_challenge?.trim());
694
+ const hasMethod = Boolean(parsed.code_challenge_method);
695
+ if (!confidential) {
696
+ if (!hasChallenge || parsed.code_challenge_method !== "S256") {
697
+ return {
698
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
699
+ message: "Public clients must send code_challenge and code_challenge_method=S256."
700
+ };
701
+ }
702
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
703
+ return {
704
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
705
+ message: "Invalid code_challenge."
706
+ };
707
+ }
708
+ return null;
709
+ }
710
+ if (hasChallenge !== hasMethod) {
711
+ return {
712
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
713
+ message: "code_challenge and code_challenge_method must be sent together."
714
+ };
715
+ }
716
+ if (hasChallenge) {
717
+ if (parsed.code_challenge_method !== "S256") {
718
+ return {
719
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
720
+ message: "Only code_challenge_method=S256 is supported."
721
+ };
722
+ }
723
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
724
+ return {
725
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
726
+ message: "Invalid code_challenge."
727
+ };
728
+ }
729
+ }
730
+ return null;
731
+ }
732
+ function normalizeQuery(raw) {
733
+ const result = {};
734
+ for (const [key, value] of Object.entries(raw)) {
735
+ if (Array.isArray(value)) {
736
+ result[key] = value[0];
737
+ } else {
738
+ result[key] = value;
739
+ }
740
+ }
741
+ if (!result.response_type) {
742
+ result.response_type = "code";
743
+ }
744
+ return result;
745
+ }
746
+ function isRedirectUriAllowed(redirectUri, client) {
747
+ return client.redirect_uris.includes(redirectUri);
748
+ }
749
+
750
+ // src/server/utils/oauthRedirectUtils.ts
751
+ function buildOAuthRedirectUrl(redirectUri, params) {
752
+ const url = new URL(redirectUri);
753
+ for (const [key, value] of Object.entries(params)) {
754
+ if (value != null && value !== "") {
755
+ url.searchParams.set(key, value);
756
+ }
757
+ }
758
+ return url.toString();
759
+ }
760
+ function parseScopeList(scope) {
761
+ if (!scope?.trim()) {
762
+ return ["openid", "profile", "email"];
763
+ }
764
+ return scope.trim().split(/\s+/).filter(Boolean);
765
+ }
766
+
767
+ // src/server/services/OAuthWrapperService.ts
768
+ var import_node_crypto = require("crypto");
769
+ var OAuthWrapperService = class {
937
770
  constructor(oauthSession, tokenEncryption, oauthRepo) {
938
- super();
939
771
  this.oauthSession = oauthSession;
940
772
  this.oauthRepo = oauthRepo;
941
773
  this.tokenService = new OAuthTokenService(
@@ -946,26 +778,29 @@ var OAuthWrapperService = class extends OAuthAbstractProvider {
946
778
  }
947
779
  oauthSession;
948
780
  oauthRepo;
781
+ authCodeTTLMs = 5 * 60 * 1e3;
949
782
  tokenService;
950
783
  /**
951
784
  * @override
952
785
  */
953
786
  async login(params) {
954
- const credentials = await this.providerLogin(params);
955
- const sessionToken = credentials.token;
787
+ const session = await this.providerLogin(params);
788
+ const sessionToken = session.providerRefreshToken;
956
789
  if (!sessionToken) {
957
790
  throw new Error("User provider login did not return a session token");
958
791
  }
959
792
  const userInfo = await this.providerGetUserInfo(sessionToken);
960
- const sessionPayload = this.generageSessionPayload({
961
- email: params.email,
962
- ...userInfo,
963
- sessionToken
964
- });
793
+ const sessionPayload = {
794
+ ...session,
795
+ user: {
796
+ ...session.user,
797
+ ...userInfo
798
+ }
799
+ };
965
800
  this.oauthSession.setSession(sessionPayload);
966
801
  const oauthrepo = this.getOAuthRepo();
967
802
  await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
968
- provider_session_token: sessionPayload.providerSessionToken
803
+ provider_session_token: sessionPayload.providerRefreshToken
969
804
  });
970
805
  return sessionPayload;
971
806
  }
@@ -1013,38 +848,182 @@ var OAuthWrapperService = class extends OAuthAbstractProvider {
1013
848
  async getUserInfoWithAccessToken(accessToken) {
1014
849
  try {
1015
850
  const profile = await this.providerGetUserInfoByAccessToken(accessToken);
1016
- return this.toUserInfoResponse(profile);
851
+ return this.toUser(profile);
1017
852
  } catch {
1018
853
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
1019
854
  }
1020
855
  }
1021
- toUserInfoResponse(profile) {
1022
- const sub = String(profile.id);
1023
- if (!sub || sub === "NaN") {
1024
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
856
+ toUser(profile) {
857
+ return profile;
858
+ }
859
+ isQuery(query) {
860
+ return OAuthAuthorizeQuerySchema.safeParse(query).success;
861
+ }
862
+ isValidateConsent(value) {
863
+ return OAuthConsentBodySchema.safeParse(value).success;
864
+ }
865
+ /**
866
+ * @override
867
+ */
868
+ async logout(userId) {
869
+ const oauthRepo = this.getOAuthRepo();
870
+ await oauthRepo.revokeRefreshTokensByUserId(userId);
871
+ await oauthRepo.upsertUserCredentials(userId, {
872
+ provider_refresh_token: null,
873
+ provider_session_token: null
874
+ });
875
+ await this.clearSession();
876
+ }
877
+ /**
878
+ * @override
879
+ */
880
+ async resolveAuthorizePage(rawQuery) {
881
+ const query = normalizeQuery(rawQuery);
882
+ if (!this.isQuery(query)) {
883
+ return {
884
+ ok: false,
885
+ error: {
886
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
887
+ message: "Missing or invalid authorization request parameters."
888
+ }
889
+ };
1025
890
  }
1026
- const email = profile.email?.trim();
1027
- if (!email) {
1028
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
891
+ if (query.response_type !== "code") {
892
+ return {
893
+ ok: false,
894
+ error: {
895
+ errorKey: OAuthRfcCodes.UNSUPPORTED_RESPONSE_TYPE,
896
+ message: "Only response_type=code is supported."
897
+ }
898
+ };
899
+ }
900
+ const oauthRepo = this.getOAuthRepo();
901
+ const client = await oauthRepo.findClientById(query.client_id);
902
+ if (!client) {
903
+ return {
904
+ ok: false,
905
+ error: {
906
+ errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
907
+ message: "Unknown client_id."
908
+ }
909
+ };
910
+ }
911
+ if (!isRedirectUriAllowed(query.redirect_uri, client)) {
912
+ return {
913
+ ok: false,
914
+ error: {
915
+ errorKey: OAuthRfcCodes.UNAUTHORIZED_CLIENT,
916
+ message: "redirect_uri is not registered for this client."
917
+ }
918
+ };
919
+ }
920
+ const requestedScopes = parseScopeList(query.scope);
921
+ const invalidScope = requestedScopes.find(
922
+ (scope) => !client.scopes.includes(scope)
923
+ );
924
+ if (invalidScope) {
925
+ return {
926
+ ok: false,
927
+ error: {
928
+ errorKey: OAuthRfcCodes.INVALID_SCOPE,
929
+ message: `Scope "${invalidScope}" is not allowed for this client.`
930
+ }
931
+ };
932
+ }
933
+ const pkceError = validatePkceParams(query, client.confidential);
934
+ if (pkceError) {
935
+ return { ok: false, error: pkceError };
936
+ }
937
+ return {
938
+ ok: true,
939
+ data: {
940
+ clientId: client.client_id,
941
+ clientName: client.client_name,
942
+ clientUri: client.client_uri ?? null,
943
+ logoUri: client.logo_uri ?? null,
944
+ redirectUri: query.redirect_uri,
945
+ scopes: requestedScopes,
946
+ state: query.state,
947
+ responseType: "code",
948
+ codeChallenge: query.code_challenge,
949
+ codeChallengeMethod: query.code_challenge_method,
950
+ confidential: client.confidential
951
+ }
952
+ };
953
+ }
954
+ /**
955
+ * @override
956
+ */
957
+ async processConsent(requestBody) {
958
+ if (!this.isValidateConsent(requestBody)) {
959
+ throw new import_fe_corekit2.ExecutorError(
960
+ OAuthRfcCodes.INVALID_REQUEST,
961
+ "Invalid consent request body"
962
+ );
963
+ }
964
+ const session = await this.getSession();
965
+ if (!session) {
966
+ throw new import_fe_corekit2.ExecutorError(
967
+ OAuthRfcCodes.ACCESS_DENIED,
968
+ "User session expired. Please sign in again."
969
+ );
970
+ }
971
+ const pageResult = await this.resolveAuthorizePage({
972
+ response_type: "code",
973
+ client_id: requestBody.client_id,
974
+ redirect_uri: requestBody.redirect_uri,
975
+ scope: requestBody.scope,
976
+ state: requestBody.state,
977
+ code_challenge: requestBody.code_challenge,
978
+ code_challenge_method: requestBody.code_challenge_method
979
+ });
980
+ if (!pageResult.ok) {
981
+ throw new import_fe_corekit2.ExecutorError(
982
+ pageResult.error.errorKey,
983
+ pageResult.error.message
984
+ );
985
+ }
986
+ const { data } = pageResult;
987
+ if (requestBody.action === "deny") {
988
+ return {
989
+ redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
990
+ error: OAuthRfcCodes.ACCESS_DENIED,
991
+ error_description: "The resource owner denied the request",
992
+ state: data.state
993
+ })
994
+ };
1029
995
  }
1030
- const name = profile.name?.trim() || email;
996
+ const code = (0, import_node_crypto.randomBytes)(32).toString("base64url");
997
+ const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
998
+ const oauthRepo = this.getOAuthRepo();
999
+ await oauthRepo.create({
1000
+ code,
1001
+ client_id: data.clientId,
1002
+ user_id: session.userId,
1003
+ redirect_uri: data.redirectUri,
1004
+ scope: data.scopes.join(" ") || null,
1005
+ code_challenge: data.codeChallenge ?? null,
1006
+ code_challenge_method: data.codeChallengeMethod ?? null,
1007
+ expires_at: expiresAt
1008
+ });
1009
+ void requestBody.trust;
1031
1010
  return {
1032
- sub,
1033
- email,
1034
- name,
1035
- ...profile.roles?.length ? { roles: profile.roles } : {}
1011
+ redirectUrl: buildOAuthRedirectUrl(data.redirectUri, {
1012
+ code,
1013
+ state: data.state
1014
+ })
1036
1015
  };
1037
1016
  }
1038
1017
  };
1039
1018
 
1040
1019
  // src/server/utils/clientSecretHash.ts
1041
- var import_crypto4 = require("crypto");
1020
+ var import_crypto3 = require("crypto");
1042
1021
  var import_util = require("util");
1043
- var scryptAsync = (0, import_util.promisify)(import_crypto4.scrypt);
1022
+ var scryptAsync = (0, import_util.promisify)(import_crypto3.scrypt);
1044
1023
  var PREFIX = "scrypt";
1045
1024
  var KEY_LEN = 64;
1046
1025
  async function hashClientSecret(secret) {
1047
- const salt = (0, import_crypto4.randomBytes)(16);
1026
+ const salt = (0, import_crypto3.randomBytes)(16);
1048
1027
  const derived = await scryptAsync(secret, salt, KEY_LEN);
1049
1028
  return `${PREFIX}$${salt.toString("base64")}$${derived.toString("base64")}`;
1050
1029
  }
@@ -1059,11 +1038,10 @@ async function verifyClientSecret(secret, storedHash) {
1059
1038
  if (derived.length !== expected.length) {
1060
1039
  return false;
1061
1040
  }
1062
- return (0, import_crypto4.timingSafeEqual)(derived, expected);
1041
+ return (0, import_crypto3.timingSafeEqual)(derived, expected);
1063
1042
  }
1064
1043
  // Annotate the CommonJS export names for ESM import in node:
1065
1044
  0 && (module.exports = {
1066
- OAuthAbstractProvider,
1067
1045
  OAuthAuthorizationCodeRowSchema,
1068
1046
  OAuthAuthorizeQuerySchema,
1069
1047
  OAuthClientCreateResponseSchema,