@qlover/oauth-wrapper 0.2.0 → 0.2.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,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,
@@ -63,119 +64,6 @@ __export(server_exports, {
63
64
  });
64
65
  module.exports = __toCommonJS(server_exports);
65
66
 
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
67
  // src/core/schema/OAuthAuthorizeSchema.ts
180
68
  var import_zod = require("zod");
181
69
  function isOAuthRedirectUri(value) {
@@ -192,7 +80,7 @@ function isOAuthRedirectUri(value) {
192
80
  }
193
81
  var oauthRedirectUriSchema = import_zod.z.string().min(1).refine(isOAuthRedirectUri, { message: "Invalid redirect URI" });
194
82
  var OAuthClientRowSchema = import_zod.z.object({
195
- id: import_zod.z.number(),
83
+ id: import_zod.z.string(),
196
84
  client_id: import_zod.z.string(),
197
85
  client_secret_hash: import_zod.z.string().nullable().optional(),
198
86
  client_name: import_zod.z.string(),
@@ -202,7 +90,7 @@ var OAuthClientRowSchema = import_zod.z.object({
202
90
  grant_types: import_zod.z.array(import_zod.z.string()),
203
91
  scopes: import_zod.z.array(import_zod.z.string()),
204
92
  confidential: import_zod.z.boolean(),
205
- owner_user_id: import_zod.z.number(),
93
+ owner_user_id: import_zod.z.string(),
206
94
  created_at: import_zod.z.string(),
207
95
  updated_at: import_zod.z.string()
208
96
  });
@@ -275,7 +163,7 @@ var OAuthConsentBodySchema = import_zod.z.object({
275
163
  var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
276
164
  code: import_zod.z.string(),
277
165
  client_id: import_zod.z.string(),
278
- user_id: import_zod.z.number(),
166
+ user_id: import_zod.z.string(),
279
167
  redirect_uri: import_zod.z.string(),
280
168
  scope: import_zod.z.string().nullable().optional(),
281
169
  code_challenge: import_zod.z.string().nullable().optional(),
@@ -288,16 +176,16 @@ var OAuthAuthorizationCodeRowSchema = import_zod.z.object({
288
176
  // src/core/schema/OAuthClientSchema.ts
289
177
  var import_zod2 = require("zod");
290
178
  var OAuthUserCredentialsSchema = import_zod2.z.object({
291
- user_id: import_zod2.z.number(),
179
+ user_id: import_zod2.z.string(),
292
180
  provider_refresh_token: import_zod2.z.string().nullable().optional(),
293
181
  provider_session_token: import_zod2.z.string().nullable().optional(),
294
182
  updated_at: import_zod2.z.string()
295
183
  });
296
184
  var OAuthRefreshTokenSchema = import_zod2.z.object({
297
- id: import_zod2.z.number(),
185
+ id: import_zod2.z.string(),
298
186
  refresh_token: import_zod2.z.string(),
299
187
  client_id: import_zod2.z.string(),
300
- user_id: import_zod2.z.number(),
188
+ user_id: import_zod2.z.string(),
301
189
  expires_at: import_zod2.z.string(),
302
190
  revoked: import_zod2.z.boolean(),
303
191
  created_at: import_zod2.z.string()
@@ -314,12 +202,12 @@ var OAuthTokenResponseSchema = import_zod2.z.object({
314
202
  var import_zod3 = require("zod");
315
203
  var OAuthUserInfoResponseSchema = import_zod3.z.object({
316
204
  sub: import_zod3.z.string(),
317
- email: import_zod3.z.string().email(),
205
+ email: import_zod3.z.email(),
318
206
  name: import_zod3.z.string(),
319
207
  roles: import_zod3.z.array(import_zod3.z.string()).optional()
320
208
  });
321
209
  var OAuthUserInfoErrorResponseSchema = import_zod3.z.object({
322
- error: import_zod3.z.literal("invalid_token"),
210
+ error: import_zod3.z.string(),
323
211
  error_id: import_zod3.z.string().optional()
324
212
  });
325
213
 
@@ -400,16 +288,6 @@ function randomOAuthState() {
400
288
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
401
289
  }
402
290
 
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
291
  // src/server/utils/pkce.ts
414
292
  var import_crypto = require("crypto");
415
293
  var PKCE_VERIFIER_MIN = 43;
@@ -443,267 +321,43 @@ function verifyPkceS256(verifier, challenge) {
443
321
  }
444
322
  }
445
323
 
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
- );
470
- }
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);
479
- }
480
- if (!client.grant_types.includes(request.grant_type)) {
481
- throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
324
+ // src/server/utils/authorizeUtil.ts
325
+ function validatePkceParams(parsed, confidential) {
326
+ const hasChallenge = Boolean(parsed.code_challenge?.trim());
327
+ const hasMethod = Boolean(parsed.code_challenge_method);
328
+ if (!confidential) {
329
+ if (!hasChallenge || parsed.code_challenge_method !== "S256") {
330
+ return {
331
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
332
+ message: "Public clients must send code_challenge and code_challenge_method=S256."
333
+ };
482
334
  }
483
- if (request.grant_type === "authorization_code") {
484
- return await this.exchangeAuthorizationCode(request, client.client_id);
335
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
336
+ return {
337
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
338
+ message: "Invalid code_challenge."
339
+ };
485
340
  }
486
- return await this.exchangeRefreshToken(request, client.client_id);
341
+ return null;
487
342
  }
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
- );
503
- }
504
- if (authCode.redirect_uri !== request.redirect_uri) {
505
- throw new OAuthWrapperError(
506
- OAuthRfcCodes.INVALID_GRANT,
507
- 400,
508
- "redirect_uri mismatch"
509
- );
510
- }
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
- );
343
+ if (hasChallenge !== hasMethod) {
519
344
  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
345
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
346
+ message: "code_challenge and code_challenge_method must be sent together."
525
347
  };
526
348
  }
527
- assertPkceForAuthorizationCode(request, authCode) {
528
- const storedChallenge = authCode.code_challenge?.trim();
529
- if (storedChallenge) {
530
- const verifier = request.code_verifier?.trim();
531
- if (!verifier) {
532
- throw new OAuthWrapperError(
533
- OAuthRfcCodes.INVALID_GRANT,
534
- 400,
535
- "code_verifier is required"
536
- );
537
- }
538
- if (authCode.code_challenge_method !== "S256") {
539
- throw new OAuthWrapperError(
540
- OAuthRfcCodes.INVALID_GRANT,
541
- 400,
542
- "Unsupported PKCE method"
543
- );
544
- }
545
- if (!verifyPkceS256(verifier, storedChallenge)) {
546
- throw new OAuthWrapperError(
547
- OAuthRfcCodes.INVALID_GRANT,
548
- 400,
549
- "code_verifier mismatch"
550
- );
551
- }
552
- return;
349
+ if (hasChallenge) {
350
+ if (parsed.code_challenge_method !== "S256") {
351
+ return {
352
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
353
+ message: "Only code_challenge_method=S256 is supported."
354
+ };
553
355
  }
554
- if (!request.client_secret?.trim()) {
555
- throw new OAuthWrapperError(
556
- OAuthRfcCodes.INVALID_CLIENT,
557
- 401,
558
- "client_secret is required when PKCE is not used"
559
- );
560
- }
561
- }
562
- async exchangeRefreshToken(request, verifiedClientId) {
563
- const tokenHash = hashOpaqueToken(request.refresh_token);
564
- const stored = await this.oauthRepo.findByTokenHash(tokenHash);
565
- if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
566
- throw new OAuthWrapperError(
567
- OAuthRfcCodes.INVALID_GRANT,
568
- 400,
569
- "Refresh token is invalid"
570
- );
571
- }
572
- const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
573
- await this.oauthRepo.revokeByTokenHash(tokenHash);
574
- const middlewareRefresh = await this.issueMiddlewareRefreshToken(
575
- stored.client_id,
576
- stored.user_id
577
- );
578
- return {
579
- access_token: providerTokens.access_token,
580
- token_type: "Bearer",
581
- expires_in: providerTokens.expires_in,
582
- refresh_token: middlewareRefresh
583
- };
584
- }
585
- async fetchProviderAccessToken(userId) {
586
- const credentials = await this.oauthRepo.getUserCredentials(userId);
587
- const sessionToken = credentials?.provider_session_token?.trim();
588
- if (!sessionToken) {
589
- throw new OAuthWrapperError(
590
- OAuthRfcCodes.INVALID_GRANT,
591
- 400,
592
- "User credentials expired. Re-authorization required."
593
- );
594
- }
595
- try {
596
- const access = await this.userAdapter.exchangeAccessToken({
597
- token: sessionToken
598
- });
599
- if (access.refresh_token) {
600
- await this.oauthRepo.upsertUserCredentials(userId, {
601
- provider_refresh_token: this.tokenEncryption.encrypt(
602
- access.refresh_token
603
- )
604
- });
605
- }
606
- return {
607
- access_token: access.access_token,
608
- expires_in: access.expires_in
609
- };
610
- } catch {
611
- throw new OAuthWrapperError(
612
- OAuthRfcCodes.INVALID_GRANT,
613
- 400,
614
- "Failed to obtain access token from user provider"
615
- );
616
- }
617
- }
618
- async issueMiddlewareRefreshToken(clientId, userId) {
619
- const plain = (0, import_crypto2.randomBytes)(32).toString("base64url");
620
- const expiresAt = new Date(
621
- Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
622
- ).toISOString();
623
- await this.oauthRepo.createRefreshToken({
624
- refresh_token: hashOpaqueToken(plain),
625
- client_id: clientId,
626
- user_id: userId,
627
- expires_at: expiresAt
628
- });
629
- return plain;
630
- }
631
- /**
632
- * @override
633
- * RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
634
- */
635
- async revokeToken(rawFields) {
636
- let request;
637
- try {
638
- request = OAuthTokenRevokeSchema.parse(rawFields);
639
- } catch {
640
- throw new OAuthWrapperError(
641
- OAuthRfcCodes.INVALID_REQUEST,
642
- 400,
643
- "Malformed revocation request"
644
- );
645
- }
646
- try {
647
- await this.oauthRepo.verifyClientCredentials(
648
- request.client_id,
649
- request.client_secret
650
- );
651
- } catch {
652
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
653
- }
654
- if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
655
- return;
656
- }
657
- const tokenHash = hashOpaqueToken(request.token);
658
- const stored = await this.oauthRepo.findByTokenHash(tokenHash);
659
- if (!stored || stored.client_id !== request.client_id) {
660
- return;
661
- }
662
- await this.oauthRepo.revokeByTokenHash(tokenHash);
663
- }
664
- };
665
-
666
- // 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
- };
356
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
357
+ return {
358
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
359
+ message: "Invalid code_challenge."
360
+ };
707
361
  }
708
362
  }
709
363
  return null;
@@ -743,44 +397,36 @@ function parseScopeList(scope) {
743
397
  return scope.trim().split(/\s+/).filter(Boolean);
744
398
  }
745
399
 
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) {
750
- this.oauthSession = oauthSession;
751
- this.userAdapter = userAdapter;
752
- this.tokenService = tokenService;
753
- this.oauthRepo = oauthRepo;
754
- }
755
- /**
756
- * @override
757
- */
758
- getOAuthSession() {
759
- return this.oauthSession;
400
+ // src/server/services/OAuthAbstractProvider.ts
401
+ var import_fe_corekit = require("@qlover/fe-corekit");
402
+ var import_crypto2 = require("crypto");
403
+ var OAuthAbstractProvider = class {
404
+ authCodeTTLMs = 5 * 60 * 1e3;
405
+ isQuery(query) {
406
+ return OAuthAuthorizeQuerySchema.safeParse(query).success;
760
407
  }
761
- /**
762
- * @override
763
- */
764
- getOAuthAdapter() {
765
- return this.userAdapter;
408
+ isValidateConsent(value) {
409
+ return OAuthConsentBodySchema.safeParse(value).success;
766
410
  }
767
- /**
768
- * @override
769
- */
770
- getOAuthTokenService() {
771
- return this.tokenService;
411
+ generageSessionPayload(userInfo) {
412
+ return {
413
+ userId: String(userInfo.id),
414
+ email: userInfo.email,
415
+ name: userInfo.name ?? userInfo.email,
416
+ providerSessionToken: userInfo.sessionToken
417
+ };
772
418
  }
773
419
  /**
774
420
  * @override
775
421
  */
776
- getOAuthRepo() {
777
- return this.oauthRepo;
778
- }
779
- isQuery(query) {
780
- return OAuthAuthorizeQuerySchema.safeParse(query).success;
781
- }
782
- isValidateConsent(value) {
783
- return OAuthConsentBodySchema.safeParse(value).success;
422
+ async logout(userId) {
423
+ const oauthRepo = this.getOAuthRepo();
424
+ await oauthRepo.revokeRefreshTokensByUserId(userId);
425
+ await oauthRepo.upsertUserCredentials(userId, {
426
+ provider_refresh_token: null,
427
+ provider_session_token: null
428
+ });
429
+ await this.clearSession();
784
430
  }
785
431
  /**
786
432
  * @override
@@ -805,7 +451,8 @@ var OAuthWrapperService = class {
805
451
  }
806
452
  };
807
453
  }
808
- const client = await this.oauthRepo.findClientById(query.client_id);
454
+ const oauthRepo = this.getOAuthRepo();
455
+ const client = await oauthRepo.findClientById(query.client_id);
809
456
  if (!client) {
810
457
  return {
811
458
  ok: false,
@@ -863,14 +510,14 @@ var OAuthWrapperService = class {
863
510
  */
864
511
  async processConsent(requestBody) {
865
512
  if (!this.isValidateConsent(requestBody)) {
866
- throw new import_fe_corekit2.ExecutorError(
513
+ throw new import_fe_corekit.ExecutorError(
867
514
  OAuthRfcCodes.INVALID_REQUEST,
868
515
  "Invalid consent request body"
869
516
  );
870
517
  }
871
- const session = await this.oauthSession.getSession();
518
+ const session = await this.getSession();
872
519
  if (!session) {
873
- throw new import_fe_corekit2.ExecutorError(
520
+ throw new import_fe_corekit.ExecutorError(
874
521
  OAuthRfcCodes.ACCESS_DENIED,
875
522
  "User session expired. Please sign in again."
876
523
  );
@@ -885,7 +532,7 @@ var OAuthWrapperService = class {
885
532
  code_challenge_method: requestBody.code_challenge_method
886
533
  });
887
534
  if (!pageResult.ok) {
888
- throw new import_fe_corekit2.ExecutorError(
535
+ throw new import_fe_corekit.ExecutorError(
889
536
  pageResult.error.errorKey,
890
537
  pageResult.error.message
891
538
  );
@@ -900,9 +547,10 @@ var OAuthWrapperService = class {
900
547
  })
901
548
  };
902
549
  }
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({
550
+ const code = (0, import_crypto2.randomBytes)(32).toString("base64url");
551
+ const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
552
+ const oauthRepo = this.getOAuthRepo();
553
+ await oauthRepo.create({
906
554
  code,
907
555
  client_id: data.clientId,
908
556
  user_id: session.userId,
@@ -920,21 +568,423 @@ var OAuthWrapperService = class {
920
568
  })
921
569
  };
922
570
  }
571
+ };
572
+
573
+ // src/server/services/OAuthClientsService.ts
574
+ var OAuthClientsService = class {
575
+ constructor(clientsRepo) {
576
+ this.clientsRepo = clientsRepo;
577
+ }
578
+ clientsRepo;
923
579
  /**
580
+ * List all OAuth clients owned by a user
924
581
  * @override
925
582
  */
926
- async exchangeToken(rawFields) {
927
- return await this.tokenService.exchangeToken(rawFields);
583
+ async listForOwner(ownerUserId) {
584
+ return this.clientsRepo.listClientByOwner(ownerUserId);
928
585
  }
929
586
  /**
587
+ * Get detailed information about a specific OAuth client
930
588
  * @override
931
589
  */
932
- async revokeToken(rawFields) {
933
- return await this.tokenService.revokeToken(rawFields);
590
+ async getByClientId(ownerUserId, clientId) {
591
+ const client = await this.clientsRepo.findClientById(clientId);
592
+ if (!client) {
593
+ throw new Error("Client not found");
594
+ }
595
+ if (client.owner_user_id !== ownerUserId) {
596
+ throw new Error("Access denied");
597
+ }
598
+ return this.mapToDetail(client);
599
+ }
600
+ /**
601
+ * Create a new OAuth client
602
+ * @override
603
+ */
604
+ async create(ownerUserId, input) {
605
+ const result = await this.clientsRepo.createClient(ownerUserId, input);
606
+ return {
607
+ client_id: result.client.client_id,
608
+ client_secret: result.clientSecret,
609
+ confidential: result.client.confidential,
610
+ client_name: result.client.client_name,
611
+ client_uri: result.client.client_uri,
612
+ redirect_uris: result.client.redirect_uris,
613
+ created_at: result.client.created_at
614
+ };
615
+ }
616
+ /**
617
+ * Update an existing OAuth client
618
+ * @override
619
+ */
620
+ async update(ownerUserId, clientId, input) {
621
+ const existing = await this.clientsRepo.findClientById(clientId);
622
+ if (!existing) {
623
+ throw new Error("Client not found");
624
+ }
625
+ if (existing.owner_user_id !== ownerUserId) {
626
+ throw new Error("Access denied");
627
+ }
628
+ return this.clientsRepo.updateClient(ownerUserId, clientId, input);
629
+ }
630
+ /**
631
+ * Rotate the client secret
632
+ * @override
633
+ */
634
+ async rotateSecret(ownerUserId, clientId) {
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
+ if (!existing.confidential) {
643
+ throw new Error("Public clients do not have a client_secret");
644
+ }
645
+ const result = await this.clientsRepo.rotateClientSecret(
646
+ ownerUserId,
647
+ clientId
648
+ );
649
+ return {
650
+ client_id: clientId,
651
+ client_secret: result.clientSecret
652
+ };
934
653
  }
935
654
  /**
655
+ * Delete an OAuth client
936
656
  * @override
937
657
  */
658
+ async delete(ownerUserId, clientId) {
659
+ const existing = await this.clientsRepo.findClientById(clientId);
660
+ if (!existing) {
661
+ throw new Error("Client not found");
662
+ }
663
+ if (existing.owner_user_id !== ownerUserId) {
664
+ throw new Error("Access denied");
665
+ }
666
+ await this.clientsRepo.deleteClient(ownerUserId, clientId);
667
+ }
668
+ mapToDetail(row) {
669
+ return {
670
+ client_id: row.client_id,
671
+ client_name: row.client_name,
672
+ client_uri: row.client_uri,
673
+ logo_uri: row.logo_uri,
674
+ redirect_uris: row.redirect_uris,
675
+ grant_types: row.grant_types,
676
+ scopes: row.scopes,
677
+ confidential: row.confidential,
678
+ created_at: row.created_at,
679
+ updated_at: row.updated_at
680
+ };
681
+ }
682
+ };
683
+
684
+ // src/server/services/OAuthTokenService.ts
685
+ var import_crypto3 = require("crypto");
686
+
687
+ // src/server/utils/OAuthWrapperError.ts
688
+ var import_fe_corekit2 = require("@qlover/fe-corekit");
689
+ var OAuthWrapperError = class extends import_fe_corekit2.ExecutorError {
690
+ constructor(id, status, cause) {
691
+ super(id, cause);
692
+ this.status = status;
693
+ }
694
+ status;
695
+ name = "OAuthWrapperError";
696
+ };
697
+
698
+ // src/server/services/OAuthTokenService.ts
699
+ function hashOpaqueToken(token) {
700
+ return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
701
+ }
702
+ var OAuthTokenService = class _OAuthTokenService {
703
+ constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
704
+ this.tokenEncryption = tokenEncryption;
705
+ this.exchangeProviderAccessToken = exchangeProviderAccessToken;
706
+ this.oauthRepo = oauthRepo;
707
+ }
708
+ tokenEncryption;
709
+ exchangeProviderAccessToken;
710
+ oauthRepo;
711
+ static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
712
+ /**
713
+ * @override
714
+ */
715
+ async exchangeToken(rawFields) {
716
+ let request;
717
+ try {
718
+ request = OAuthTokenRequestSchema.parse(rawFields);
719
+ } catch {
720
+ throw new OAuthWrapperError(
721
+ OAuthRfcCodes.INVALID_REQUEST,
722
+ 400,
723
+ "Malformed token request"
724
+ );
725
+ }
726
+ let client;
727
+ try {
728
+ client = await this.oauthRepo.verifyClientCredentials(
729
+ request.client_id,
730
+ request.client_secret
731
+ );
732
+ } catch {
733
+ throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
734
+ }
735
+ if (!client.grant_types.includes(request.grant_type)) {
736
+ throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
737
+ }
738
+ if (request.grant_type === "authorization_code") {
739
+ return await this.exchangeAuthorizationCode(request, client.client_id);
740
+ }
741
+ return await this.exchangeRefreshToken(request, client.client_id);
742
+ }
743
+ async exchangeAuthorizationCode(request, verifiedClientId) {
744
+ const authCode = await this.oauthRepo.consumeCode(request.code);
745
+ if (!authCode) {
746
+ throw new OAuthWrapperError(
747
+ OAuthRfcCodes.INVALID_GRANT,
748
+ 400,
749
+ "Authorization code is invalid, expired, or already used"
750
+ );
751
+ }
752
+ if (authCode.client_id !== verifiedClientId) {
753
+ throw new OAuthWrapperError(
754
+ OAuthRfcCodes.INVALID_GRANT,
755
+ 400,
756
+ "client_id mismatch"
757
+ );
758
+ }
759
+ if (authCode.redirect_uri !== request.redirect_uri) {
760
+ throw new OAuthWrapperError(
761
+ OAuthRfcCodes.INVALID_GRANT,
762
+ 400,
763
+ "redirect_uri mismatch"
764
+ );
765
+ }
766
+ this.assertPkceForAuthorizationCode(request, authCode);
767
+ const providerTokens = await this.fetchProviderAccessToken(
768
+ authCode.user_id
769
+ );
770
+ const middlewareRefresh = await this.issueMiddlewareRefreshToken(
771
+ authCode.client_id,
772
+ authCode.user_id
773
+ );
774
+ return {
775
+ access_token: providerTokens.access_token,
776
+ token_type: "Bearer",
777
+ expires_in: providerTokens.expires_in,
778
+ refresh_token: middlewareRefresh,
779
+ scope: authCode.scope ?? void 0
780
+ };
781
+ }
782
+ assertPkceForAuthorizationCode(request, authCode) {
783
+ const storedChallenge = authCode.code_challenge?.trim();
784
+ if (storedChallenge) {
785
+ const verifier = request.code_verifier?.trim();
786
+ if (!verifier) {
787
+ throw new OAuthWrapperError(
788
+ OAuthRfcCodes.INVALID_GRANT,
789
+ 400,
790
+ "code_verifier is required"
791
+ );
792
+ }
793
+ if (authCode.code_challenge_method !== "S256") {
794
+ throw new OAuthWrapperError(
795
+ OAuthRfcCodes.INVALID_GRANT,
796
+ 400,
797
+ "Unsupported PKCE method"
798
+ );
799
+ }
800
+ if (!verifyPkceS256(verifier, storedChallenge)) {
801
+ throw new OAuthWrapperError(
802
+ OAuthRfcCodes.INVALID_GRANT,
803
+ 400,
804
+ "code_verifier mismatch"
805
+ );
806
+ }
807
+ return;
808
+ }
809
+ if (!request.client_secret?.trim()) {
810
+ throw new OAuthWrapperError(
811
+ OAuthRfcCodes.INVALID_CLIENT,
812
+ 401,
813
+ "client_secret is required when PKCE is not used"
814
+ );
815
+ }
816
+ }
817
+ async exchangeRefreshToken(request, verifiedClientId) {
818
+ const tokenHash = hashOpaqueToken(request.refresh_token);
819
+ const stored = await this.oauthRepo.findByTokenHash(tokenHash);
820
+ if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
821
+ throw new OAuthWrapperError(
822
+ OAuthRfcCodes.INVALID_GRANT,
823
+ 400,
824
+ "Refresh token is invalid"
825
+ );
826
+ }
827
+ const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
828
+ await this.oauthRepo.revokeByTokenHash(tokenHash);
829
+ const middlewareRefresh = await this.issueMiddlewareRefreshToken(
830
+ stored.client_id,
831
+ stored.user_id
832
+ );
833
+ return {
834
+ access_token: providerTokens.access_token,
835
+ token_type: "Bearer",
836
+ expires_in: providerTokens.expires_in,
837
+ refresh_token: middlewareRefresh
838
+ };
839
+ }
840
+ async fetchProviderAccessToken(userId) {
841
+ const credentials = await this.oauthRepo.getUserCredentials(userId);
842
+ const sessionToken = credentials?.provider_session_token?.trim();
843
+ if (!sessionToken) {
844
+ throw new OAuthWrapperError(
845
+ OAuthRfcCodes.INVALID_GRANT,
846
+ 400,
847
+ "User credentials expired. Re-authorization required."
848
+ );
849
+ }
850
+ try {
851
+ const access = await this.exchangeProviderAccessToken({
852
+ token: sessionToken
853
+ });
854
+ if (access.refresh_token) {
855
+ await this.oauthRepo.upsertUserCredentials(userId, {
856
+ provider_refresh_token: this.tokenEncryption.encrypt(
857
+ access.refresh_token
858
+ )
859
+ });
860
+ }
861
+ return {
862
+ access_token: access.access_token,
863
+ expires_in: access.expires_in
864
+ };
865
+ } catch {
866
+ throw new OAuthWrapperError(
867
+ OAuthRfcCodes.INVALID_GRANT,
868
+ 400,
869
+ "Failed to obtain access token from user provider"
870
+ );
871
+ }
872
+ }
873
+ async issueMiddlewareRefreshToken(clientId, userId) {
874
+ const plain = (0, import_crypto3.randomBytes)(32).toString("base64url");
875
+ const expiresAt = new Date(
876
+ Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
877
+ ).toISOString();
878
+ await this.oauthRepo.createRefreshToken({
879
+ refresh_token: hashOpaqueToken(plain),
880
+ client_id: clientId,
881
+ user_id: userId,
882
+ expires_at: expiresAt
883
+ });
884
+ return plain;
885
+ }
886
+ /**
887
+ * @override
888
+ * RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
889
+ */
890
+ async revokeToken(rawFields) {
891
+ let request;
892
+ try {
893
+ request = OAuthTokenRevokeSchema.parse(rawFields);
894
+ } catch {
895
+ throw new OAuthWrapperError(
896
+ OAuthRfcCodes.INVALID_REQUEST,
897
+ 400,
898
+ "Malformed revocation request"
899
+ );
900
+ }
901
+ try {
902
+ await this.oauthRepo.verifyClientCredentials(
903
+ request.client_id,
904
+ request.client_secret
905
+ );
906
+ } catch {
907
+ throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
908
+ }
909
+ if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
910
+ return;
911
+ }
912
+ const tokenHash = hashOpaqueToken(request.token);
913
+ const stored = await this.oauthRepo.findByTokenHash(tokenHash);
914
+ if (!stored || stored.client_id !== request.client_id) {
915
+ return;
916
+ }
917
+ await this.oauthRepo.revokeByTokenHash(tokenHash);
918
+ }
919
+ };
920
+
921
+ // src/server/services/OAuthWrapperService.ts
922
+ var OAuthWrapperService = class extends OAuthAbstractProvider {
923
+ constructor(oauthSession, tokenEncryption, oauthRepo) {
924
+ super();
925
+ this.oauthSession = oauthSession;
926
+ this.oauthRepo = oauthRepo;
927
+ this.tokenService = new OAuthTokenService(
928
+ tokenEncryption,
929
+ (params) => this.providerExchangeAccessToken(params),
930
+ oauthRepo
931
+ );
932
+ }
933
+ oauthSession;
934
+ oauthRepo;
935
+ tokenService;
936
+ /**
937
+ * @override
938
+ */
939
+ async login(params) {
940
+ const credentials = await this.providerLogin(params);
941
+ const sessionToken = credentials.token;
942
+ if (!sessionToken) {
943
+ throw new Error("User provider login did not return a session token");
944
+ }
945
+ const userInfo = await this.providerGetUserInfo(sessionToken);
946
+ const sessionPayload = this.generageSessionPayload({
947
+ email: params.email,
948
+ ...userInfo,
949
+ sessionToken
950
+ });
951
+ this.oauthSession.setSession(sessionPayload);
952
+ const oauthrepo = this.getOAuthRepo();
953
+ await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
954
+ provider_session_token: sessionPayload.providerSessionToken
955
+ });
956
+ return sessionPayload;
957
+ }
958
+ /**
959
+ * @override
960
+ */
961
+ getOAuthRepo() {
962
+ return this.oauthRepo;
963
+ }
964
+ /**
965
+ * @override
966
+ */
967
+ async exchangeToken(rawFields) {
968
+ return await this.tokenService.exchangeToken(rawFields);
969
+ }
970
+ /**
971
+ * @override
972
+ */
973
+ getSession() {
974
+ return this.oauthSession.getSession();
975
+ }
976
+ /**
977
+ * @override
978
+ */
979
+ clearSession() {
980
+ return this.oauthSession.clearSession();
981
+ }
982
+ /**
983
+ * @override
984
+ */
985
+ async revokeToken(rawFields) {
986
+ return await this.tokenService.revokeToken(rawFields);
987
+ }
938
988
  async logoutUser(userId) {
939
989
  await this.oauthRepo.revokeRefreshTokensByUserId(userId);
940
990
  await this.oauthRepo.upsertUserCredentials(userId, {
@@ -946,9 +996,9 @@ var OAuthWrapperService = class {
946
996
  /**
947
997
  * @override
948
998
  */
949
- async getUserInfo(accessToken) {
999
+ async getUserInfoWithAccessToken(accessToken) {
950
1000
  try {
951
- const profile = await this.userAdapter.getUserInfoByAccessToken(accessToken);
1001
+ const profile = await this.providerGetUserInfoByAccessToken(accessToken);
952
1002
  return this.toUserInfoResponse(profile);
953
1003
  } catch {
954
1004
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
@@ -963,8 +1013,7 @@ var OAuthWrapperService = class {
963
1013
  if (!email) {
964
1014
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
965
1015
  }
966
- const nameFromParts = [profile.first_name, profile.last_name].filter(Boolean).join(" ");
967
- const name = profile.name?.trim() || nameFromParts || email;
1016
+ const name = profile.name?.trim() || email;
968
1017
  return {
969
1018
  sub,
970
1019
  email,
@@ -1000,6 +1049,7 @@ async function verifyClientSecret(secret, storedHash) {
1000
1049
  }
1001
1050
  // Annotate the CommonJS export names for ESM import in node:
1002
1051
  0 && (module.exports = {
1052
+ OAuthAbstractProvider,
1003
1053
  OAuthAuthorizationCodeRowSchema,
1004
1054
  OAuthAuthorizeQuerySchema,
1005
1055
  OAuthClientCreateResponseSchema,