@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.js CHANGED
@@ -1,116 +1,3 @@
1
- // src/server/services/OAuthClientsService.ts
2
- var OAuthClientsService = class {
3
- constructor(clientsRepo) {
4
- this.clientsRepo = clientsRepo;
5
- }
6
- /**
7
- * List all OAuth clients owned by a user
8
- * @override
9
- */
10
- async listForOwner(ownerUserId) {
11
- return this.clientsRepo.listClientByOwner(ownerUserId);
12
- }
13
- /**
14
- * Get detailed information about a specific OAuth client
15
- * @override
16
- */
17
- async getByClientId(ownerUserId, clientId) {
18
- const client = await this.clientsRepo.findClientById(clientId);
19
- if (!client) {
20
- throw new Error("Client not found");
21
- }
22
- if (client.owner_user_id !== ownerUserId) {
23
- throw new Error("Access denied");
24
- }
25
- return this.mapToDetail(client);
26
- }
27
- /**
28
- * Create a new OAuth client
29
- * @override
30
- */
31
- async create(ownerUserId, input) {
32
- const result = await this.clientsRepo.createClient(ownerUserId, input);
33
- return {
34
- client_id: result.client.client_id,
35
- client_secret: result.clientSecret,
36
- confidential: result.client.confidential,
37
- client_name: result.client.client_name,
38
- client_uri: result.client.client_uri,
39
- redirect_uris: result.client.redirect_uris,
40
- created_at: result.client.created_at
41
- };
42
- }
43
- /**
44
- * Update an existing OAuth client
45
- * @override
46
- */
47
- async update(ownerUserId, clientId, input) {
48
- const existing = await this.clientsRepo.findClientById(clientId);
49
- if (!existing) {
50
- throw new Error("Client not found");
51
- }
52
- if (existing.owner_user_id !== ownerUserId) {
53
- throw new Error("Access denied");
54
- }
55
- return this.clientsRepo.updateClient(ownerUserId, clientId, input);
56
- }
57
- /**
58
- * Rotate the client secret
59
- * @override
60
- */
61
- async rotateSecret(ownerUserId, clientId) {
62
- const existing = await this.clientsRepo.findClientById(clientId);
63
- if (!existing) {
64
- throw new Error("Client not found");
65
- }
66
- if (existing.owner_user_id !== ownerUserId) {
67
- throw new Error("Access denied");
68
- }
69
- if (!existing.confidential) {
70
- throw new Error("Public clients do not have a client_secret");
71
- }
72
- const result = await this.clientsRepo.rotateClientSecret(
73
- ownerUserId,
74
- clientId
75
- );
76
- return {
77
- client_id: clientId,
78
- client_secret: result.clientSecret
79
- };
80
- }
81
- /**
82
- * Delete an OAuth client
83
- * @override
84
- */
85
- async delete(ownerUserId, clientId) {
86
- const existing = await this.clientsRepo.findClientById(clientId);
87
- if (!existing) {
88
- throw new Error("Client not found");
89
- }
90
- if (existing.owner_user_id !== ownerUserId) {
91
- throw new Error("Access denied");
92
- }
93
- await this.clientsRepo.deleteClient(ownerUserId, clientId);
94
- }
95
- mapToDetail(row) {
96
- return {
97
- client_id: row.client_id,
98
- client_name: row.client_name,
99
- client_uri: row.client_uri,
100
- logo_uri: row.logo_uri,
101
- redirect_uris: row.redirect_uris,
102
- grant_types: row.grant_types,
103
- scopes: row.scopes,
104
- confidential: row.confidential,
105
- created_at: row.created_at,
106
- updated_at: row.updated_at
107
- };
108
- }
109
- };
110
-
111
- // src/server/services/OAuthTokenService.ts
112
- import { createHash as createHash2, randomBytes } from "crypto";
113
-
114
1
  // src/core/schema/OAuthAuthorizeSchema.ts
115
2
  import { z } from "zod";
116
3
  function isOAuthRedirectUri(value) {
@@ -127,7 +14,7 @@ function isOAuthRedirectUri(value) {
127
14
  }
128
15
  var oauthRedirectUriSchema = z.string().min(1).refine(isOAuthRedirectUri, { message: "Invalid redirect URI" });
129
16
  var OAuthClientRowSchema = z.object({
130
- id: z.number(),
17
+ id: z.string(),
131
18
  client_id: z.string(),
132
19
  client_secret_hash: z.string().nullable().optional(),
133
20
  client_name: z.string(),
@@ -137,7 +24,7 @@ var OAuthClientRowSchema = z.object({
137
24
  grant_types: z.array(z.string()),
138
25
  scopes: z.array(z.string()),
139
26
  confidential: z.boolean(),
140
- owner_user_id: z.number(),
27
+ owner_user_id: z.string(),
141
28
  created_at: z.string(),
142
29
  updated_at: z.string()
143
30
  });
@@ -210,7 +97,7 @@ var OAuthConsentBodySchema = z.object({
210
97
  var OAuthAuthorizationCodeRowSchema = z.object({
211
98
  code: z.string(),
212
99
  client_id: z.string(),
213
- user_id: z.number(),
100
+ user_id: z.string(),
214
101
  redirect_uri: z.string(),
215
102
  scope: z.string().nullable().optional(),
216
103
  code_challenge: z.string().nullable().optional(),
@@ -223,16 +110,16 @@ var OAuthAuthorizationCodeRowSchema = z.object({
223
110
  // src/core/schema/OAuthClientSchema.ts
224
111
  import { z as z2 } from "zod";
225
112
  var OAuthUserCredentialsSchema = z2.object({
226
- user_id: z2.number(),
113
+ user_id: z2.string(),
227
114
  provider_refresh_token: z2.string().nullable().optional(),
228
115
  provider_session_token: z2.string().nullable().optional(),
229
116
  updated_at: z2.string()
230
117
  });
231
118
  var OAuthRefreshTokenSchema = z2.object({
232
- id: z2.number(),
119
+ id: z2.string(),
233
120
  refresh_token: z2.string(),
234
121
  client_id: z2.string(),
235
- user_id: z2.number(),
122
+ user_id: z2.string(),
236
123
  expires_at: z2.string(),
237
124
  revoked: z2.boolean(),
238
125
  created_at: z2.string()
@@ -249,12 +136,12 @@ var OAuthTokenResponseSchema = z2.object({
249
136
  import { z as z3 } from "zod";
250
137
  var OAuthUserInfoResponseSchema = z3.object({
251
138
  sub: z3.string(),
252
- email: z3.string().email(),
139
+ email: z3.email(),
253
140
  name: z3.string(),
254
141
  roles: z3.array(z3.string()).optional()
255
142
  });
256
143
  var OAuthUserInfoErrorResponseSchema = z3.object({
257
- error: z3.literal("invalid_token"),
144
+ error: z3.string(),
258
145
  error_id: z3.string().optional()
259
146
  });
260
147
 
@@ -335,16 +222,6 @@ function randomOAuthState() {
335
222
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
336
223
  }
337
224
 
338
- // src/server/utils/OAuthWrapperError.ts
339
- import { ExecutorError } from "@qlover/fe-corekit";
340
- var OAuthWrapperError = class extends ExecutorError {
341
- constructor(id, status, cause) {
342
- super(id, cause);
343
- this.status = status;
344
- }
345
- name = "OAuthWrapperError";
346
- };
347
-
348
225
  // src/server/utils/pkce.ts
349
226
  import { createHash, timingSafeEqual } from "crypto";
350
227
  var PKCE_VERIFIER_MIN = 43;
@@ -378,267 +255,43 @@ function verifyPkceS256(verifier, challenge) {
378
255
  }
379
256
  }
380
257
 
381
- // src/server/services/OAuthTokenService.ts
382
- function hashOpaqueToken(token) {
383
- return createHash2("sha256").update(token).digest("hex");
384
- }
385
- var OAuthTokenService = class _OAuthTokenService {
386
- constructor(tokenEncryption, userAdapter, oauthRepo) {
387
- this.tokenEncryption = tokenEncryption;
388
- this.userAdapter = userAdapter;
389
- this.oauthRepo = oauthRepo;
390
- }
391
- static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
392
- /**
393
- * @override
394
- */
395
- async exchangeToken(rawFields) {
396
- let request;
397
- try {
398
- request = OAuthTokenRequestSchema.parse(rawFields);
399
- } catch {
400
- throw new OAuthWrapperError(
401
- OAuthRfcCodes.INVALID_REQUEST,
402
- 400,
403
- "Malformed token request"
404
- );
405
- }
406
- let client;
407
- try {
408
- client = await this.oauthRepo.verifyClientCredentials(
409
- request.client_id,
410
- request.client_secret
411
- );
412
- } catch {
413
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
414
- }
415
- if (!client.grant_types.includes(request.grant_type)) {
416
- throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
258
+ // src/server/utils/authorizeUtil.ts
259
+ function validatePkceParams(parsed, confidential) {
260
+ const hasChallenge = Boolean(parsed.code_challenge?.trim());
261
+ const hasMethod = Boolean(parsed.code_challenge_method);
262
+ if (!confidential) {
263
+ if (!hasChallenge || parsed.code_challenge_method !== "S256") {
264
+ return {
265
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
266
+ message: "Public clients must send code_challenge and code_challenge_method=S256."
267
+ };
417
268
  }
418
- if (request.grant_type === "authorization_code") {
419
- return await this.exchangeAuthorizationCode(request, client.client_id);
269
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
270
+ return {
271
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
272
+ message: "Invalid code_challenge."
273
+ };
420
274
  }
421
- return await this.exchangeRefreshToken(request, client.client_id);
275
+ return null;
422
276
  }
423
- async exchangeAuthorizationCode(request, verifiedClientId) {
424
- const authCode = await this.oauthRepo.consumeCode(request.code);
425
- if (!authCode) {
426
- throw new OAuthWrapperError(
427
- OAuthRfcCodes.INVALID_GRANT,
428
- 400,
429
- "Authorization code is invalid, expired, or already used"
430
- );
431
- }
432
- if (authCode.client_id !== verifiedClientId) {
433
- throw new OAuthWrapperError(
434
- OAuthRfcCodes.INVALID_GRANT,
435
- 400,
436
- "client_id mismatch"
437
- );
438
- }
439
- if (authCode.redirect_uri !== request.redirect_uri) {
440
- throw new OAuthWrapperError(
441
- OAuthRfcCodes.INVALID_GRANT,
442
- 400,
443
- "redirect_uri mismatch"
444
- );
445
- }
446
- this.assertPkceForAuthorizationCode(request, authCode);
447
- const providerTokens = await this.fetchProviderAccessToken(
448
- authCode.user_id
449
- );
450
- const middlewareRefresh = await this.issueMiddlewareRefreshToken(
451
- authCode.client_id,
452
- authCode.user_id
453
- );
277
+ if (hasChallenge !== hasMethod) {
454
278
  return {
455
- access_token: providerTokens.access_token,
456
- token_type: "Bearer",
457
- expires_in: providerTokens.expires_in,
458
- refresh_token: middlewareRefresh,
459
- scope: authCode.scope ?? void 0
279
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
280
+ message: "code_challenge and code_challenge_method must be sent together."
460
281
  };
461
282
  }
462
- assertPkceForAuthorizationCode(request, authCode) {
463
- const storedChallenge = authCode.code_challenge?.trim();
464
- if (storedChallenge) {
465
- const verifier = request.code_verifier?.trim();
466
- if (!verifier) {
467
- throw new OAuthWrapperError(
468
- OAuthRfcCodes.INVALID_GRANT,
469
- 400,
470
- "code_verifier is required"
471
- );
472
- }
473
- if (authCode.code_challenge_method !== "S256") {
474
- throw new OAuthWrapperError(
475
- OAuthRfcCodes.INVALID_GRANT,
476
- 400,
477
- "Unsupported PKCE method"
478
- );
479
- }
480
- if (!verifyPkceS256(verifier, storedChallenge)) {
481
- throw new OAuthWrapperError(
482
- OAuthRfcCodes.INVALID_GRANT,
483
- 400,
484
- "code_verifier mismatch"
485
- );
486
- }
487
- return;
283
+ if (hasChallenge) {
284
+ if (parsed.code_challenge_method !== "S256") {
285
+ return {
286
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
287
+ message: "Only code_challenge_method=S256 is supported."
288
+ };
488
289
  }
489
- if (!request.client_secret?.trim()) {
490
- throw new OAuthWrapperError(
491
- OAuthRfcCodes.INVALID_CLIENT,
492
- 401,
493
- "client_secret is required when PKCE is not used"
494
- );
495
- }
496
- }
497
- async exchangeRefreshToken(request, verifiedClientId) {
498
- const tokenHash = hashOpaqueToken(request.refresh_token);
499
- const stored = await this.oauthRepo.findByTokenHash(tokenHash);
500
- if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
501
- throw new OAuthWrapperError(
502
- OAuthRfcCodes.INVALID_GRANT,
503
- 400,
504
- "Refresh token is invalid"
505
- );
506
- }
507
- const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
508
- await this.oauthRepo.revokeByTokenHash(tokenHash);
509
- const middlewareRefresh = await this.issueMiddlewareRefreshToken(
510
- stored.client_id,
511
- stored.user_id
512
- );
513
- return {
514
- access_token: providerTokens.access_token,
515
- token_type: "Bearer",
516
- expires_in: providerTokens.expires_in,
517
- refresh_token: middlewareRefresh
518
- };
519
- }
520
- async fetchProviderAccessToken(userId) {
521
- const credentials = await this.oauthRepo.getUserCredentials(userId);
522
- const sessionToken = credentials?.provider_session_token?.trim();
523
- if (!sessionToken) {
524
- throw new OAuthWrapperError(
525
- OAuthRfcCodes.INVALID_GRANT,
526
- 400,
527
- "User credentials expired. Re-authorization required."
528
- );
529
- }
530
- try {
531
- const access = await this.userAdapter.exchangeAccessToken({
532
- token: sessionToken
533
- });
534
- if (access.refresh_token) {
535
- await this.oauthRepo.upsertUserCredentials(userId, {
536
- provider_refresh_token: this.tokenEncryption.encrypt(
537
- access.refresh_token
538
- )
539
- });
540
- }
541
- return {
542
- access_token: access.access_token,
543
- expires_in: access.expires_in
544
- };
545
- } catch {
546
- throw new OAuthWrapperError(
547
- OAuthRfcCodes.INVALID_GRANT,
548
- 400,
549
- "Failed to obtain access token from user provider"
550
- );
551
- }
552
- }
553
- async issueMiddlewareRefreshToken(clientId, userId) {
554
- const plain = randomBytes(32).toString("base64url");
555
- const expiresAt = new Date(
556
- Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
557
- ).toISOString();
558
- await this.oauthRepo.createRefreshToken({
559
- refresh_token: hashOpaqueToken(plain),
560
- client_id: clientId,
561
- user_id: userId,
562
- expires_at: expiresAt
563
- });
564
- return plain;
565
- }
566
- /**
567
- * @override
568
- * RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
569
- */
570
- async revokeToken(rawFields) {
571
- let request;
572
- try {
573
- request = OAuthTokenRevokeSchema.parse(rawFields);
574
- } catch {
575
- throw new OAuthWrapperError(
576
- OAuthRfcCodes.INVALID_REQUEST,
577
- 400,
578
- "Malformed revocation request"
579
- );
580
- }
581
- try {
582
- await this.oauthRepo.verifyClientCredentials(
583
- request.client_id,
584
- request.client_secret
585
- );
586
- } catch {
587
- throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
588
- }
589
- if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
590
- return;
591
- }
592
- const tokenHash = hashOpaqueToken(request.token);
593
- const stored = await this.oauthRepo.findByTokenHash(tokenHash);
594
- if (!stored || stored.client_id !== request.client_id) {
595
- return;
596
- }
597
- await this.oauthRepo.revokeByTokenHash(tokenHash);
598
- }
599
- };
600
-
601
- // src/server/services/OAuthWrapperService.ts
602
- import { randomBytes as randomBytes2 } from "crypto";
603
- import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
604
-
605
- // src/server/utils/authorizeUtil.ts
606
- function validatePkceParams(parsed, confidential) {
607
- const hasChallenge = Boolean(parsed.code_challenge?.trim());
608
- const hasMethod = Boolean(parsed.code_challenge_method);
609
- if (!confidential) {
610
- if (!hasChallenge || parsed.code_challenge_method !== "S256") {
611
- return {
612
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
613
- message: "Public clients must send code_challenge and code_challenge_method=S256."
614
- };
615
- }
616
- if (!isValidCodeChallenge(parsed.code_challenge)) {
617
- return {
618
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
619
- message: "Invalid code_challenge."
620
- };
621
- }
622
- return null;
623
- }
624
- if (hasChallenge !== hasMethod) {
625
- return {
626
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
627
- message: "code_challenge and code_challenge_method must be sent together."
628
- };
629
- }
630
- if (hasChallenge) {
631
- if (parsed.code_challenge_method !== "S256") {
632
- return {
633
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
634
- message: "Only code_challenge_method=S256 is supported."
635
- };
636
- }
637
- if (!isValidCodeChallenge(parsed.code_challenge)) {
638
- return {
639
- errorKey: OAuthRfcCodes.INVALID_REQUEST,
640
- message: "Invalid code_challenge."
641
- };
290
+ if (!isValidCodeChallenge(parsed.code_challenge)) {
291
+ return {
292
+ errorKey: OAuthRfcCodes.INVALID_REQUEST,
293
+ message: "Invalid code_challenge."
294
+ };
642
295
  }
643
296
  }
644
297
  return null;
@@ -678,44 +331,36 @@ function parseScopeList(scope) {
678
331
  return scope.trim().split(/\s+/).filter(Boolean);
679
332
  }
680
333
 
681
- // src/server/services/OAuthWrapperService.ts
682
- var AUTH_CODE_TTL_MS = 5 * 60 * 1e3;
683
- var OAuthWrapperService = class {
684
- constructor(oauthSession, userAdapter, tokenService, oauthRepo) {
685
- this.oauthSession = oauthSession;
686
- this.userAdapter = userAdapter;
687
- this.tokenService = tokenService;
688
- this.oauthRepo = oauthRepo;
689
- }
690
- /**
691
- * @override
692
- */
693
- getOAuthSession() {
694
- return this.oauthSession;
334
+ // src/server/services/OAuthAbstractProvider.ts
335
+ import { ExecutorError } from "@qlover/fe-corekit";
336
+ import { randomBytes } from "crypto";
337
+ var OAuthAbstractProvider = class {
338
+ authCodeTTLMs = 5 * 60 * 1e3;
339
+ isQuery(query) {
340
+ return OAuthAuthorizeQuerySchema.safeParse(query).success;
695
341
  }
696
- /**
697
- * @override
698
- */
699
- getOAuthAdapter() {
700
- return this.userAdapter;
342
+ isValidateConsent(value) {
343
+ return OAuthConsentBodySchema.safeParse(value).success;
701
344
  }
702
- /**
703
- * @override
704
- */
705
- getOAuthTokenService() {
706
- return this.tokenService;
345
+ generageSessionPayload(userInfo) {
346
+ return {
347
+ userId: String(userInfo.id),
348
+ email: userInfo.email,
349
+ name: userInfo.name ?? userInfo.email,
350
+ providerSessionToken: userInfo.sessionToken
351
+ };
707
352
  }
708
353
  /**
709
354
  * @override
710
355
  */
711
- getOAuthRepo() {
712
- return this.oauthRepo;
713
- }
714
- isQuery(query) {
715
- return OAuthAuthorizeQuerySchema.safeParse(query).success;
716
- }
717
- isValidateConsent(value) {
718
- return OAuthConsentBodySchema.safeParse(value).success;
356
+ async logout(userId) {
357
+ const oauthRepo = this.getOAuthRepo();
358
+ await oauthRepo.revokeRefreshTokensByUserId(userId);
359
+ await oauthRepo.upsertUserCredentials(userId, {
360
+ provider_refresh_token: null,
361
+ provider_session_token: null
362
+ });
363
+ await this.clearSession();
719
364
  }
720
365
  /**
721
366
  * @override
@@ -740,7 +385,8 @@ var OAuthWrapperService = class {
740
385
  }
741
386
  };
742
387
  }
743
- const client = await this.oauthRepo.findClientById(query.client_id);
388
+ const oauthRepo = this.getOAuthRepo();
389
+ const client = await oauthRepo.findClientById(query.client_id);
744
390
  if (!client) {
745
391
  return {
746
392
  ok: false,
@@ -798,14 +444,14 @@ var OAuthWrapperService = class {
798
444
  */
799
445
  async processConsent(requestBody) {
800
446
  if (!this.isValidateConsent(requestBody)) {
801
- throw new ExecutorError2(
447
+ throw new ExecutorError(
802
448
  OAuthRfcCodes.INVALID_REQUEST,
803
449
  "Invalid consent request body"
804
450
  );
805
451
  }
806
- const session = await this.oauthSession.getSession();
452
+ const session = await this.getSession();
807
453
  if (!session) {
808
- throw new ExecutorError2(
454
+ throw new ExecutorError(
809
455
  OAuthRfcCodes.ACCESS_DENIED,
810
456
  "User session expired. Please sign in again."
811
457
  );
@@ -820,7 +466,7 @@ var OAuthWrapperService = class {
820
466
  code_challenge_method: requestBody.code_challenge_method
821
467
  });
822
468
  if (!pageResult.ok) {
823
- throw new ExecutorError2(
469
+ throw new ExecutorError(
824
470
  pageResult.error.errorKey,
825
471
  pageResult.error.message
826
472
  );
@@ -835,9 +481,10 @@ var OAuthWrapperService = class {
835
481
  })
836
482
  };
837
483
  }
838
- const code = randomBytes2(32).toString("base64url");
839
- const expiresAt = new Date(Date.now() + AUTH_CODE_TTL_MS).toISOString();
840
- await this.oauthRepo.create({
484
+ const code = randomBytes(32).toString("base64url");
485
+ const expiresAt = new Date(Date.now() + this.authCodeTTLMs).toISOString();
486
+ const oauthRepo = this.getOAuthRepo();
487
+ await oauthRepo.create({
841
488
  code,
842
489
  client_id: data.clientId,
843
490
  user_id: session.userId,
@@ -855,21 +502,423 @@ var OAuthWrapperService = class {
855
502
  })
856
503
  };
857
504
  }
505
+ };
506
+
507
+ // src/server/services/OAuthClientsService.ts
508
+ var OAuthClientsService = class {
509
+ constructor(clientsRepo) {
510
+ this.clientsRepo = clientsRepo;
511
+ }
512
+ clientsRepo;
858
513
  /**
514
+ * List all OAuth clients owned by a user
859
515
  * @override
860
516
  */
861
- async exchangeToken(rawFields) {
862
- return await this.tokenService.exchangeToken(rawFields);
517
+ async listForOwner(ownerUserId) {
518
+ return this.clientsRepo.listClientByOwner(ownerUserId);
863
519
  }
864
520
  /**
521
+ * Get detailed information about a specific OAuth client
865
522
  * @override
866
523
  */
867
- async revokeToken(rawFields) {
868
- return await this.tokenService.revokeToken(rawFields);
524
+ async getByClientId(ownerUserId, clientId) {
525
+ const client = await this.clientsRepo.findClientById(clientId);
526
+ if (!client) {
527
+ throw new Error("Client not found");
528
+ }
529
+ if (client.owner_user_id !== ownerUserId) {
530
+ throw new Error("Access denied");
531
+ }
532
+ return this.mapToDetail(client);
533
+ }
534
+ /**
535
+ * Create a new OAuth client
536
+ * @override
537
+ */
538
+ async create(ownerUserId, input) {
539
+ const result = await this.clientsRepo.createClient(ownerUserId, input);
540
+ return {
541
+ client_id: result.client.client_id,
542
+ client_secret: result.clientSecret,
543
+ confidential: result.client.confidential,
544
+ client_name: result.client.client_name,
545
+ client_uri: result.client.client_uri,
546
+ redirect_uris: result.client.redirect_uris,
547
+ created_at: result.client.created_at
548
+ };
549
+ }
550
+ /**
551
+ * Update an existing OAuth client
552
+ * @override
553
+ */
554
+ async update(ownerUserId, clientId, input) {
555
+ const existing = await this.clientsRepo.findClientById(clientId);
556
+ if (!existing) {
557
+ throw new Error("Client not found");
558
+ }
559
+ if (existing.owner_user_id !== ownerUserId) {
560
+ throw new Error("Access denied");
561
+ }
562
+ return this.clientsRepo.updateClient(ownerUserId, clientId, input);
563
+ }
564
+ /**
565
+ * Rotate the client secret
566
+ * @override
567
+ */
568
+ async rotateSecret(ownerUserId, clientId) {
569
+ const existing = await this.clientsRepo.findClientById(clientId);
570
+ if (!existing) {
571
+ throw new Error("Client not found");
572
+ }
573
+ if (existing.owner_user_id !== ownerUserId) {
574
+ throw new Error("Access denied");
575
+ }
576
+ if (!existing.confidential) {
577
+ throw new Error("Public clients do not have a client_secret");
578
+ }
579
+ const result = await this.clientsRepo.rotateClientSecret(
580
+ ownerUserId,
581
+ clientId
582
+ );
583
+ return {
584
+ client_id: clientId,
585
+ client_secret: result.clientSecret
586
+ };
869
587
  }
870
588
  /**
589
+ * Delete an OAuth client
871
590
  * @override
872
591
  */
592
+ async delete(ownerUserId, clientId) {
593
+ const existing = await this.clientsRepo.findClientById(clientId);
594
+ if (!existing) {
595
+ throw new Error("Client not found");
596
+ }
597
+ if (existing.owner_user_id !== ownerUserId) {
598
+ throw new Error("Access denied");
599
+ }
600
+ await this.clientsRepo.deleteClient(ownerUserId, clientId);
601
+ }
602
+ mapToDetail(row) {
603
+ return {
604
+ client_id: row.client_id,
605
+ client_name: row.client_name,
606
+ client_uri: row.client_uri,
607
+ logo_uri: row.logo_uri,
608
+ redirect_uris: row.redirect_uris,
609
+ grant_types: row.grant_types,
610
+ scopes: row.scopes,
611
+ confidential: row.confidential,
612
+ created_at: row.created_at,
613
+ updated_at: row.updated_at
614
+ };
615
+ }
616
+ };
617
+
618
+ // src/server/services/OAuthTokenService.ts
619
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
620
+
621
+ // src/server/utils/OAuthWrapperError.ts
622
+ import { ExecutorError as ExecutorError2 } from "@qlover/fe-corekit";
623
+ var OAuthWrapperError = class extends ExecutorError2 {
624
+ constructor(id, status, cause) {
625
+ super(id, cause);
626
+ this.status = status;
627
+ }
628
+ status;
629
+ name = "OAuthWrapperError";
630
+ };
631
+
632
+ // src/server/services/OAuthTokenService.ts
633
+ function hashOpaqueToken(token) {
634
+ return createHash2("sha256").update(token).digest("hex");
635
+ }
636
+ var OAuthTokenService = class _OAuthTokenService {
637
+ constructor(tokenEncryption, exchangeProviderAccessToken, oauthRepo) {
638
+ this.tokenEncryption = tokenEncryption;
639
+ this.exchangeProviderAccessToken = exchangeProviderAccessToken;
640
+ this.oauthRepo = oauthRepo;
641
+ }
642
+ tokenEncryption;
643
+ exchangeProviderAccessToken;
644
+ oauthRepo;
645
+ static REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
646
+ /**
647
+ * @override
648
+ */
649
+ async exchangeToken(rawFields) {
650
+ let request;
651
+ try {
652
+ request = OAuthTokenRequestSchema.parse(rawFields);
653
+ } catch {
654
+ throw new OAuthWrapperError(
655
+ OAuthRfcCodes.INVALID_REQUEST,
656
+ 400,
657
+ "Malformed token request"
658
+ );
659
+ }
660
+ let client;
661
+ try {
662
+ client = await this.oauthRepo.verifyClientCredentials(
663
+ request.client_id,
664
+ request.client_secret
665
+ );
666
+ } catch {
667
+ throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
668
+ }
669
+ if (!client.grant_types.includes(request.grant_type)) {
670
+ throw new OAuthWrapperError(OAuthRfcCodes.UNSUPPORTED_GRANT_TYPE, 400);
671
+ }
672
+ if (request.grant_type === "authorization_code") {
673
+ return await this.exchangeAuthorizationCode(request, client.client_id);
674
+ }
675
+ return await this.exchangeRefreshToken(request, client.client_id);
676
+ }
677
+ async exchangeAuthorizationCode(request, verifiedClientId) {
678
+ const authCode = await this.oauthRepo.consumeCode(request.code);
679
+ if (!authCode) {
680
+ throw new OAuthWrapperError(
681
+ OAuthRfcCodes.INVALID_GRANT,
682
+ 400,
683
+ "Authorization code is invalid, expired, or already used"
684
+ );
685
+ }
686
+ if (authCode.client_id !== verifiedClientId) {
687
+ throw new OAuthWrapperError(
688
+ OAuthRfcCodes.INVALID_GRANT,
689
+ 400,
690
+ "client_id mismatch"
691
+ );
692
+ }
693
+ if (authCode.redirect_uri !== request.redirect_uri) {
694
+ throw new OAuthWrapperError(
695
+ OAuthRfcCodes.INVALID_GRANT,
696
+ 400,
697
+ "redirect_uri mismatch"
698
+ );
699
+ }
700
+ this.assertPkceForAuthorizationCode(request, authCode);
701
+ const providerTokens = await this.fetchProviderAccessToken(
702
+ authCode.user_id
703
+ );
704
+ const middlewareRefresh = await this.issueMiddlewareRefreshToken(
705
+ authCode.client_id,
706
+ authCode.user_id
707
+ );
708
+ return {
709
+ access_token: providerTokens.access_token,
710
+ token_type: "Bearer",
711
+ expires_in: providerTokens.expires_in,
712
+ refresh_token: middlewareRefresh,
713
+ scope: authCode.scope ?? void 0
714
+ };
715
+ }
716
+ assertPkceForAuthorizationCode(request, authCode) {
717
+ const storedChallenge = authCode.code_challenge?.trim();
718
+ if (storedChallenge) {
719
+ const verifier = request.code_verifier?.trim();
720
+ if (!verifier) {
721
+ throw new OAuthWrapperError(
722
+ OAuthRfcCodes.INVALID_GRANT,
723
+ 400,
724
+ "code_verifier is required"
725
+ );
726
+ }
727
+ if (authCode.code_challenge_method !== "S256") {
728
+ throw new OAuthWrapperError(
729
+ OAuthRfcCodes.INVALID_GRANT,
730
+ 400,
731
+ "Unsupported PKCE method"
732
+ );
733
+ }
734
+ if (!verifyPkceS256(verifier, storedChallenge)) {
735
+ throw new OAuthWrapperError(
736
+ OAuthRfcCodes.INVALID_GRANT,
737
+ 400,
738
+ "code_verifier mismatch"
739
+ );
740
+ }
741
+ return;
742
+ }
743
+ if (!request.client_secret?.trim()) {
744
+ throw new OAuthWrapperError(
745
+ OAuthRfcCodes.INVALID_CLIENT,
746
+ 401,
747
+ "client_secret is required when PKCE is not used"
748
+ );
749
+ }
750
+ }
751
+ async exchangeRefreshToken(request, verifiedClientId) {
752
+ const tokenHash = hashOpaqueToken(request.refresh_token);
753
+ const stored = await this.oauthRepo.findByTokenHash(tokenHash);
754
+ if (!stored || stored.revoked || stored.client_id !== verifiedClientId || new Date(stored.expires_at) <= /* @__PURE__ */ new Date()) {
755
+ throw new OAuthWrapperError(
756
+ OAuthRfcCodes.INVALID_GRANT,
757
+ 400,
758
+ "Refresh token is invalid"
759
+ );
760
+ }
761
+ const providerTokens = await this.fetchProviderAccessToken(stored.user_id);
762
+ await this.oauthRepo.revokeByTokenHash(tokenHash);
763
+ const middlewareRefresh = await this.issueMiddlewareRefreshToken(
764
+ stored.client_id,
765
+ stored.user_id
766
+ );
767
+ return {
768
+ access_token: providerTokens.access_token,
769
+ token_type: "Bearer",
770
+ expires_in: providerTokens.expires_in,
771
+ refresh_token: middlewareRefresh
772
+ };
773
+ }
774
+ async fetchProviderAccessToken(userId) {
775
+ const credentials = await this.oauthRepo.getUserCredentials(userId);
776
+ const sessionToken = credentials?.provider_session_token?.trim();
777
+ if (!sessionToken) {
778
+ throw new OAuthWrapperError(
779
+ OAuthRfcCodes.INVALID_GRANT,
780
+ 400,
781
+ "User credentials expired. Re-authorization required."
782
+ );
783
+ }
784
+ try {
785
+ const access = await this.exchangeProviderAccessToken({
786
+ token: sessionToken
787
+ });
788
+ if (access.refresh_token) {
789
+ await this.oauthRepo.upsertUserCredentials(userId, {
790
+ provider_refresh_token: this.tokenEncryption.encrypt(
791
+ access.refresh_token
792
+ )
793
+ });
794
+ }
795
+ return {
796
+ access_token: access.access_token,
797
+ expires_in: access.expires_in
798
+ };
799
+ } catch {
800
+ throw new OAuthWrapperError(
801
+ OAuthRfcCodes.INVALID_GRANT,
802
+ 400,
803
+ "Failed to obtain access token from user provider"
804
+ );
805
+ }
806
+ }
807
+ async issueMiddlewareRefreshToken(clientId, userId) {
808
+ const plain = randomBytes2(32).toString("base64url");
809
+ const expiresAt = new Date(
810
+ Date.now() + _OAuthTokenService.REFRESH_TOKEN_TTL_MS
811
+ ).toISOString();
812
+ await this.oauthRepo.createRefreshToken({
813
+ refresh_token: hashOpaqueToken(plain),
814
+ client_id: clientId,
815
+ user_id: userId,
816
+ expires_at: expiresAt
817
+ });
818
+ return plain;
819
+ }
820
+ /**
821
+ * @override
822
+ * RFC 7009 — revoke middleware refresh tokens. Idempotent: unknown tokens are ignored.
823
+ */
824
+ async revokeToken(rawFields) {
825
+ let request;
826
+ try {
827
+ request = OAuthTokenRevokeSchema.parse(rawFields);
828
+ } catch {
829
+ throw new OAuthWrapperError(
830
+ OAuthRfcCodes.INVALID_REQUEST,
831
+ 400,
832
+ "Malformed revocation request"
833
+ );
834
+ }
835
+ try {
836
+ await this.oauthRepo.verifyClientCredentials(
837
+ request.client_id,
838
+ request.client_secret
839
+ );
840
+ } catch {
841
+ throw new OAuthWrapperError(OAuthRfcCodes.INVALID_CLIENT, 401);
842
+ }
843
+ if (request.token_type_hint && request.token_type_hint !== "refresh_token") {
844
+ return;
845
+ }
846
+ const tokenHash = hashOpaqueToken(request.token);
847
+ const stored = await this.oauthRepo.findByTokenHash(tokenHash);
848
+ if (!stored || stored.client_id !== request.client_id) {
849
+ return;
850
+ }
851
+ await this.oauthRepo.revokeByTokenHash(tokenHash);
852
+ }
853
+ };
854
+
855
+ // src/server/services/OAuthWrapperService.ts
856
+ var OAuthWrapperService = class extends OAuthAbstractProvider {
857
+ constructor(oauthSession, tokenEncryption, oauthRepo) {
858
+ super();
859
+ this.oauthSession = oauthSession;
860
+ this.oauthRepo = oauthRepo;
861
+ this.tokenService = new OAuthTokenService(
862
+ tokenEncryption,
863
+ (params) => this.providerExchangeAccessToken(params),
864
+ oauthRepo
865
+ );
866
+ }
867
+ oauthSession;
868
+ oauthRepo;
869
+ tokenService;
870
+ /**
871
+ * @override
872
+ */
873
+ async login(params) {
874
+ const credentials = await this.providerLogin(params);
875
+ const sessionToken = credentials.token;
876
+ if (!sessionToken) {
877
+ throw new Error("User provider login did not return a session token");
878
+ }
879
+ const userInfo = await this.providerGetUserInfo(sessionToken);
880
+ const sessionPayload = this.generageSessionPayload({
881
+ email: params.email,
882
+ ...userInfo,
883
+ sessionToken
884
+ });
885
+ this.oauthSession.setSession(sessionPayload);
886
+ const oauthrepo = this.getOAuthRepo();
887
+ await oauthrepo.upsertUserCredentials(sessionPayload.userId, {
888
+ provider_session_token: sessionPayload.providerSessionToken
889
+ });
890
+ return sessionPayload;
891
+ }
892
+ /**
893
+ * @override
894
+ */
895
+ getOAuthRepo() {
896
+ return this.oauthRepo;
897
+ }
898
+ /**
899
+ * @override
900
+ */
901
+ async exchangeToken(rawFields) {
902
+ return await this.tokenService.exchangeToken(rawFields);
903
+ }
904
+ /**
905
+ * @override
906
+ */
907
+ getSession() {
908
+ return this.oauthSession.getSession();
909
+ }
910
+ /**
911
+ * @override
912
+ */
913
+ clearSession() {
914
+ return this.oauthSession.clearSession();
915
+ }
916
+ /**
917
+ * @override
918
+ */
919
+ async revokeToken(rawFields) {
920
+ return await this.tokenService.revokeToken(rawFields);
921
+ }
873
922
  async logoutUser(userId) {
874
923
  await this.oauthRepo.revokeRefreshTokensByUserId(userId);
875
924
  await this.oauthRepo.upsertUserCredentials(userId, {
@@ -881,9 +930,9 @@ var OAuthWrapperService = class {
881
930
  /**
882
931
  * @override
883
932
  */
884
- async getUserInfo(accessToken) {
933
+ async getUserInfoWithAccessToken(accessToken) {
885
934
  try {
886
- const profile = await this.userAdapter.getUserInfoByAccessToken(accessToken);
935
+ const profile = await this.providerGetUserInfoByAccessToken(accessToken);
887
936
  return this.toUserInfoResponse(profile);
888
937
  } catch {
889
938
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
@@ -898,8 +947,7 @@ var OAuthWrapperService = class {
898
947
  if (!email) {
899
948
  throw new OAuthWrapperError(OAuthRfcCodes.INVALID_TOKEN, 401);
900
949
  }
901
- const nameFromParts = [profile.first_name, profile.last_name].filter(Boolean).join(" ");
902
- const name = profile.name?.trim() || nameFromParts || email;
950
+ const name = profile.name?.trim() || email;
903
951
  return {
904
952
  sub,
905
953
  email,
@@ -934,6 +982,7 @@ async function verifyClientSecret(secret, storedHash) {
934
982
  return timingSafeEqual2(derived, expected);
935
983
  }
936
984
  export {
985
+ OAuthAbstractProvider,
937
986
  OAuthAuthorizationCodeRowSchema,
938
987
  OAuthAuthorizeQuerySchema,
939
988
  OAuthClientCreateResponseSchema,